Skip to content

Instantly share code, notes, and snippets.

@honzabrecka
Created January 4, 2024 12:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save honzabrecka/4a9afa740bae761f1984bfd6eae843ef to your computer and use it in GitHub Desktop.
Save honzabrecka/4a9afa740bae761f1984bfd6eae843ef to your computer and use it in GitHub Desktop.
import React, {
useEffect,
useState,
useCallback,
StrictMode,
useSyncExternalStore,
} from 'react';
import { render, waitFor, screen } from '@testing-library/react';
export const strictWrapper = ({ children }: any) => (
<StrictMode>{children}</StrictMode>
);
type ValueOrUpdater = unknown | ((prev: unknown) => unknown);
class Store {
values = {};
listeners = {};
subscribe(name: string, cb: () => void) {
this.listeners[name] ||= new Set();
this.listeners[name].add(cb);
}
unsubscribe(name: string, cb: () => void) {
this.listeners[name] ||= new Set();
this.listeners[name].delete(cb);
if (this.listeners[name].size === 0) {
delete this.listeners[name];
}
}
getSnapshot(name: string) {
return this.values[name];
}
setValue(name: string, value: ValueOrUpdater) {
this.values[name] =
typeof value === 'function' ? value(this.values[name]) : value;
if (this.listeners[name]) {
this.listeners[name].forEach((cb: () => void) => cb());
}
}
}
let order;
const Child = ({ store, id }) => {
const name = `${id}/child`;
useEffect(() => {
order('Child Effect');
store.setValue(name, (state) => state || 'baz');
return () => {
order('Child Cleanup');
store.setValue(name, undefined);
};
}, [name]);
const subscribe = useCallback(
(cb) => {
store.subscribe(name, cb);
return () => {
store.unsubscribe(name, cb);
};
},
[name],
);
const getSnapshot = useCallback(() => {
return store.getSnapshot(name);
}, [name]);
const value = useSyncExternalStore(subscribe, getSnapshot);
order(`Child Render: ${id}, ${value}`);
return <div data-testid="child">{value}</div>;
};
const Parent = ({ store }) => {
const [id] = useState(() => {
order('Parent State');
// stable id does not help either
return 'parent';
});
const [show, setShow] = useState(false); // <-- set to true to render in different order
useEffect(() => {
order('Parent Effect');
store.setValue(`${id}/child`, 'bar');
setShow(true);
return () => {
order('Parent Cleanup');
store.setValue(`${id}/child`, undefined);
};
}, [id]);
order(`Parent Render: ${id}`);
// this conditional rendering is the problem
return show ? <Child store={store} id={id} /> : null;
};
beforeEach(() => {
order = jest.fn();
});
test('react: call order in StrictMode', async () => {
const store = new Store();
const { unmount } = render(<Parent store={store} />, {
wrapper: strictWrapper,
});
await waitFor(() => {
expect(Object.values(store.values)).toEqual(['baz']);
expect(screen.getByTestId('child').textContent).toEqual('baz');
});
unmount();
await waitFor(() => {
expect(order.mock.calls).toEqual([
// order without conditional rendering
// ['Parent State'],
// ['Parent Render: parent'],
// ['Parent State'],
// ['Parent Render: parent'],
// ['Child Render: parent, undefined'],
// ['Child Render: parent, undefined'],
// ['Child Effect'],
// ['Parent Effect'],
// ['Child Cleanup'],
// ['Parent Cleanup'],
// ['Child Effect'],
// ['Parent Effect'],
// ['Child Render: parent, bar'],
// ['Child Render: parent, bar'],
// ['Parent Cleanup'],
// ['Child Cleanup'],
['Parent State'],
['Parent Render: parent'],
['Parent State'],
['Parent Render: parent'],
['Parent Effect'],
['Parent Cleanup'],
['Parent Effect'],
['Parent Render: parent'],
['Parent Render: parent'],
['Child Render: parent, bar'],
['Child Render: parent, bar'],
['Child Effect'],
['Child Cleanup'], // <-- this one resets (different order)
['Child Effect'],
['Child Render: parent, baz'],
['Child Render: parent, baz'], // <-- child renders with "baz"
['Parent Cleanup'],
['Child Cleanup'],
]);
});
});
test('react: call order in production', async () => {
const store = new Store();
const { unmount } = render(<Parent store={store} />);
await waitFor(() => {
expect(Object.values(store.values)).toEqual(['bar']);
expect(screen.getByTestId('child').textContent).toEqual('bar');
});
unmount();
await waitFor(() => {
expect(order.mock.calls).toEqual([
// order without conditional rendering
// ['Parent State'],
// ['Parent Render'],
// ['Child Render: undefined'],
// ['Child Effect'],
// ['Parent Effect'],
// ['Child Render: bar'],
// ['Parent Cleanup'],
// ['Child Cleanup'],
['Parent State'],
['Parent Render: parent'],
['Parent Effect'],
['Parent Render: parent'],
['Child Render: parent, bar'], // <-- child renders with "bar"
['Child Effect'],
['Parent Cleanup'],
['Child Cleanup'],
]);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment