diff --git a/src/types/docs/ui-react.js b/src/types/docs/ui-react.js index eed3053d1aa..44ac94490ef 100644 --- a/src/types/docs/ui-react.js +++ b/src/types/docs/ui-react.js @@ -9366,11 +9366,9 @@ * * It is possible to create a Persister outside of the React app with the * regular createPersister function and pass it in, but you may prefer to create - * it within the app, perhaps inside the top-level component. To defend against - * a new Persister being created every time the app renders or re-renders, the - * useCreatePersister hook wraps the creation in a memoization, and provides a - * second callback so that you can configure the Persister, once, and - * asynchronously, when it is created. + * it within the app, perhaps inside the top-level component. To prevent a new + * Persister being created every time the app renders or re-renders, since v5.0 + * the useCreateMetrics hook performs the creation in an effect. * * If your `create` function (the second parameter to the hook) contains * dependencies, the changing of which should cause the Persister to be @@ -9378,19 +9376,18 @@ * you would for any React hook with dependencies. The Store passed in as the * first parameter of this hook is used as a dependency by default. * - * A second `then` callback can be provided as the fourth parameter. This is - * called after the creation, and, importantly, can be asynchronous, so that you - * can configure the Persister with the startAutoLoad method and startAutoSave - * method, for example. If this callback contains dependencies, the changing of - * which should cause the Persister to be reconfigured, you can provide them in - * an array in the fifth parameter. The Persister itself is used as a dependency - * by default. + * A second callback, called `then`, can be provided as the fourth parameter. + * This is called after the creation, and, importantly, can be asynchronous, so + * that you can configure the Persister with the startAutoLoad method and + * startAutoSave method, for example. If this callback contains dependencies, + * the changing of which should cause the Persister to be reconfigured, you can + * provide them in an array in the fifth parameter. The Persister itself is used + * as a dependency by default. * * Since v4.3.0, the `create` function can return undefined, meaning that you * can enable or disable persistence conditionally within this hook. This is * useful for applications which might turn on or off their cloud persistence or - * collaboration features. As a result, the `then` callback may also get passed - * undefined, which you should handle accordingly. + * collaboration features. * * Since v4.3.19, a `destroy` function can be provided which will be called * after an old Persister is destroyed due to a change in the `createDeps` @@ -9509,8 +9506,8 @@ * // -> '{\"pets\":{\"fido\":{\"species\":\"dog\"}}}' * * root.render(); // !act - * // -> 'Persister created for session key cujoStore' * // -> 'Persister destroyed for session key fidoStore' + * // -> 'Persister created for session key cujoStore' * * // ... // !act * console.log(app.innerHTML); diff --git a/src/types/ui-react.d.ts b/src/types/ui-react.d.ts index 5426e5b3474..67d58792cd5 100644 --- a/src/types/ui-react.d.ts +++ b/src/types/ui-react.d.ts @@ -959,9 +959,9 @@ export function useCreatePersister< store: Store, create: (store: Store) => PersisterOrUndefined, createDeps?: React.DependencyList, - then?: (persister: PersisterOrUndefined) => Promise, + then?: (persister: Persister) => Promise, thenDeps?: React.DependencyList, - destroy?: (persister: PersisterOrUndefined) => void, + destroy?: (persister: Persister) => void, destroyDeps?: React.DependencyList, ): PersisterOrUndefined; diff --git a/src/types/with-schemas/ui-react.d.ts b/src/types/with-schemas/ui-react.d.ts index de3126a78f7..688141c573c 100644 --- a/src/types/with-schemas/ui-react.d.ts +++ b/src/types/with-schemas/ui-react.d.ts @@ -1143,9 +1143,9 @@ export type WithSchemas = { store: Store, create: (store: Store) => PersisterOrUndefined, createDeps?: React.DependencyList, - then?: (persister?: PersisterOrUndefined) => Promise, + then?: (persister: Persister) => Promise, thenDeps?: React.DependencyList, - destroy?: (persister: PersisterOrUndefined) => void, + destroy?: (persister: Persister) => void, destroyDeps?: React.DependencyList, ) => PersisterOrUndefined; diff --git a/src/ui-react/hooks.ts b/src/ui-react/hooks.ts index f90f90c3096..622cbcd1aa0 100644 --- a/src/ui-react/hooks.ts +++ b/src/ui-react/hooks.ts @@ -1741,32 +1741,33 @@ export const useCreatePersister: typeof useCreatePersisterDecl = < store: Store, create: (store: Store) => PersisterOrUndefined, createDeps: React.DependencyList = EMPTY_ARRAY, - then?: (persister: PersisterOrUndefined) => Promise, + then?: (persister: Persister) => Promise, thenDeps: React.DependencyList = EMPTY_ARRAY, - destroy?: (persister: PersisterOrUndefined) => void, + destroy?: (persister: Persister) => void, destroyDeps: React.DependencyList = EMPTY_ARRAY, ): PersisterOrUndefined => { const [, rerender] = useState<[]>(); - const persister = useMemo( - () => create(store) as any, - // eslint-disable-next-line react-hooks/exhaustive-deps - [store, ...createDeps], - ); + const [persister, setPersister] = useState(); + useEffect( () => { - (async () => { + const newPersister = create(store); + setPersister(newPersister); + if (newPersister) { if (then) { - await then(persister); - rerender([]); + (async () => { + await then(newPersister); + rerender([]); + })(); } - })(); - return () => { - persister?.destroy(); - destroy?.(persister); - }; + return () => { + newPersister.destroy(); + destroy?.(newPersister); + }; + } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [persister, ...thenDeps, ...destroyDeps], + [store, ...createDeps, ...thenDeps, ...destroyDeps], ); return persister; }; diff --git a/test/unit/ui-react/hooks.test.tsx b/test/unit/ui-react/hooks.test.tsx index 57182050982..ee2962fd9d8 100644 --- a/test/unit/ui-react/hooks.test.tsx +++ b/test/unit/ui-react/hooks.test.tsx @@ -407,9 +407,11 @@ describe('Create Hooks', () => { }); const Test = ({id}: {id: number}) => { const store = useCreateStore(initStore); - const persister = useCreatePersister(store, createPersister); + const persister = useCreatePersister(store, createPersister, [id]); const cell = useCell('t1', 'r1', 'c1', store); - return didRender(<>{JSON.stringify([id, persister.getStats(), cell])}); + return didRender( + <>{JSON.stringify([id, persister?.getStats(), cell])}, + ); }; act(() => { renderer = create(); @@ -423,9 +425,18 @@ describe('Create Hooks', () => { expect(renderer.toJSON()).toEqual( JSON.stringify([1, {loads: 1, saves: 0}, 1]), ); + act(() => { + renderer.update(); + }); + await act(async () => { + await pause(); + }); + expect(renderer.toJSON()).toEqual( + JSON.stringify([2, {loads: 0, saves: 0}, 1]), + ); expect(initStore).toHaveBeenCalledTimes(1); - expect(createPersister).toHaveBeenCalledTimes(1); - expect(didRender).toHaveBeenCalledTimes(2); + expect(createPersister).toHaveBeenCalledTimes(2); + expect(didRender).toHaveBeenCalledTimes(5); _persister?.stopAutoLoad()?.stopAutoSave(); }); }); @@ -435,9 +446,11 @@ test('useCreatePersister, then, no destroy', async () => { tmp.setGracefulCleanup(); const fileName = tmp.fileSync().name; const initStore = jest.fn(createStore); - const createPersister = jest.fn((store: Store) => { - _persister = createFilePersister(store, fileName); - return _persister; + const createPersister = jest.fn((store: Store, id: number) => { + if (id != 0) { + _persister = createFilePersister(store, fileName); + return _persister; + } }); const initPersister = jest.fn(async (persister: Persister, id: number) => { await persister.load({t1: {r1: {c1: id}}}); @@ -446,17 +459,20 @@ test('useCreatePersister, then, no destroy', async () => { const store = useCreateStore(initStore); const persister = useCreatePersister( store, - (store) => createPersister(store), - undefined, + (store) => createPersister(store, id), + [id], async (persister) => await initPersister(persister, id), [id], ); - return didRender(<>{JSON.stringify([id, persister.getStats()])}); + return didRender(<>{JSON.stringify([id, persister?.getStats()])}); }; + act(() => { + renderer = create(); + }); + expect(renderer.toJSON()).toEqual(JSON.stringify([0, null])); act(() => { renderer = create(); }); - expect(renderer.toJSON()).toEqual(JSON.stringify([1, {loads: 0, saves: 0}])); await act(async () => { await pause(); }); @@ -464,15 +480,14 @@ test('useCreatePersister, then, no destroy', async () => { act(() => { renderer.update(); }); - expect(renderer.toJSON()).toEqual(JSON.stringify([2, {loads: 1, saves: 0}])); await act(async () => { await pause(); }); - expect(renderer.toJSON()).toEqual(JSON.stringify([2, {loads: 2, saves: 0}])); - expect(initStore).toHaveBeenCalledTimes(1); - expect(createPersister).toHaveBeenCalledTimes(1); + expect(renderer.toJSON()).toEqual(JSON.stringify([2, {loads: 1, saves: 0}])); + expect(initStore).toHaveBeenCalledTimes(2); + expect(createPersister).toHaveBeenCalledTimes(3); expect(initPersister).toHaveBeenCalledTimes(2); - expect(didRender).toHaveBeenCalledTimes(4); + expect(didRender).toHaveBeenCalledTimes(7); _persister?.stopAutoLoad()?.stopAutoSave(); }); @@ -502,12 +517,11 @@ test('useCreatePersister, then, destroy', async () => { [id], destroyPersister, ); - return didRender(<>{JSON.stringify([id, persister.getStats()])}); + return didRender(<>{JSON.stringify([id, persister?.getStats()])}); }; act(() => { renderer = create(); }); - expect(renderer.toJSON()).toEqual(JSON.stringify([1, {loads: 0, saves: 0}])); await act(async () => { await pause(); }); @@ -515,7 +529,6 @@ test('useCreatePersister, then, destroy', async () => { act(() => { renderer.update(); }); - expect(renderer.toJSON()).toEqual(JSON.stringify([2, {loads: 0, saves: 0}])); await act(async () => { await pause(); }); @@ -525,7 +538,7 @@ test('useCreatePersister, then, destroy', async () => { expect(initPersister).toHaveBeenCalledTimes(2); expect(destroyPersister).toHaveBeenCalledTimes(1); expect(destroyPersister).toHaveBeenCalledWith(persisters[1]); - expect(didRender).toHaveBeenCalledTimes(4); + expect(didRender).toHaveBeenCalledTimes(6); persisters.forEach((persister) => persister.stopAutoLoad().stopAutoSave()); });