diff --git a/src/data/apiHooks.test.tsx b/src/data/apiHooks.test.tsx index 996a62daf1..7dd143f805 100644 --- a/src/data/apiHooks.test.tsx +++ b/src/data/apiHooks.test.tsx @@ -1,3 +1,6 @@ +import { useRef } from 'react'; +import userEvent from '@testing-library/user-event'; + import { initializeMocks, cleanup, @@ -5,7 +8,7 @@ import { render, waitFor, } from '../testUtils'; -import { useWaffleFlags } from './apiHooks'; +import { createGlobalState, useWaffleFlags } from './apiHooks'; import { getApiWaffleFlagsUrl } from './api'; // A little component for testing our waffle flag hooks. @@ -110,3 +113,60 @@ describe('useWaffleFlags', () => { expect(await screen.findByLabelText('useReactMarkdownEditor')).toHaveTextContent('enabled'); }); }); + +// A little component for testing the global state hooks. +const useCounter = createGlobalState<{ count: number; }>(() => ['test', 'counter'], { count: 0 }); + +const CounterComponent = () => { + const { data, setData, resetData } = useCounter(); + const firstSetData = useRef(setData); + const firstResetData = useRef(resetData); + const callbacksKeptIdentity = setData === firstSetData.current && resetData === firstResetData.current; + + return ( + + ); +}; + +describe('createGlobalState', () => { + it('keeps its callbacks across renders, so effects depending on them do not re-run', async () => { + const user = userEvent.setup(); + initializeMocks(); + render(); + await waitFor(() => expect(screen.getByLabelText('count')).toHaveTextContent('0')); + + await user.click(screen.getByRole('button', { name: 'increment' })); + await waitFor(() => expect(screen.getByLabelText('count')).toHaveTextContent('1')); + + expect(screen.getByLabelText('callbacks')).toHaveTextContent('same'); + }); + + it('resets the value it stores', async () => { + const user = userEvent.setup(); + initializeMocks(); + render(); + await waitFor(() => expect(screen.getByLabelText('count')).toHaveTextContent('0')); + + await user.click(screen.getByRole('button', { name: 'increment' })); + await waitFor(() => expect(screen.getByLabelText('count')).toHaveTextContent('1')); + + await user.click(screen.getByRole('button', { name: 'reset' })); + await waitFor(() => expect(screen.getByLabelText('count')).toHaveTextContent('0')); + }); +}); diff --git a/src/data/apiHooks.ts b/src/data/apiHooks.ts index ba8baee8ff..f2652f3994 100644 --- a/src/data/apiHooks.ts +++ b/src/data/apiHooks.ts @@ -1,5 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { AxiosError } from 'axios'; +import { useCallback, useMemo } from 'react'; + import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { UserAgreement, UserAgreementRecord } from '@src/data/types'; @@ -158,7 +160,7 @@ export function createGlobalState( ) { return (queryKeyArgs?: any) => { const queryClient = useQueryClient(); - const queryKey = queryKeyFn(queryKeyArgs); + const queryKey = useMemo(() => queryKeyFn(queryKeyArgs), [queryKeyArgs]); const { data } = useQuery({ queryKey, @@ -170,15 +172,13 @@ export function createGlobalState( refetchIntervalInBackground: false, }); - function setData(x: Partial) { + const setData = useCallback((x: Partial) => { queryClient.setQueryData(queryKey, x); - } + }, [queryClient, queryKey]); - async function resetData() { - await queryClient.invalidateQueries({ - queryKey, - }); - } + const resetData = useCallback(async () => { + await queryClient.invalidateQueries({ queryKey }); + }, [queryClient, queryKey]); return { data, setData, resetData }; };