diff --git a/packages/ra-core/src/dataProvider/useCreate.ts b/packages/ra-core/src/dataProvider/useCreate.ts index 4aae973adfa..f2490be18e2 100644 --- a/packages/ra-core/src/dataProvider/useCreate.ts +++ b/packages/ra-core/src/dataProvider/useCreate.ts @@ -9,6 +9,7 @@ import { import { useDataProvider } from './useDataProvider'; import { RaRecord, CreateParams } from '../types'; +import { useEvent } from '../util'; /** * Get a callback to call the dataProvider.create() method, the result and the loading state. @@ -142,7 +143,7 @@ export const useCreate = < ); }; - return [create, mutation]; + return [useEvent(create), mutation]; }; export interface UseCreateMutateParams { diff --git a/packages/ra-core/src/dataProvider/useDelete.ts b/packages/ra-core/src/dataProvider/useDelete.ts index da72dd49fc9..da0ecc1952b 100644 --- a/packages/ra-core/src/dataProvider/useDelete.ts +++ b/packages/ra-core/src/dataProvider/useDelete.ts @@ -16,6 +16,7 @@ import { MutationMode, GetListResult as OriginalGetListResult, } from '../types'; +import { useEvent } from '../util'; /** * Get a callback to call the dataProvider.delete() method, the result and the loading state. @@ -387,7 +388,7 @@ export const useDelete = < } }; - return [mutate, mutation]; + return [useEvent(mutate), mutation]; }; type Snapshot = [key: QueryKey, value: any][]; diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.ts b/packages/ra-core/src/dataProvider/useDeleteMany.ts index 4ebab5a2048..1dcbc4491b2 100644 --- a/packages/ra-core/src/dataProvider/useDeleteMany.ts +++ b/packages/ra-core/src/dataProvider/useDeleteMany.ts @@ -16,6 +16,7 @@ import { MutationMode, GetListResult as OriginalGetListResult, } from '../types'; +import { useEvent } from '../util'; /** * Get a callback to call the dataProvider.delete() method, the result and the loading state. @@ -394,7 +395,7 @@ export const useDeleteMany = < } }; - return [mutate, mutation]; + return [useEvent(mutate), mutation]; }; type Snapshot = [key: QueryKey, value: any][]; diff --git a/packages/ra-core/src/dataProvider/useUpdate.ts b/packages/ra-core/src/dataProvider/useUpdate.ts index e0ea0504667..31011573752 100644 --- a/packages/ra-core/src/dataProvider/useUpdate.ts +++ b/packages/ra-core/src/dataProvider/useUpdate.ts @@ -16,6 +16,7 @@ import { MutationMode, GetListResult as OriginalGetListResult, } from '../types'; +import { useEvent } from '../util'; /** * Get a callback to call the dataProvider.update() method, the result and the loading state. @@ -418,7 +419,7 @@ export const useUpdate = < } }; - return [update, mutation]; + return [useEvent(update), mutation]; }; type Snapshot = [key: QueryKey, value: any][]; diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.ts b/packages/ra-core/src/dataProvider/useUpdateMany.ts index f1705001855..8dd3c9200c8 100644 --- a/packages/ra-core/src/dataProvider/useUpdateMany.ts +++ b/packages/ra-core/src/dataProvider/useUpdateMany.ts @@ -16,6 +16,7 @@ import { MutationMode, GetListResult as OriginalGetListResult, } from '../types'; +import { useEvent } from '../util'; import { Identifier } from '..'; /** @@ -420,7 +421,7 @@ export const useUpdateMany = < } }; - return [updateMany, mutation]; + return [useEvent(updateMany), mutation]; }; type Snapshot = [key: QueryKey, value: any][]; diff --git a/packages/ra-core/src/store/useStore.ts b/packages/ra-core/src/store/useStore.ts index cdc78615d10..d5ebbf18074 100644 --- a/packages/ra-core/src/store/useStore.ts +++ b/packages/ra-core/src/store/useStore.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import isEqual from 'lodash/isEqual'; -import { useEventCallback } from '../util'; +import { useEvent } from '../util'; import { useStoreContext } from './useStoreContext'; /** @@ -64,26 +64,21 @@ export const useStore = ( return () => unsubscribe(); }, [key, subscribe, defaultValue, getItem, value]); - const set = useEventCallback( - (valueParam: T, runtimeDefaultValue: T) => { - const newValue = - typeof valueParam === 'function' - ? valueParam(value) - : valueParam; - // we only set the value in the Store; - // the value in the local state will be updated - // by the useEffect during the next render - setItem( - key, - typeof newValue === 'undefined' - ? typeof runtimeDefaultValue === 'undefined' - ? defaultValue - : runtimeDefaultValue - : newValue - ); - }, - [key, setItem, defaultValue, value] - ); + const set = useEvent((valueParam: T, runtimeDefaultValue: T) => { + const newValue = + typeof valueParam === 'function' ? valueParam(value) : valueParam; + // we only set the value in the Store; + // the value in the local state will be updated + // by the useEffect during the next render + setItem( + key, + typeof newValue === 'undefined' + ? typeof runtimeDefaultValue === 'undefined' + ? defaultValue + : runtimeDefaultValue + : newValue + ); + }); return [value, set]; }; diff --git a/packages/ra-core/src/util/index.ts b/packages/ra-core/src/util/index.ts index 75f1a5dff10..7294d1352f3 100644 --- a/packages/ra-core/src/util/index.ts +++ b/packages/ra-core/src/util/index.ts @@ -9,7 +9,7 @@ import warning from './warning'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; import { getMutationMode } from './getMutationMode'; export * from './mergeRefs'; -export * from './useEventCallback'; +export * from './useEvent'; export { escapePath, diff --git a/packages/ra-core/src/util/useEvent.spec.tsx b/packages/ra-core/src/util/useEvent.spec.tsx new file mode 100644 index 00000000000..5b8d12a8e7c --- /dev/null +++ b/packages/ra-core/src/util/useEvent.spec.tsx @@ -0,0 +1,41 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { useEvent } from './useEvent'; + +describe('useEvent', () => { + const Parent = () => { + const [value, setValue] = React.useState(0); + const handler = useEvent(() => { + return 1; + }); + + return ( + <> + Parent {value}; + + + + ); + }; + + const Child = React.memo(({ handler }: { handler: () => number }) => { + const [value, setValue] = React.useState(0); + React.useEffect(() => { + setValue(val => val + 1); + }, [handler]); + + return Child {value}; + }); + + it('should be referentially stable', async () => { + render(); + expect(screen.getByText('Parent 0')).not.toBeNull(); + expect(screen.getByText('Child 1')).not.toBeNull(); + fireEvent.click(screen.getByText('Click')); + expect(screen.getByText('Parent 1')).not.toBeNull(); + expect(screen.getByText('Child 1')).not.toBeNull(); + fireEvent.click(screen.getByText('Click')); + expect(screen.getByText('Parent 2')).not.toBeNull(); + expect(screen.getByText('Child 1')).not.toBeNull(); + }); +}); diff --git a/packages/ra-core/src/util/useEventCallback.ts b/packages/ra-core/src/util/useEvent.ts similarity index 78% rename from packages/ra-core/src/util/useEventCallback.ts rename to packages/ra-core/src/util/useEvent.ts index 7bec7364a64..9a9714c68dc 100644 --- a/packages/ra-core/src/util/useEventCallback.ts +++ b/packages/ra-core/src/util/useEvent.ts @@ -11,9 +11,8 @@ const useLayoutEffect = * @see https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback * @see https://github.com/facebook/react/issues/14099#issuecomment-440013892 */ -export const useEventCallback = ( - fn: (...args: Args) => Return, - dependencies: any[] +export const useEvent = ( + fn: (...args: Args) => Return ): ((...args: Args) => Return) => { const ref = React.useRef<(...args: Args) => Return>(() => { throw new Error('Cannot call an event handler while rendering.'); @@ -21,8 +20,7 @@ export const useEventCallback = ( useLayoutEffect(() => { ref.current = fn; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fn, ...dependencies]); + }); return useCallback((...args: Args) => ref.current(...args), []); };