UNPKG

convex

Version:

Client for the Convex Cloud

114 lines (99 loc) 4.53 kB
import { useEffect, useState } from "react"; /** * This code is taken from https://gist.github.com/bvaughn/e25397f70e8c65b0ae0d7c90b731b189 * because correct subscriptions in async React is complex! */ // Hook used for safely managing subscriptions in concurrent mode. // // In order to avoid removing and re-adding subscriptions each time this hook is called, // the parameters passed to this hook should be memoized in some way– // either by wrapping the entire params object with useMemo() // or by wrapping the individual callbacks with useCallback(). export function useSubscription<Value>({ // (Synchronously) returns the current value of our subscription. getCurrentValue, // This function is passed an event handler to attach to the subscription. // It should return an unsubscribe function that removes the handler. subscribe, }: { getCurrentValue: () => Value; subscribe: (callback: () => void) => () => void; }): Value { // Read the current value from our subscription. // When this value changes, we'll schedule an update with React. // It's important to also store the hook params so that we can check for staleness. // (See the comment in checkForUpdates() below for more info.) const [state, setState] = useState(() => ({ getCurrentValue, subscribe, value: getCurrentValue(), })); let valueToReturn = state.value; // If parameters have changed since our last render, schedule an update with its current value. if ( state.getCurrentValue !== getCurrentValue || state.subscribe !== subscribe ) { // If the subscription has been updated, we'll schedule another update with React. // React will process this update immediately, so the old subscription value won't be committed. // It is still nice to avoid returning a mismatched value though, so let's override the return value. valueToReturn = getCurrentValue(); setState({ getCurrentValue, subscribe, value: valueToReturn, }); } // It is important not to subscribe while rendering because this can lead to memory leaks. // (Learn more at reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects) // Instead, we wait until the commit phase to attach our handler. // // We intentionally use a passive effect (useEffect) rather than a synchronous one (useLayoutEffect) // so that we don't stretch the commit phase. // This also has an added benefit when multiple components are subscribed to the same source: // It allows each of the event handlers to safely schedule work without potentially removing an another handler. // (Learn more at https://codesandbox.io/s/k0yvr5970o) useEffect(() => { let didUnsubscribe = false; const checkForUpdates = () => { // It's possible that this callback will be invoked even after being unsubscribed, // if it's removed as a result of a subscription event/update. // In this case, React will log a DEV warning about an update from an unmounted component. // We can avoid triggering that warning with this check. if (didUnsubscribe) { return; } setState(prevState => { // Ignore values from stale sources! // Since we subscribe an unsubscribe in a passive effect, // it's possible that this callback will be invoked for a stale (previous) subscription. // This check avoids scheduling an update for that stale subscription. if ( prevState.getCurrentValue !== getCurrentValue || prevState.subscribe !== subscribe ) { return prevState; } // Some subscriptions will auto-invoke the handler, even if the value hasn't changed. // If the value hasn't changed, no update is needed. // Return state as-is so React can bail out and avoid an unnecessary render. const value = getCurrentValue(); if (prevState.value === value) { return prevState; } return { ...prevState, value }; }); }; const unsubscribe = subscribe(checkForUpdates); // Because we're subscribing in a passive effect, // it's possible that an update has occurred between render and our effect handler. // Check for this and schedule an update if work has occurred. checkForUpdates(); return () => { didUnsubscribe = true; unsubscribe(); }; }, [getCurrentValue, subscribe]); // Return the current value for our caller to use while rendering. return valueToReturn; }