UNPKG

@reactodia/workspace

Version:

Reactodia Workspace -- library for visual interaction with graphs in a form of a diagram.

138 lines (128 loc) 4.39 kB
import * as React from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import type { Events } from './events'; /** * Represents an event store which can be subscribed to listen its changes. * * This store exactly the same as accepted by * [React.useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) hook. */ export type SyncStore = (onChange: () => void) => (() => void); /** * Subscribes to a value which changes are tracked by the specified event. * * @param events an observable object to subscribe with the result store * @param key event type from the `events` to subscribe with the result store * @param getSnapshot a function to get a snapshot of an observed property state * @param deps hook dependency list to re-subscribe to the store on changes * @category Hooks * @see {@link useEventStore} * @see {@link useSyncStore} */ export function useObservedProperty<E, K extends keyof E, R>( events: Events<E>, key: K, getSnapshot: () => R, deps?: React.DependencyList ): R { const subscribe = useEventStore(events, key, deps); return useSyncStore(subscribe, getSnapshot); } const NEVER_SYNC_STORE_DISPOSE = (): void => {}; const NEVER_SYNC_STORE: SyncStore = () => NEVER_SYNC_STORE_DISPOSE; /** * An event store that never triggers any change. * * @category Utility */ export function neverSyncStore(): SyncStore { return NEVER_SYNC_STORE; } /** * Creates an event store which changes when an event triggers with the specified event type. * * @param events an observable object to subscribe with the result store * @param key event type from the `events` to subscribe with the result store * @param deps hook dependency list to re-subscribe to the store on changes * @category Hooks */ export function useEventStore<E, K extends keyof E>( events: Events<E> | undefined, key: K, deps?: React.DependencyList ): SyncStore { return React.useCallback((onStoreChange: () => void) => { if (events) { events.on(key, onStoreChange); return () => events.off(key, onStoreChange); } else { return NEVER_SYNC_STORE_DISPOSE; } }, deps ? [events, key, ...deps] : [events, key]); } /** * Transforms event store in a way that the result store debounces the changes * until the next rendered frame via * [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame). * * @category Hooks */ export function useFrameDebouncedStore(subscribe: SyncStore): SyncStore { return React.useCallback<SyncStore>(onChange => { let scheduled: number | undefined; const onFrame = () => { scheduled = undefined; onChange(); }; const dispose = subscribe(() => { if (scheduled === undefined) { scheduled = requestAnimationFrame(onFrame); } }); return () => { if (scheduled !== undefined) { cancelAnimationFrame(scheduled); } dispose(); }; }, [subscribe]); } /** * Same as [React.useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) * with a support shim for lower React versions. * * @category Hooks */ export function useSyncStore<R>(subscribe: SyncStore, getSnapshot: () => R): R { return useSyncExternalStore(subscribe, getSnapshot); } /** * Same as [React.useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) * with custom equality comparison for snapshot values. * * Update will be skipped unless `equalResults()` called with previous and * current snapshot returns `false`. * * @category Hooks */ export function useSyncStoreWithComparator<R>( subscribe: SyncStore, getSnapshot: () => R, equalResults: (a: R, b: R) => boolean ) { const lastSnapshot = React.useRef<[R]>(undefined); return useSyncExternalStore( subscribe, () => { const result = getSnapshot(); if (lastSnapshot.current) { const [lastResult] = lastSnapshot.current; if (equalResults(lastResult, result)) { return lastResult; } } lastSnapshot.current = [result]; return result; } ); }