Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

renderHook: Context Provider, state change in child function act() error #1206

Open
quintonjs opened this issue May 3, 2023 · 5 comments
Open
Labels
question Further information is requested

Comments

@quintonjs
Copy link

quintonjs commented May 3, 2023

I have a custom hook that uses a Context Provider for global state management. I am passing a dispatch and data from the state taken from the context inside the hook to a function that will at the end of requesting data from an endpoint dispatch this data coursing a state update inside the context API Provider passed down to the custom hook.

The issue is when I test the custom hook with a wrapper of the Context Provider passed into renderHook() function, I get in the test run the dreaded "act()" error of:

Warning: An update to StoreProvider inside a test was not wrapped in act(...)
      20 |      .then((res) => {
    > 22 |              setLoading(false)
         |              ^
      23 |              dispatch({ 
      24 |                      type: ActionTypesEnum.FETCH_USERS, 
      25 |                      payload: res.data

you can see the full explanation at: https://stackoverflow.com/questions/76157226/react-hooks-testing-library-context-provider-state-change-in-child-function-ac

The useUserSearch Hook:

const useUserSearch = (searchCriteria: string): SearchHookProps => {
  // data from the context and stored in the state
  const { dispatch, state } = useStateContext();
  const { users } = state;
  const [ userData, setUserData ] = useState<UserProps[]>([]);
  const [ loading, setLoading ] = useState<boolean>(false);
  const [ error, setError ] = useState<boolean>(false);
  // connected to the pagination affected by the search
  const [ currentPage, setCurrentPage ] = useState<number>(1)
  const [ totalPages, setTotalPages ] = useState<number>(1)
  const [ totalUserCount, setTotalUserCount ] = useState<number>(0)
  const totalPerPage = 12

  useEffect(() => {
    if (users === null) FetchUsers(dispatch, searchCriteria, setLoading, setError);
  }, [users]);

  useEffect(() => {
      if (searchCriteria.length > 0) FetchUsers(dispatch, searchCriteria, setLoading, setError);
  }, [searchCriteria]);

  return {
    userData,
    totalUserCount,
    setCurrentPage,
    currentPage,
    totalPages,
    loading,
    error,
  }
}

FetchUsers function:

const FetchUsers = (
	dispatch: Dispatch<any>,
	searchCriteria: string,
	setLoading: Dispatch<SetStateAction<boolean>>,
	setError: Dispatch<SetStateAction<boolean>>,) => {
	const { global: { apiUrl } } = AppConfig

	const search = `?q=${encodeURIComponent(searchCriteria)}`

	setLoading(true)
	axios.get(`${apiUrl}/users?q=${search}+in:login+type:user`)
	.then((res) => {
		setLoading(false)
		dispatch({ 
			type: ActionTypesEnum.FETCH_USERS, 
			payload: res.data
		})
	})
	.catch(() => {
		setLoading(false)
		setError(true)
	})
}

StoreProvider:

import React, { useContext, useReducer, useMemo, createContext } from 'react';
import Reducer from '../../Reducers';
import AppConfig from '../../Configs';
import { StateInterface, StoreInterface, ProviderInterface } from '../../Interfaces';

export const initialState: StateInterface = {
  appConfig: AppConfig,
  users: null,
};

export const StoreContext = createContext({
  state: initialState,
} as StoreInterface);

export const useStateContext = () => useContext(StoreContext);

export const StoreProvider = ({ children }: ProviderInterface) => {
  const [state, dispatch] = useReducer(Reducer, initialState);
  const StoreProviderValue = useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return <StoreContext.Provider value={StoreProviderValue}>{children}</StoreContext.Provider>;
};

The Test spec file:

import React from 'react'
import { render, cleanup, waitFor, renderHook, act } from '@testing-library/react'
import axios from 'axios'

import usersMock from '../__mocks__/usersMock'
import { StoreProvider } from '../Providers'
import useUserSearch from './useUserSearch'

jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('Test that <User />', () => {
  const renderHookComponent = (searchCriteria: string) => {
    const wrapper = ({ children }: any) => <StoreProvider>{children}</StoreProvider>
    const { result, rerender, unmount } = renderHook(() => useUserSearch(searchCriteria), {
      wrapper: wrapper as React.ComponentType<any>
    })
    return { result, rerender, unmount }
  }

  afterEach(() => {
    cleanup()
  })

  it('renders the hook useUserSearch', async () => {
    mockedAxios.get.mockResolvedValue(usersMock);
    const { result } = renderHookComponent('test')

    // await waitFor(() => {
    //   expect(result.current.userData.length).toBe(0)
    // })
  })
})
@quintonjs quintonjs added the question Further information is requested label May 3, 2023
@mpeyper
Copy link
Member

mpeyper commented May 4, 2023

Your imports suggest you are using renderHook from @testing-library/react rather than from this library (@testing-library/react-hooks). It's likely to be an issue with your implementation rather than anything we/they are doing (when to act can be tricky in async hooks), but I'll transfer the issue over there for you for them to confirm.

@mpeyper mpeyper transferred this issue from testing-library/react-hooks-testing-library May 4, 2023
@mpeyper mpeyper changed the title react-hooks-testing-library: Context Provider, state change in child function act() error renderHook: Context Provider, state change in child function act() error May 4, 2023
@dev-frid
Copy link

dev-frid commented Jun 20, 2023

Getting this as well for a hook that sets state in an effect.

Everywhere on the internet tells to use waitForNextUpdate returned by renderHook, but that function was deleted during the migration from @testing-library/react-hooks to @testing-library/react, so I'm stuck with the warning.

There is nothing to act() or waitFor in my case, either, so I'm just stuck with the error...

@MatanBobi
Copy link
Member

@dev-frid waitForNextUpdate was too much of a React implementation detail and we couldn't support it in RTL.
The option we provide now is to use waitFor and wait for a specific assertion, for example:

await waitFor(() => {
  expect(mockFunction).toHaveBeenCalled();
})

@mpeyper
Copy link
Member

mpeyper commented Jun 21, 2023

It may not help in your case (if there is truely nothing to waitFor, but I have proposed some utilities that replicate some of the old features using the new API in the "will probably never actually be completed" migration guide.

@quintonjs
Copy link
Author

Thanks, guys for all your input, I took my eyes away from this thread for a wee while too long. will look through the comments and see what I can do with my code to rectify the issue I have. Thanks all!
P.S. will let you know how it goes :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants