Skip to content

Latest commit

 

History

History
239 lines (186 loc) · 8.3 KB

MIGRATION_GUIDE.md

File metadata and controls

239 lines (186 loc) · 8.3 KB

React 18 Migration guide

React Hooks Testing Library will not be updated to support React 18. Instead, React Testing Library and React Native Testing Library are including their own renderHook APIs with the goal of providing more unified and consistent experience for our users.

In general, the new renderHook functions are largely compatible with the React Hooks Testing Library version and many users will just be able to update their imports, but there are a few notable exceptions as well as some scenarios which are no longer supported at all. This guide will outline what has changed and what has been dropped and some strategies to smooth the transition.

Choose your renderer

React Hooks Testing Library supported three different React renderers that could be used for testing hooks in different types of environments, as well as a auto-detect import that would attempt to resolve whichever renderer happened to be installed. renderHook could be imported using any of the following modules:

  1. @testing-library/react-hooks/dom (react-dom), for testing hooks in a web environment
  2. @testing-library/react-hooks/native (react-test-renderer), for testing hooks in a react-native environment
  3. @testing-library/react-hooks/server (react-dom/server), for testing hooks in a SSR environment
  4. @testing-library/react-hooks, auto-detect either the dom or native variants based on the installed renderer

Depending on which renderer you were using will determine which package to migrate to.

  • @testing-library/react-hooks/dom

    npm uninstall @testing-library/react-hooks
    npm install --save-dev @testing-library/react
    -import { renderHook } from '@testing-library/react-hooks`;
    +import { renderHook } from '@testing-library/react';
  • @testing-library/react-hooks/native

    npm uninstall @testing-library/react-hooks
    npm install --save-dev @testing-library/react-native
    -import { renderHook } from '@testing-library/react-hooks`;
    +import { renderHook } from '@testing-library/react-native';
  • @testing-library/react-hooks/server

    There is not an equivalent renderer for this import in the @testing-library/react package. You will need to wrap the hook in your own test component and render it with react-dom/server manually.

  • @testing-library/react-hooks

    If your project is a react-native app, follow the instructions above for @testing-library/react-hooks/native, otherwise follow the instructions for @testing-library/react-hooks/dom.

waitFor

This utility should now be imported at the same time as renderHook instead of being accessed from the renderHook return value.

-import { renderHook } from '@testing-library/react-hooks`;
+import { renderHook, waitFor } from '@testing-library/react';
+// or import { renderHook, waitFor } from '@testing-library/react-native';
-const { result, waitFor } = renderHook(() => useHook());
+const { result } = renderHook(() => useHook());

The React Hooks Testing Library version of waitFor supported either returning a boolean value or using assertions (e.g. expect) to wait for the condition to be met. Both the React Testing Library and React Native Testing Library version only support the assertion style for their waitFor utilities. If you were using the boolean style, you will need to update the callbacks like so:

-await waitFor(() => result.current.state !== 'loading');
+await waitFor(() => {
+  expect(result.current.state).not.toBe('loading');
+});

It should also be noted that the React Hooks Testing Library version of waitFor would recheck the condition any time the hook triggered a render, as well as on a periodic interval but due to implementation differences of waitFor in the new version, the condition will only be checked on the interval. If your condition can potentially be missed by waiting for the default interval time (100ms), you may need to adjust the timings using the interval option:

await waitFor(() => {
  expect(result.current.state).not.toBe('loading');
-});
+}, { interval: 20 });

waitForValueToChange

This utility has not been included in the React Testing Library or React Native Testing Library APIs. A similar result can be achieved by using waitFor:

-await waitForValueToChange(() => result.current.state);
+const initialValue = result.current.state;
+await waitFor(() => {
+  expect(result.current.state).not.toBe(initialValue);
+});

Alternatively this utility can be implemented as the following, for a direct migration:

import { waitFor } from '@testing-library/react';

const waitForValueToChange = async <T>(getValue: () => T) => {
    const original = getValue();

    await waitFor(async () => {
        expect(await original).not.toBe(await getValue());
    });
};

waitForNextUpdate

This utility has not been included in the React Testing Library or React Native Testing Library APIs. A similar result can be achieved by using waitFor:

-await waitForValueToChange(() => result.current.state);
+const initialValue = result.current;
+await waitFor(() => {
+  expect(result.current).not.toBe(initialValue);
+});

Note that this is not quite the same as the previous implementation, which simply waited for the next render regardless of whether the value of result.current has changed or not, but this is more in line with how the utility was intended to be used. Writing tests that rely on specific timing or numbers of renders is discouraged in the Testing Library methodology as it focuses too much on implementation details of the hooks.

result.error

Errors are now thrown directly from renderHook, rerender and unmount calls. If you were previously using result.error to test for error values, you should update your tests to instead check for thrown errors:

-const { result } = renderHook(() => useHook());
-expect(result.error).toBe(Error('something expected'));
+expect(() => renderHook(() => useHook())).toThrow(Error('something expected'));

There is an edge case that is no longer covered which is when an asynchronous update to a hook causes the next render to throw an error, e.g.

function useAsyncValue() {
  const [loading, setLoading] = useState(true)
  const [value, setValue] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    getAsyncValue()
      .then(setValue)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [])

  if (error) {
    throw error
  }

  return { loading, value }
}

In this scenario, calling renderHook(() => useAsyncValue()) will not throw any errors. Tests that need to access an asynchronous error like this can use the wrapper option to wrap the hook call in an error boundary and capture the error there instead:

+let asyncError = null;
+
+class ErrorBoundary extends React.Component {
+  componentDidCatch(error) {
+    asyncError = error;
+  }
+
+  render() {
+    return !asyncError && this.props.children;
+  }
+ }

-const { result, waitFor } = renderHook(() => useAsyncValue());
+const { result } = renderHook(() => useAsyncValue(), {
+  wrapper: ErrorBoundary,
+});

await waitFor(() => {
- expect(result.error).toEqual(Error('something expected'));
+ expect(asyncError).toEqual(Error('something expected'));
});

result.all

The new renderHook APIs in React Testing Library and React Native Testing Library have not included result.all as it was deemed to promote testing implementation details. Tests that rely on result.all should be rewritten to just use result.current and/or waitFor with more emphasis on testing the value that will be observed by users and not the intermediate values in between observable results.

Suspense

Previously, React Hooks Testing Library would automatically wrap the hook call in a Suspense boundary. This functionality has not been replicated in either React Testing Library or React Native Testing Library so hooks that rely on suspense will need to add their own suspense boundaries using the wrapper option:

+const SuspenseBoundary = ({ children }) => <Suspense fallback={null}>{children}</Suspense>

-const { result } = renderHook(() => useSuspendingHook());
+const { result } = renderHook(() => useSuspendingHook(), {
+  wrapper: SuspenseBoundary,
+});

wrapper Props