Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions compat/src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import { useState, useLayoutEffect, useEffect } from 'preact/hooks';
import { options as _options } from 'preact';

const MODE_HYDRATE = 1 << 5;

/** @type {boolean} */
let hydrating;
// Cast to use internal Options type
const options = /** @type {import('../../src/internal').Options} */ (_options);
let oldBeforeRender = options._render;

/** @type {(vnode: import('./internal').VNode) => void} */
options._render = _vnode => {
hydrating = !!(_vnode._flags & MODE_HYDRATE);
if (oldBeforeRender) oldBeforeRender(_vnode);
};

/**
* This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84
* on a high level this cuts out the warnings, ... and attempts a smaller implementation
* @typedef {{ _value: any; _getSnapshot: () => any }} Store
*/
export function useSyncExternalStore(subscribe, getSnapshot) {
const value = getSnapshot();
export function useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
) {
const value =
typeof window === 'undefined' || hydrating
? getServerSnapshot
? getServerSnapshot()
: missingGetServerSnapshot()
: getSnapshot();

/**
* @typedef {{ _instance: Store }} StoreRef
Expand Down Expand Up @@ -52,6 +76,12 @@ function didSnapshotChange(inst) {
}
}

function missingGetServerSnapshot() {
throw new Error(
'Missing "getServerSnapshot" parameter for "useSyncExternalStore", this is required for server rendering & hydration of server-rendered content'
);
}

export function startTransition(cb) {
cb();
}
Expand Down
3 changes: 2 additions & 1 deletion compat/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ declare namespace React {
export function useDeferredValue<T = any>(val: T): T;
export function useSyncExternalStore<T>(
subscribe: (flush: () => void) => () => void,
getSnapshot: () => T
getSnapshot: () => T,
getServerSnapshot?: () => T
): T;

// Preact Defaults
Expand Down
41 changes: 41 additions & 0 deletions compat/test/browser/useSyncExternalStore.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
Fragment,
useSyncExternalStore,
render,
hydrate,
useState,
useCallback,
useEffect,
Expand Down Expand Up @@ -781,6 +782,46 @@ describe('useSyncExternalStore', () => {
expect(container.textContent).to.equal('NaN');
});

it('basic server hydration', async () => {
const store = createExternalStore('client');

const ref = React.createRef();
function App() {
const text = useSyncExternalStore(
store.subscribe,
store.getState,
() => 'server'
);
useEffect(() => {
Scheduler.log('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];

await act(() => {
hydrate(<App />, container);
});
assertLog([
// 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'
]);

expect(container.textContent).toEqual('client');
expect(ref.current).toEqual(serverRenderedDiv);
});

it('regression test for facebook/react#23150', async () => {
const store = createExternalStore('Initial');

Expand Down
Loading