Skip to content
Open
31 changes: 29 additions & 2 deletions compat/src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
import { useState, useLayoutEffect, useEffect } from 'preact/hooks';
import { MODE_HYDRATE } from '../../src/constants';
Comment thread
rschristian marked this conversation as resolved.
Outdated
import { options as _options } from 'preact';

/** @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 => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move this to the render.js file you can benefit from options._render there and save more bytes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having hooks.js only contain some hooks, not all, is kinda weird... what was the byte difference there, if you remember? Should we even keep this module around, or should it be inlined into render?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried it locally it was 32b gzip

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roughly a third then, hefty.

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
Comment thread
rschristian marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This or we use options._skipEffects

? getServerSnapshot
? getServerSnapshot()
: missingGetServerSnapshot()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add oversized error strings, this seems a bit against our bundle philosophy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That (the oversized string) was my suggestion FWIW, my bad

: getSnapshot();

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

function missingGetServerSnapshot() {
throw new Error('Missing getServerSnapshot for server rendering');
}
Comment thread
rschristian marked this conversation as resolved.
Outdated

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 () => {
Comment thread
rschristian marked this conversation as resolved.
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