feat: higher fps & latency less than 5ms
This commit is contained in:
@@ -1,84 +1,177 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
|
||||||
import { useSocketStore } from '../../context/socket';
|
import { useSocketStore } from '../../context/socket';
|
||||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
import { useActionContext } from '../../context/browserActions';
|
import { useActionContext } from '../../context/browserActions';
|
||||||
import DatePicker from './DatePicker';
|
|
||||||
import Dropdown from './Dropdown';
|
|
||||||
import TimePicker from './TimePicker';
|
|
||||||
import DateTimeLocalPicker from './DateTimeLocalPicker';
|
|
||||||
import { FrontendPerformanceMonitor } from '../../../perf/performance';
|
import { FrontendPerformanceMonitor } from '../../../perf/performance';
|
||||||
|
|
||||||
// Optimized throttle with RAF
|
// Lazy load components that aren't always needed
|
||||||
const rafThrottle = <T extends (...args: any[]) => any>(callback: T) => {
|
const DatePicker = React.lazy(() => import('./DatePicker'));
|
||||||
let requestId: number | null = null;
|
const Dropdown = React.lazy(() => import('./Dropdown'));
|
||||||
let lastArgs: Parameters<T>;
|
const TimePicker = React.lazy(() => import('./TimePicker'));
|
||||||
|
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
|
||||||
|
|
||||||
const later = () => {
|
// High-performance RAF scheduler
|
||||||
requestId = null;
|
class RAFScheduler {
|
||||||
callback.apply(null, lastArgs);
|
private queue: Set<() => void> = new Set();
|
||||||
};
|
private isProcessing: boolean = false;
|
||||||
|
private frameId: number | null = null;
|
||||||
|
|
||||||
return (...args: Parameters<T>) => {
|
schedule(callback: () => void): void {
|
||||||
lastArgs = args;
|
this.queue.add(callback);
|
||||||
if (requestId === null) {
|
if (!this.isProcessing) {
|
||||||
requestId = requestAnimationFrame(later);
|
this.process();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cache DOM measurements
|
|
||||||
let measurementCache = new WeakMap<HTMLElement, DOMRect>();
|
|
||||||
const getBoundingClientRectCached = (element: HTMLElement) => {
|
|
||||||
let rect = measurementCache.get(element);
|
|
||||||
if (!rect) {
|
|
||||||
rect = element.getBoundingClientRect();
|
|
||||||
measurementCache.set(element, rect);
|
|
||||||
}
|
}
|
||||||
return rect;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Types (kept the same)
|
private process = (): void => {
|
||||||
interface CreateRefCallback {
|
this.isProcessing = true;
|
||||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
this.frameId = requestAnimationFrame(() => {
|
||||||
|
const callbacks = Array.from(this.queue);
|
||||||
|
this.queue.clear();
|
||||||
|
|
||||||
|
callbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('RAF Scheduler error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isProcessing = false;
|
||||||
|
this.frameId = null;
|
||||||
|
|
||||||
|
if (this.queue.size > 0) {
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.queue.clear();
|
||||||
|
if (this.frameId !== null) {
|
||||||
|
cancelAnimationFrame(this.frameId);
|
||||||
|
this.frameId = null;
|
||||||
|
}
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced event debouncer with priority queue
|
||||||
|
class EventDebouncer {
|
||||||
|
private highPriorityQueue: Array<() => void> = [];
|
||||||
|
private lowPriorityQueue: Array<() => void> = [];
|
||||||
|
private processing: boolean = false;
|
||||||
|
private scheduler: RAFScheduler;
|
||||||
|
|
||||||
|
constructor(scheduler: RAFScheduler) {
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(callback: () => void, highPriority: boolean = false): void {
|
||||||
|
if (highPriority) {
|
||||||
|
this.highPriorityQueue.push(callback);
|
||||||
|
} else {
|
||||||
|
this.lowPriorityQueue.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.processing) {
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private process(): void {
|
||||||
|
this.processing = true;
|
||||||
|
this.scheduler.schedule(() => {
|
||||||
|
while (this.highPriorityQueue.length > 0) {
|
||||||
|
const callback = this.highPriorityQueue.shift();
|
||||||
|
callback?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lowPriorityQueue.length > 0) {
|
||||||
|
const callback = this.lowPriorityQueue.shift();
|
||||||
|
callback?.();
|
||||||
|
|
||||||
|
if (this.lowPriorityQueue.length > 0) {
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.highPriorityQueue = [];
|
||||||
|
this.lowPriorityQueue = [];
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized measurement cache with LRU
|
||||||
|
class MeasurementCache {
|
||||||
|
private cache: Map<HTMLElement, DOMRect>;
|
||||||
|
private maxSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number = 100) {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(element: HTMLElement): DOMRect | undefined {
|
||||||
|
const cached = this.cache.get(element);
|
||||||
|
if (cached) {
|
||||||
|
// Refresh the entry
|
||||||
|
this.cache.delete(element);
|
||||||
|
this.cache.set(element, cached);
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(element: HTMLElement, rect: DOMRect): void {
|
||||||
|
if (this.cache.size >= this.maxSize) {
|
||||||
|
// Remove oldest entry
|
||||||
|
const firstKey = this.cache.keys().next().value;
|
||||||
|
if (firstKey !== undefined) {
|
||||||
|
this.cache.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache.set(element, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasProps {
|
interface CanvasProps {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
onCreateRef: CreateRefCallback;
|
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Coordinates {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch updates helper
|
|
||||||
const batchedUpdates = (updates: Array<() => void>) => {
|
|
||||||
unstable_batchedUpdates(() => {
|
|
||||||
updates.forEach(update => update());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
||||||
const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
// Core refs and state
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
const { setLastAction } = useGlobalInfoStore();
|
||||||
const { getText, getList } = useActionContext();
|
const { getText, getList } = useActionContext();
|
||||||
|
|
||||||
// Use a single ref object to reduce memory allocations
|
// Performance optimization instances
|
||||||
|
const scheduler = useRef(new RAFScheduler());
|
||||||
|
const debouncer = useRef(new EventDebouncer(scheduler.current));
|
||||||
|
const measurementCache = useRef(new MeasurementCache(50));
|
||||||
|
const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
||||||
|
|
||||||
|
// Consolidated refs
|
||||||
const refs = useRef({
|
const refs = useRef({
|
||||||
getText,
|
getText,
|
||||||
getList,
|
getList,
|
||||||
lastMousePosition: { x: 0, y: 0 },
|
lastMousePosition: { x: 0, y: 0 },
|
||||||
frameRequest: 0,
|
lastFrameTime: 0,
|
||||||
eventQueue: [] as Array<() => void>,
|
context: null as CanvasRenderingContext2D | null,
|
||||||
isProcessing: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Consolidated state using a single reducer
|
// Optimized state management
|
||||||
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'BATCH_UPDATE':
|
case 'BATCH_UPDATE':
|
||||||
@@ -93,53 +186,32 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
|||||||
dateTimeLocalInfo: null
|
dateTimeLocalInfo: null
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process events in batches
|
// Optimized coordinate calculation
|
||||||
const processEventQueue = useCallback(() => {
|
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
|
||||||
if (refs.current.isProcessing || refs.current.eventQueue.length === 0) return;
|
if (!canvasRef.current) return { x: 0, y: 0 };
|
||||||
|
|
||||||
refs.current.isProcessing = true;
|
|
||||||
const events = [...refs.current.eventQueue];
|
|
||||||
refs.current.eventQueue = [];
|
|
||||||
|
|
||||||
batchedUpdates(events.map(event => () => event()));
|
let rect = measurementCache.current.get(canvasRef.current);
|
||||||
|
if (!rect) {
|
||||||
refs.current.isProcessing = false;
|
rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
measurementCache.current.set(canvasRef.current, rect);
|
||||||
if (refs.current.eventQueue.length > 0) {
|
|
||||||
requestAnimationFrame(processEventQueue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: event.clientX - rect.left,
|
||||||
|
y: event.clientY - rect.top
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Optimized mouse move handler using RAF throttle
|
// High-performance mouse handler
|
||||||
const handleMouseMove = useMemo(
|
const handleMouseEvent = useCallback((event: MouseEvent) => {
|
||||||
() => rafThrottle((coordinates: Coordinates) => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const current = refs.current.lastMousePosition;
|
|
||||||
if (current.x !== coordinates.x || current.y !== coordinates.y) {
|
|
||||||
refs.current.lastMousePosition = coordinates;
|
|
||||||
socket.emit('input:mousemove', coordinates);
|
|
||||||
refs.current.eventQueue.push(() => setLastAction('move'));
|
|
||||||
requestAnimationFrame(processEventQueue);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[socket, processEventQueue]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Optimized event handler with better performance characteristics
|
|
||||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
|
||||||
if (!socket || !canvasRef.current) return;
|
if (!socket || !canvasRef.current) return;
|
||||||
|
|
||||||
performanceMonitor.current.measureEventLatency(event);
|
performanceMonitor.current.measureEventLatency(event);
|
||||||
const rect = getBoundingClientRectCached(canvasRef.current);
|
const coordinates = getEventCoordinates(event);
|
||||||
const coordinates = {
|
|
||||||
x: event.clientX - rect.left,
|
|
||||||
y: event.clientY - rect.top,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'mousedown':
|
case 'mousedown':
|
||||||
refs.current.eventQueue.push(() => {
|
debouncer.current.add(() => {
|
||||||
if (refs.current.getText) {
|
if (refs.current.getText) {
|
||||||
console.log('Capturing Text...');
|
console.log('Capturing Text...');
|
||||||
} else if (refs.current.getList) {
|
} else if (refs.current.getList) {
|
||||||
@@ -148,62 +220,95 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
|||||||
socket.emit('input:mousedown', coordinates);
|
socket.emit('input:mousedown', coordinates);
|
||||||
}
|
}
|
||||||
setLastAction('click');
|
setLastAction('click');
|
||||||
});
|
}, true); // High priority
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mousemove':
|
case 'mousemove':
|
||||||
handleMouseMove(coordinates);
|
if (refs.current.lastMousePosition.x !== coordinates.x ||
|
||||||
|
refs.current.lastMousePosition.y !== coordinates.y) {
|
||||||
|
debouncer.current.add(() => {
|
||||||
|
refs.current.lastMousePosition = coordinates;
|
||||||
|
socket.emit('input:mousemove', coordinates);
|
||||||
|
setLastAction('move');
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'wheel':
|
case 'wheel':
|
||||||
if (refs.current.frameRequest) {
|
const wheelEvent = event as WheelEvent;
|
||||||
cancelAnimationFrame(refs.current.frameRequest);
|
debouncer.current.add(() => {
|
||||||
}
|
|
||||||
refs.current.frameRequest = requestAnimationFrame(() => {
|
|
||||||
const wheelEvent = event as WheelEvent;
|
|
||||||
socket.emit('input:wheel', {
|
socket.emit('input:wheel', {
|
||||||
deltaX: Math.round(wheelEvent.deltaX),
|
deltaX: Math.round(wheelEvent.deltaX),
|
||||||
deltaY: Math.round(wheelEvent.deltaY)
|
deltaY: Math.round(wheelEvent.deltaY)
|
||||||
});
|
});
|
||||||
refs.current.eventQueue.push(() => setLastAction('scroll'));
|
setLastAction('scroll');
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}, [socket, getEventCoordinates]);
|
||||||
requestAnimationFrame(processEventQueue);
|
|
||||||
}, [socket, handleMouseMove, processEventQueue]);
|
|
||||||
|
|
||||||
// Optimized keyboard handler
|
// Optimized keyboard handler
|
||||||
const onKeyboardEvent = useMemo(
|
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||||
() => rafThrottle((event: KeyboardEvent) => {
|
if (!socket) return;
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
refs.current.eventQueue.push(() => {
|
debouncer.current.add(() => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'keydown':
|
case 'keydown':
|
||||||
socket.emit('input:keydown', {
|
socket.emit('input:keydown', {
|
||||||
key: event.key,
|
key: event.key,
|
||||||
coordinates: refs.current.lastMousePosition
|
coordinates: refs.current.lastMousePosition
|
||||||
});
|
});
|
||||||
setLastAction(`${event.key} pressed`);
|
setLastAction(`${event.key} pressed`);
|
||||||
break;
|
break;
|
||||||
case 'keyup':
|
case 'keyup':
|
||||||
socket.emit('input:keyup', event.key);
|
socket.emit('input:keyup', event.key);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}, event.type === 'keydown'); // High priority for keydown
|
||||||
requestAnimationFrame(processEventQueue);
|
}, [socket]);
|
||||||
}),
|
|
||||||
[socket, processEventQueue]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update refs
|
// Setup and cleanup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refs.current.getText = getText;
|
if (!canvasRef.current) return;
|
||||||
refs.current.getList = getList;
|
|
||||||
}, [getText, getList]);
|
|
||||||
|
|
||||||
// Socket event setup with optimized cleanup
|
const canvas = canvasRef.current;
|
||||||
|
refs.current.context = canvas.getContext('2d', {
|
||||||
|
alpha: false,
|
||||||
|
desynchronized: true
|
||||||
|
});
|
||||||
|
|
||||||
|
onCreateRef(canvasRef);
|
||||||
|
|
||||||
|
const options = { passive: true };
|
||||||
|
canvas.addEventListener('mousedown', handleMouseEvent, options);
|
||||||
|
canvas.addEventListener('mousemove', handleMouseEvent, options);
|
||||||
|
canvas.addEventListener('wheel', handleMouseEvent, options);
|
||||||
|
canvas.addEventListener('keydown', handleKeyboardEvent, options);
|
||||||
|
canvas.addEventListener('keyup', handleKeyboardEvent, options);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canvas.removeEventListener('mousedown', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('mousemove', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('wheel', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('keydown', handleKeyboardEvent);
|
||||||
|
canvas.removeEventListener('keyup', handleKeyboardEvent);
|
||||||
|
|
||||||
|
scheduler.current.clear();
|
||||||
|
debouncer.current.clear();
|
||||||
|
measurementCache.current.clear();
|
||||||
|
};
|
||||||
|
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
|
||||||
|
|
||||||
|
// Performance monitoring
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
|
||||||
|
}, 20000); // Reduced frequency
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Socket events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
|
|
||||||
@@ -214,59 +319,13 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
|||||||
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(handlers).forEach(([event, handler]) => {
|
Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
|
||||||
socket.on(event, handler);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Object.keys(handlers).forEach(event => {
|
Object.keys(handlers).forEach(event => socket.off(event));
|
||||||
socket.off(event);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
useEffect(() => {
|
const memoizedDimensions = useMemo(() => ({
|
||||||
const monitor = performanceMonitor.current;
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
console.log('Frontend Performance Report:', monitor.getPerformanceReport());
|
|
||||||
}, 15000); // Increased to 15 seconds
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
if (refs.current.frameRequest) {
|
|
||||||
cancelAnimationFrame(refs.current.frameRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear measurement cache on unmount
|
|
||||||
measurementCache = new WeakMap(); // Reset the WeakMap
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Canvas setup with optimized event binding
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canvasRef.current) return;
|
|
||||||
|
|
||||||
onCreateRef(canvasRef);
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
|
|
||||||
const options = { passive: true };
|
|
||||||
canvas.addEventListener('mousedown', onMouseEvent, options);
|
|
||||||
canvas.addEventListener('mousemove', onMouseEvent, options);
|
|
||||||
canvas.addEventListener('wheel', onMouseEvent, options);
|
|
||||||
canvas.addEventListener('keydown', onKeyboardEvent, options);
|
|
||||||
canvas.addEventListener('keyup', onKeyboardEvent, options);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
canvas.removeEventListener('mousedown', onMouseEvent);
|
|
||||||
canvas.removeEventListener('mousemove', onMouseEvent);
|
|
||||||
canvas.removeEventListener('wheel', onMouseEvent);
|
|
||||||
canvas.removeEventListener('keydown', onKeyboardEvent);
|
|
||||||
canvas.removeEventListener('keyup', onKeyboardEvent);
|
|
||||||
};
|
|
||||||
}, [onMouseEvent, onKeyboardEvent, onCreateRef]);
|
|
||||||
|
|
||||||
const memoizedSize = useMemo(() => ({
|
|
||||||
width: width || 900,
|
width: width || 900,
|
||||||
height: height || 400
|
height: height || 400
|
||||||
}), [width, height]);
|
}), [width, height]);
|
||||||
@@ -276,10 +335,11 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
|||||||
<canvas
|
<canvas
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
height={memoizedSize.height}
|
height={memoizedDimensions.height}
|
||||||
width={memoizedSize.width}
|
width={memoizedDimensions.width}
|
||||||
className="block"
|
className="block"
|
||||||
/>
|
/>
|
||||||
|
<Suspense fallback={null}>
|
||||||
{state.datePickerInfo && (
|
{state.datePickerInfo && (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
coordinates={state.datePickerInfo.coordinates}
|
coordinates={state.datePickerInfo.coordinates}
|
||||||
@@ -304,6 +364,7 @@ const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
|||||||
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
|
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user