Repository: facebook/react
Pull request: Implement getServerSnapshot in userspace shim

If the DOM is not present, we assume that we are running in a server environment and return the result of `getServerSnapshot`.

This heuristic doesn't work in React Native, so we'll need to provide a separate native build (using the `.native` extension). I've left this for a follow-up.

We can't call `getServerSnapshot` on the client, because in versions of React before 18, there's no built-in mechanism to detect whether we're hydrating. To avoid a server mismatch warning, users must account for this themselves and return the correct value inside `getSnapshot`.

Note that none of this is relevant to the built-in API that is being added in 18. This only affects the userspace shim that is provided for backwards compatibility with versions 16 and 17.

Changed files:
filename: packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
patch: @@ -12,7 +12,7 @@
 let useSyncExternalStore;
 let useSyncExternalStoreExtra;
 let React;
-let ReactNoop;
+let ReactDOM;
 let Scheduler;
 let act;
 let useState;
@@ -27,11 +27,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
 
     // Remove the built-in API from the React exports to force the package to
     // use the shim.
-    // TODO: Don't do this during a variant test run. That way these tests run
-    // against both the shim and the built-in implementation.
-    if (gate(flags => flags.variant)) {
-      // We'll use the variant flag to represent the native implementation
-    } else {
+    if (!gate(flags => flags.supportsNativeUseSyncExternalStore)) {
       // and the non-variant tests for the shim.
       //
       // Remove useSyncExternalStore from the React imports so that we use the
@@ -55,7 +51,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     }
 
     React = require('react');
-    ReactNoop = require('react-noop-renderer');
+    ReactDOM = require('react-dom');
     Scheduler = require('scheduler');
     useState = React.useState;
     useEffect = React.useEffect;
@@ -66,7 +62,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     // The internal act implementation doesn't batch updates by default, since
     // it's mostly used to test concurrent mode. But since these tests run
     // in both concurrent and legacy mode, I'm adding batching here.
-    act = cb => internalAct(() => ReactNoop.batchedUpdates(cb));
+    act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb));
 
     useSyncExternalStore = require('use-sync-external-store')
       .useSyncExternalStore;
@@ -79,19 +75,24 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     return text;
   }
 
-  function createRoot(element) {
+  function createRoot(container) {
     // This wrapper function exists so we can test both legacy roots and
     // concurrent roots.
-    if (gate(flags => flags.variant)) {
+    if (gate(flags => flags.supportsNativeUseSyncExternalStore)) {
       // The native implementation only exists in 18+, so we test using
       // concurrent mode. To test the legacy root behavior in the native
       // implementation (which is supported in the sense that it needs to have
       // the correct behavior, despite the fact that the legacy root API
       // triggers a warning in 18), write a test that uses
       // createLegacyRoot directly.
-      return ReactNoop.createRoot();
+      return ReactDOM.createRoot(container);
     } else {
-      return ReactNoop.createLegacyRoot();
+      ReactDOM.render(null, container);
+      return {
+        render(children) {
+          ReactDOM.render(children, container);
+        },
+      };
     }
   }
 
@@ -101,7 +102,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     return {
       set(text) {
         currentState = text;
-        ReactNoop.batchedUpdates(() => {
+        ReactDOM.unstable_batchedUpdates(() => {
           listeners.forEach(listener => listener());
         });
       },
@@ -126,17 +127,18 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={text} />;
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     await act(() => root.render(<App />));
 
     expect(Scheduler).toHaveYielded(['Initial']);
-    expect(root).toMatchRenderedOutput('Initial');
+    expect(container.textContent).toEqual('Initial');
 
     await act(() => {
       store.set('Updated');
     });
     expect(Scheduler).toHaveYielded(['Updated']);
-    expect(root).toMatchRenderedOutput('Updated');
+    expect(container.textContent).toEqual('Updated');
   });
 
   test('skips re-rendering if nothing changes', () => {
@@ -147,19 +149,20 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={text} />;
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() => root.render(<App />));
 
     expect(Scheduler).toHaveYielded(['Initial']);
-    expect(root).toMatchRenderedOutput('Initial');
+    expect(container.textContent).toEqual('Initial');
 
     // Update to the same value
     act(() => {
       store.set('Initial');
     });
     // Should not re-render
     expect(Scheduler).toHaveYielded([]);
-    expect(root).toMatchRenderedOutput('Initial');
+    expect(container.textContent).toEqual('Initial');
   });
 
   test('switch to a different store', async () => {
@@ -174,44 +177,45 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={value} />;
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     await act(() => root.render(<App />));
 
     expect(Scheduler).toHaveYielded([0]);
-    expect(root).toMatchRenderedOutput('0');
+    expect(container.textContent).toEqual('0');
 
     await act(() => {
       storeA.set(1);
     });
     expect(Scheduler).toHaveYielded([1]);
-    expect(root).toMatchRenderedOutput('1');
+    expect(container.textContent).toEqual('1');
 
     // Switch stores and update in the same batch
     act(() => {
-      ReactNoop.flushSync(() => {
+      ReactDOM.flushSync(() => {
         // This update will be disregarded
         storeA.set(2);
         setStore(storeB);
       });
     });
     // Now reading from B instead of A
     expect(Scheduler).toHaveYielded([0]);
-    expect(root).toMatchRenderedOutput('0');
+    expect(container.textContent).toEqual('0');
 
     // Update A
     await act(() => {
       storeA.set(3);
     });
     // Nothing happened, because we're no longer subscribed to A
     expect(Scheduler).toHaveYielded([]);
-    expect(root).toMatchRenderedOutput('0');
+    expect(container.textContent).toEqual('0');
 
     // Update B
     await act(() => {
       storeB.set(1);
     });
     expect(Scheduler).toHaveYielded([1]);
-    expect(root).toMatchRenderedOutput('1');
+    expect(container.textContent).toEqual('1');
   });
 
   test('selecting a specific value inside getSnapshot', async () => {
@@ -235,32 +239,33 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       );
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() => root.render(<App />));
 
     expect(Scheduler).toHaveYielded(['A0', 'B0']);
-    expect(root).toMatchRenderedOutput('A0B0');
+    expect(container.textContent).toEqual('A0B0');
 
     // Update b but not a
     await act(() => {
       store.set({a: 0, b: 1});
     });
     // Only b re-renders
     expect(Scheduler).toHaveYielded(['B1']);
-    expect(root).toMatchRenderedOutput('A0B1');
+    expect(container.textContent).toEqual('A0B1');
 
     // Update a but not b
     await act(() => {
       store.set({a: 1, b: 1});
     });
     // Only a re-renders
     expect(Scheduler).toHaveYielded(['A1']);
-    expect(root).toMatchRenderedOutput('A1B1');
+    expect(container.textContent).toEqual('A1B1');
   });
 
   // In React 18, you can't observe in between a sync render and its
   // passive effects, so this is only relevant to legacy roots
-  // @gate !variant
+  // @gate !supportsNativeUseSyncExternalStore
   test(
     "compares to current state before bailing out, even when there's a " +
       'mutation in between the sync and passive effects',
@@ -275,7 +280,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         return <Text text={value} />;
       }
 
-      const root = createRoot();
+      const container = document.createElement('div');
+      const root = createRoot(container);
       act(() => root.render(<App />));
       expect(Scheduler).toHaveYielded([0, 'Passive effect: 0']);
 
@@ -287,7 +293,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         1,
         // Passive effect hasn't fired yet
       ]);
-      expect(root).toMatchRenderedOutput('1');
+      expect(container.textContent).toEqual('1');
 
       // Flip the store state back to the previous value.
       store.set(0);
@@ -300,7 +306,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         0,
       ]);
       // Should flip back to 0
-      expect(root).toMatchRenderedOutput('0');
+      expect(container.textContent).toEqual('0');
     },
   );
 
@@ -344,10 +350,11 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       );
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() => root.render(<App />));
     expect(Scheduler).toHaveYielded(['A1']);
-    expect(root).toMatchRenderedOutput('A1');
+    expect(container.textContent).toEqual('A1');
 
     act(() => {
       // Change getSnapshot and update the store in the same batch
@@ -360,7 +367,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       // incorrectly bailed out here instead of re-rendering.
       'B2',
     ]);
-    expect(root).toMatchRenderedOutput('B2');
+    expect(container.textContent).toEqual('B2');
   });
 
   test('mutating the store in between render and commit when getSnapshot has _not_ changed', () => {
@@ -401,10 +408,11 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       );
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() => root.render(<App />));
     expect(Scheduler).toHaveYielded(['A1']);
-    expect(root).toMatchRenderedOutput('A1');
+    expect(container.textContent).toEqual('A1');
 
     // This will cause a layout effect, and in the layout effect we'll update
     // the store
@@ -418,7 +426,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       'Update B in commit phase',
       // No re-render
     ]);
-    expect(root).toMatchRenderedOutput('A1');
+    expect(container.textContent).toEqual('A1');
   });
 
   test("does not bail out if the previous update hasn't finished yet", async () => {
@@ -440,7 +448,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={value} />;
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() =>
       root.render(
         <>
@@ -450,13 +459,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       ),
     );
     expect(Scheduler).toHaveYielded([0, 0]);
-    expect(root).toMatchRenderedOutput('00');
+    expect(container.textContent).toEqual('00');
 
     await act(() => {
       store.set(1);
     });
     expect(Scheduler).toHaveYielded([1, 1, 'Reset back to 0', 0, 0]);
-    expect(root).toMatchRenderedOutput('00');
+    expect(container.textContent).toEqual('00');
   });
 
   test('uses the latest getSnapshot, even if it changed in the same batch as a store update', () => {
@@ -473,20 +482,21 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={text} />;
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() => root.render(<App />));
     expect(Scheduler).toHaveYielded([0]);
 
     // Update the store and getSnapshot at the same time
     act(() => {
-      ReactNoop.flushSync(() => {
+      ReactDOM.flushSync(() => {
         setGetSnapshot(() => getSnapshotB);
         store.set({a: 1, b: 2});
       });
     });
     // It should read from B instead of A
     expect(Scheduler).toHaveYielded([2]);
-    expect(root).toMatchRenderedOutput('2');
+    expect(container.textContent).toEqual('2');
   });
 
   test('handles errors thrown by getSnapshot', async () => {
@@ -521,7 +531,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     }
 
     const errorBoundary = React.createRef(null);
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     act(() =>
       root.render(
         <ErrorBoundary ref={errorBoundary}>
@@ -530,13 +541,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       ),
     );
     expect(Scheduler).toHaveYielded([0]);
-    expect(root).toMatchRenderedOutput('0');
+    expect(container.textContent).toEqual('0');
 
     // Update that throws in a getSnapshot. We can catch it with an error boundary.
     await act(() => {
       store.set({value: 1, throwInGetSnapshot: true, throwInIsEqual: false});
     });
-    if (gate(flags => flags.variant)) {
+    if (gate(flags => flags.supportsNativeUseSyncExternalStore)) {
       expect(Scheduler).toHaveYielded([
         'Error in getSnapshot',
         // In a concurrent root, React renders a second time to attempt to
@@ -546,7 +557,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
     } else {
       expect(Scheduler).toHaveYielded(['Error in getSnapshot']);
     }
-    expect(root).toMatchRenderedOutput('Error in getSnapshot');
+    expect(container.textContent).toEqual('Error in getSnapshot');
   });
 
   test('Infinite loop if getSnapshot keeps returning new reference', () => {
@@ -557,19 +568,18 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       return <Text text={JSON.stringify(text)} />;
     }
 
-    spyOnDev(console, 'error');
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
 
-    expect(() => act(() => root.render(<App />))).toThrow(
-      'Maximum update depth exceeded. This can happen when a component repeatedly ' +
-        'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
-        'the number of nested updates to prevent infinite loops.',
-    );
-    if (__DEV__) {
-      expect(console.error.calls.argsFor(0)[0]).toMatch(
-        'The result of getSnapshot should be cached to avoid an infinite loop',
+    expect(() => {
+      expect(() => act(() => root.render(<App />))).toThrow(
+        'Maximum update depth exceeded. This can happen when a component repeatedly ' +
+          'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
+          'the number of nested updates to prevent infinite loops.',
       );
-    }
+    }).toErrorDev(
+      'The result of getSnapshot should be cached to avoid an infinite loop',
+    );
   });
 
   describe('extra features implemented in user-space', () => {
@@ -594,11 +604,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         return <Text text={'A' + a} />;
       }
 
-      const root = createRoot();
+      const container = document.createElement('div');
+      const root = createRoot(container);
       act(() => root.render(<App />));
 
       expect(Scheduler).toHaveYielded(['App', 'Selector', 'A0']);
-      expect(root).toMatchRenderedOutput('A0');
+      expect(container.textContent).toEqual('A0');
 
       // Update the store
       await act(() => {
@@ -612,7 +623,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         // the previous result without running the selector again
         'A1',
       ]);
-      expect(root).toMatchRenderedOutput('A1');
+      expect(container.textContent).toEqual('A1');
     });
 
     // The selector implementation uses the lazy ref initialization pattern
@@ -652,27 +663,80 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
         );
       }
 
-      const root = createRoot();
+      const container = document.createElement('div');
+      const root = createRoot(container);
       act(() => root.render(<App />));
 
       expect(Scheduler).toHaveYielded(['A0', 'B0']);
-      expect(root).toMatchRenderedOutput('A0B0');
+      expect(container.textContent).toEqual('A0B0');
 
       // Update b but not a
       await act(() => {
         store.set({a: 0, b: 1});
       });
       // Only b re-renders
       expect(Scheduler).toHaveYielded(['B1']);
-      expect(root).toMatchRenderedOutput('A0B1');
+      expect(container.textContent).toEqual('A0B1');
 
       // Update a but not b
       await act(() => {
         store.set({a: 1, b: 1});
       });
       // Only a re-renders
       expect(Scheduler).toHaveYielded(['A1']);
-      expect(root).toMatchRenderedOutput('A1B1');
+      expect(container.textContent).toEqual('A1B1');
+    });
+
+    test('basic server hydration', async () => {
+      const store = createExternalStore('client');
+
+      const ref = React.createRef();
+      function App() {
+        const text = useSyncExternalStore(
+          store.subscribe,
+          store.getState,
+          () => 'server',
+        );
+        useEffect(() => {
+          Scheduler.unstable_yieldValue('Passive effect: ' + text);
+        }, [text]);
+        return (
+          <div ref={ref}>
+            <Text text={text} />
+          </div>
+        );
+      }
+
+      const container = document.createElement('div');
+      container.innerHTML = '<div>server</div>';
+      const serverRenderedDiv = container.getElementsByTagName('div')[0];
+
+      if (gate(flags => flags.supportsNativeUseSyncExternalStore)) {
+        act(() => {
+          ReactDOM.hydrateRoot(container, <App />);
+        });
+        expect(Scheduler).toHaveYielded([
+          // First it hydrates the server rendered HTML
+          'server',
+          'Passive effect: server',
+          // Then in a second paint, it re-renders with the client state
+          'client',
+          'Passive effect: client',
+        ]);
+      } else {
+        // In the userspace shim, there's no mechanism to detect whether we're
+        // currently hydrating, so `getServerSnapshot` is not called on the
+        // client. To avoid this server mismatch warning, user must account for
+        // this themselves and return the correct value inside `getSnapshot`.
+        act(() => {
+          expect(() => ReactDOM.hydrate(<App />, container)).toErrorDev(
+            'Text content did not match',
+          );
+        });
+        expect(Scheduler).toHaveYielded(['client', 'Passive effect: client']);
+      }
+      expect(container.textContent).toEqual('client');
+      expect(ref.current).toEqual(serverRenderedDiv);
     });
   });
 
@@ -725,7 +789,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
       );
     }
 
-    const root = createRoot();
+    const container = document.createElement('div');
+    const root = createRoot(container);
     await act(() => {
       root.render(<App step={0} />);
     });

filename: packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js
patch: @@ -0,0 +1,98 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ *
+ * @jest-environment node
+ */
+
+'use strict';
+
+let useSyncExternalStore;
+let React;
+let ReactDOM;
+let ReactDOMServer;
+let Scheduler;
+
+// This tests the userspace shim of `useSyncExternalStore` in a server-rendering
+// (Node) environment
+describe('useSyncExternalStore (userspace shim, server rendering)', () => {
+  beforeEach(() => {
+    jest.resetModules();
+
+    // Remove useSyncExternalStore from the React imports so that we use the
+    // shim instead. Also removing startTransition, since we use that to detect
+    // outdated 18 alphas that don't yet include useSyncExternalStore.
+    //
+    // Longer term, we'll probably test this branch using an actual build of
+    // React 17.
+    jest.mock('react', () => {
+      const {
+        // eslint-disable-next-line no-unused-vars
+        startTransition: _,
+        // eslint-disable-next-line no-unused-vars
+        useSyncExternalStore: __,
+        // eslint-disable-next-line no-unused-vars
+        unstable_useSyncExternalStore: ___,
+        ...otherExports
+      } = jest.requireActual('react');
+      return otherExports;
+    });
+
+    React = require('react');
+    ReactDOM = require('react-dom');
+    ReactDOMServer = require('react-dom/server');
+    Scheduler = require('scheduler');
+
+    useSyncExternalStore = require('use-sync-external-store')
+      .useSyncExternalStore;
+  });
+
+  function Text({text}) {
+    Scheduler.unstable_yieldValue(text);
+    return text;
+  }
+
+  function createExternalStore(initialState) {
+    const listeners = new Set();
+    let currentState = initialState;
+    return {
+      set(text) {
+        currentState = text;
+        ReactDOM.unstable_batchedUpdates(() => {
+          listeners.forEach(listener => listener());
+        });
+      },
+      subscribe(listener) {
+        listeners.add(listener);
+        return () => listeners.delete(listener);
+      },
+      getState() {
+        return currentState;
+      },
+      getSubscriberCount() {
+        return listeners.size;
+      },
+    };
+  }
+
+  test('basic server render', async () => {
+    const store = createExternalStore('client');
+
+    function App() {
+      const text = useSyncExternalStore(
+        store.subscribe,
+        store.getState,
+        () => 'server',
+      );
+      return <Text text={text} />;
+    }
+
+    const html = ReactDOMServer.renderToString(<App />);
+    expect(Scheduler).toHaveYielded(['server']);
+    expect(html).toEqual('server');
+  });
+});

filename: packages/use-sync-external-store/src/useSyncExternalStore.js
patch: @@ -9,6 +9,8 @@
 
 import * as React from 'react';
 import is from 'shared/objectIs';
+import invariant from 'shared/invariant';
+import {canUseDOM} from 'shared/ExecutionEnvironment';
 
 // Intentionally not using named imports because Rollup uses dynamic
 // dispatch for CommonJS interop named imports.
@@ -21,17 +23,38 @@ const {
   unstable_useSyncExternalStore: builtInAPI,
 } = React;
 
+// TODO: This heuristic doesn't work in React Native. We'll need to provide a
+// special build, using the `.native` extension.
+const isServerEnvironment = !canUseDOM;
+
 // Prefer the built-in API, if it exists. If it doesn't exist, then we assume
 // we're in version 16 or 17, so rendering is always synchronous. The shim
 // does not support concurrent rendering, only the built-in API.
 export const useSyncExternalStore =
   builtInAPI !== undefined
-    ? ((builtInAPI: any): typeof useSyncExternalStore_shim)
-    : useSyncExternalStore_shim;
+    ? ((builtInAPI: any): typeof useSyncExternalStore_client)
+    : isServerEnvironment
+    ? useSyncExternalStore_server
+    : useSyncExternalStore_client;
 
 let didWarnOld18Alpha = false;
 let didWarnUncachedGetSnapshot = false;
 
+function useSyncExternalStore_server<T>(
+  subscribe: (() => void) => () => void,
+  getSnapshot: () => T,
+  getServerSnapshot?: () => T,
+): T {
+  if (getServerSnapshot === undefined) {
+    invariant(
+      false,
+      'Missing getServerSnapshot, which is required for server-' +
+        'rendered content.',
+    );
+  }
+  return getServerSnapshot();
+}
+
 // Disclaimer: This shim breaks many of the rules of React, and only works
 // because of a very particular set of implementation details and assumptions
 // -- change any one of them and it will break. The most important assumption
@@ -42,10 +65,13 @@ let didWarnUncachedGetSnapshot = false;
 //
 // Do not assume that the clever hacks used by this hook also work in general.
 // The point of this shim is to replace the need for hacks by other libraries.
-function useSyncExternalStore_shim<T>(
+function useSyncExternalStore_client<T>(
   subscribe: (() => void) => () => void,
   getSnapshot: () => T,
-  // TODO: Add a canUseDOM check and use this one on the server
+  // Note: The client shim does not use getServerSnapshot, because pre-18
+  // versions of React do not expose a way to check if we're hydrating. So
+  // users of the shim will need to track that themselves and return the
+  // correct value from `getSnapshot`.
   getServerSnapshot?: () => T,
 ): T {
   if (__DEV__) {
@@ -97,7 +123,6 @@ function useSyncExternalStore_shim<T>(
   // Track the latest getSnapshot function with a ref. This needs to be updated
   // in the layout phase so we can access it during the tearing check that
   // happens on subscribe.
-  // TODO: Circumvent SSR warning with canUseDOM check
   useLayoutEffect(() => {
     inst.value = value;
     inst.getSnapshot = getSnapshot;

filename: scripts/error-codes/codes.json
patch: @@ -395,5 +395,6 @@
   "404": "Invalid hook call. Hooks can only be called inside of the body of a function component.",
   "405": "hydrateRoot(...): Target container is not a DOM element.",
   "406": "act(...) is not supported in production builds of React.",
-  "407": "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering."
+  "407": "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.",
+  "408": "Missing getServerSnapshot, which is required for server-rendered content."
 }