1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import * as React from 'react';
    
  11. import is from 'shared/objectIs';
    
  12. 
    
  13. // Intentionally not using named imports because Rollup uses dynamic
    
  14. // dispatch for CommonJS interop named imports.
    
  15. const {useState, useEffect, useLayoutEffect, useDebugValue} = React;
    
  16. 
    
  17. let didWarnOld18Alpha = false;
    
  18. let didWarnUncachedGetSnapshot = false;
    
  19. 
    
  20. // Disclaimer: This shim breaks many of the rules of React, and only works
    
  21. // because of a very particular set of implementation details and assumptions
    
  22. // -- change any one of them and it will break. The most important assumption
    
  23. // is that updates are always synchronous, because concurrent rendering is
    
  24. // only available in versions of React that also have a built-in
    
  25. // useSyncExternalStore API. And we only use this shim when the built-in API
    
  26. // does not exist.
    
  27. //
    
  28. // Do not assume that the clever hacks used by this hook also work in general.
    
  29. // The point of this shim is to replace the need for hacks by other libraries.
    
  30. export function useSyncExternalStore<T>(
    
  31.   subscribe: (() => void) => () => void,
    
  32.   getSnapshot: () => T,
    
  33.   // Note: The shim does not use getServerSnapshot, because pre-18 versions of
    
  34.   // React do not expose a way to check if we're hydrating. So users of the shim
    
  35.   // will need to track that themselves and return the correct value
    
  36.   // from `getSnapshot`.
    
  37.   getServerSnapshot?: () => T,
    
  38. ): T {
    
  39.   if (__DEV__) {
    
  40.     if (!didWarnOld18Alpha) {
    
  41.       if (React.startTransition !== undefined) {
    
  42.         didWarnOld18Alpha = true;
    
  43.         console.error(
    
  44.           'You are using an outdated, pre-release alpha of React 18 that ' +
    
  45.             'does not support useSyncExternalStore. The ' +
    
  46.             'use-sync-external-store shim will not work correctly. Upgrade ' +
    
  47.             'to a newer pre-release.',
    
  48.         );
    
  49.       }
    
  50.     }
    
  51.   }
    
  52. 
    
  53.   // Read the current snapshot from the store on every render. Again, this
    
  54.   // breaks the rules of React, and only works here because of specific
    
  55.   // implementation details, most importantly that updates are
    
  56.   // always synchronous.
    
  57.   const value = getSnapshot();
    
  58.   if (__DEV__) {
    
  59.     if (!didWarnUncachedGetSnapshot) {
    
  60.       const cachedValue = getSnapshot();
    
  61.       if (!is(value, cachedValue)) {
    
  62.         console.error(
    
  63.           'The result of getSnapshot should be cached to avoid an infinite loop',
    
  64.         );
    
  65.         didWarnUncachedGetSnapshot = true;
    
  66.       }
    
  67.     }
    
  68.   }
    
  69. 
    
  70.   // Because updates are synchronous, we don't queue them. Instead we force a
    
  71.   // re-render whenever the subscribed state changes by updating an some
    
  72.   // arbitrary useState hook. Then, during render, we call getSnapshot to read
    
  73.   // the current value.
    
  74.   //
    
  75.   // Because we don't actually use the state returned by the useState hook, we
    
  76.   // can save a bit of memory by storing other stuff in that slot.
    
  77.   //
    
  78.   // To implement the early bailout, we need to track some things on a mutable
    
  79.   // object. Usually, we would put that in a useRef hook, but we can stash it in
    
  80.   // our useState hook instead.
    
  81.   //
    
  82.   // To force a re-render, we call forceUpdate({inst}). That works because the
    
  83.   // new object always fails an equality check.
    
  84.   const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
    
  85. 
    
  86.   // Track the latest getSnapshot function with a ref. This needs to be updated
    
  87.   // in the layout phase so we can access it during the tearing check that
    
  88.   // happens on subscribe.
    
  89.   useLayoutEffect(() => {
    
  90.     inst.value = value;
    
  91.     inst.getSnapshot = getSnapshot;
    
  92. 
    
  93.     // Whenever getSnapshot or subscribe changes, we need to check in the
    
  94.     // commit phase if there was an interleaved mutation. In concurrent mode
    
  95.     // this can happen all the time, but even in synchronous mode, an earlier
    
  96.     // effect may have mutated the store.
    
  97.     if (checkIfSnapshotChanged(inst)) {
    
  98.       // Force a re-render.
    
  99.       forceUpdate({inst});
    
  100.     }
    
  101.   }, [subscribe, value, getSnapshot]);
    
  102. 
    
  103.   useEffect(() => {
    
  104.     // Check for changes right before subscribing. Subsequent changes will be
    
  105.     // detected in the subscription handler.
    
  106.     if (checkIfSnapshotChanged(inst)) {
    
  107.       // Force a re-render.
    
  108.       forceUpdate({inst});
    
  109.     }
    
  110.     const handleStoreChange = () => {
    
  111.       // TODO: Because there is no cross-renderer API for batching updates, it's
    
  112.       // up to the consumer of this library to wrap their subscription event
    
  113.       // with unstable_batchedUpdates. Should we try to detect when this isn't
    
  114.       // the case and print a warning in development?
    
  115. 
    
  116.       // The store changed. Check if the snapshot changed since the last time we
    
  117.       // read from the store.
    
  118.       if (checkIfSnapshotChanged(inst)) {
    
  119.         // Force a re-render.
    
  120.         forceUpdate({inst});
    
  121.       }
    
  122.     };
    
  123.     // Subscribe to the store and return a clean-up function.
    
  124.     return subscribe(handleStoreChange);
    
  125.   }, [subscribe]);
    
  126. 
    
  127.   useDebugValue(value);
    
  128.   return value;
    
  129. }
    
  130. 
    
  131. function checkIfSnapshotChanged<T>(inst: {
    
  132.   value: T,
    
  133.   getSnapshot: () => T,
    
  134. }): boolean {
    
  135.   const latestGetSnapshot = inst.getSnapshot;
    
  136.   const prevValue = inst.value;
    
  137.   try {
    
  138.     const nextValue = latestGetSnapshot();
    
  139.     return !is(prevValue, nextValue);
    
  140.   } catch (error) {
    
  141.     return true;
    
  142.   }
    
  143. }