diff --git a/src/Overflow.tsx b/src/Overflow.tsx index d4c6b83..c854fba 100644 --- a/src/Overflow.tsx +++ b/src/Overflow.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import ResizeObserver from 'rc-resize-observer'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import Item from './Item'; -import { useBatchFrameState } from './hooks/useBatchFrameState'; +import useEffectState, { useBatcher } from './hooks/useEffectState'; import RawItem from './RawItem'; export const OverflowContext = React.createContext<{ @@ -89,24 +89,37 @@ function Overflow( ...restProps } = props; - const createUseState = useBatchFrameState(); - const fullySSR = ssr === 'full'; - const [containerWidth, setContainerWidth] = createUseState(null); + const notifyEffectUpdate = useBatcher(); + + const [containerWidth, setContainerWidth] = useEffectState( + notifyEffectUpdate, + null, + ); const mergedContainerWidth = containerWidth || 0; - const [itemWidths, setItemWidths] = createUseState( + const [itemWidths, setItemWidths] = useEffectState( + notifyEffectUpdate, new Map(), ); - const [prevRestWidth, setPrevRestWidth] = createUseState(0); - const [restWidth, setRestWidth] = createUseState(0); + const [prevRestWidth, setPrevRestWidth] = useEffectState( + notifyEffectUpdate, + 0, + ); + const [restWidth, setRestWidth] = useEffectState( + notifyEffectUpdate, + 0, + ); - const [suffixWidth, setSuffixWidth] = createUseState(0); + const [suffixWidth, setSuffixWidth] = useEffectState( + notifyEffectUpdate, + 0, + ); const [suffixFixedStart, setSuffixFixedStart] = useState(null); - const [displayCount, setDisplayCount] = useState(null); + const [displayCount, setDisplayCount] = useState(null); const mergedDisplayCount = React.useMemo(() => { if (displayCount === null && fullySSR) { return Number.MAX_SAFE_INTEGER; diff --git a/src/hooks/channelUpdate.ts b/src/hooks/channelUpdate.ts new file mode 100644 index 0000000..fabb27a --- /dev/null +++ b/src/hooks/channelUpdate.ts @@ -0,0 +1,11 @@ +import raf from 'rc-util/lib/raf'; + +export default function channelUpdate(callback: VoidFunction) { + if (typeof MessageChannel === 'undefined') { + raf(callback); + } else { + const channel = new MessageChannel(); + channel.port1.onmessage = () => callback(); + channel.port2.postMessage(undefined); + } +} diff --git a/src/hooks/useBatchFrameState.tsx b/src/hooks/useBatchFrameState.tsx deleted file mode 100644 index 601377d..0000000 --- a/src/hooks/useBatchFrameState.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef } from 'react'; -import raf from 'rc-util/lib/raf'; -import useState from 'rc-util/lib/hooks/useState'; - -/** - * State generate. Return a `setState` but it will flush all state with one render to save perf. - * This is not a realization of `unstable_batchedUpdates`. - */ -export function useBatchFrameState() { - const [, forceUpdate] = useState({}); - const statesRef = useRef([]); - let walkingIndex = 0; - let beforeFrameId: number = 0; - - function createState( - defaultValue: T, - ): [T, (value: T | ((origin: T) => T)) => void] { - const myIndex = walkingIndex; - walkingIndex += 1; - - // Fill value if not exist yet - if (statesRef.current.length < myIndex + 1) { - statesRef.current[myIndex] = defaultValue; - } - - // Return filled as `setState` - const value = statesRef.current[myIndex]; - - function setValue(val: any) { - statesRef.current[myIndex] = - typeof val === 'function' ? val(statesRef.current[myIndex]) : val; - - raf.cancel(beforeFrameId); - - // Flush with batch - beforeFrameId = raf(() => { - forceUpdate({}, true); - }); - } - - return [value, setValue]; - } - - return createState; -} diff --git a/src/hooks/useEffectState.tsx b/src/hooks/useEffectState.tsx new file mode 100644 index 0000000..02833f2 --- /dev/null +++ b/src/hooks/useEffectState.tsx @@ -0,0 +1,58 @@ +import useEvent from 'rc-util/lib/hooks/useEvent'; +import * as React from 'react'; +import { unstable_batchedUpdates } from 'react-dom'; +import channelUpdate from './channelUpdate'; + +type Updater = T | ((origin: T) => T); + +type UpdateCallbackFunc = VoidFunction; + +type NotifyEffectUpdate = (callback: UpdateCallbackFunc) => void; + +/** + * Batcher for record any `useEffectState` need update. + */ +export function useBatcher() { + // Updater Trigger + const updateFuncRef = React.useRef(null); + + // Notify update + const notifyEffectUpdate: NotifyEffectUpdate = callback => { + if (!updateFuncRef.current) { + updateFuncRef.current = []; + + channelUpdate(() => { + unstable_batchedUpdates(() => { + updateFuncRef.current.forEach(fn => { + fn(); + }); + updateFuncRef.current = null; + }); + }); + } + + updateFuncRef.current.push(callback); + }; + + return notifyEffectUpdate; +} + +/** + * Trigger state update by `useLayoutEffect` to save perf. + */ +export default function useEffectState( + notifyEffectUpdate: NotifyEffectUpdate, + defaultValue?: T, +): [T, (value: Updater) => void] { + // Value + const [stateValue, setStateValue] = React.useState(defaultValue); + + // Set State + const setEffectVal = useEvent((nextValue: Updater) => { + notifyEffectUpdate(() => { + setStateValue(nextValue); + }); + }); + + return [stateValue, setEffectVal]; +} diff --git a/src/index.tsx b/src/index.tsx index dbd789d..b89a2ba 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ -import Overflow, { OverflowProps } from './Overflow'; +import Overflow from './Overflow'; +import type { OverflowProps } from './Overflow'; -export { OverflowProps }; +export type { OverflowProps }; export default Overflow;