Skip to content

React v18 migration

SeungAh Hong5min read

React v16 to v18 Migration Guide (with Best Practice Code)

Updates to Client Rendering APIs Migration

The new root API also enables the new concurrent renderer, which allows you to opt-into concurrent features.

// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
 
// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App tab="home" />);

We’ve also changed unmountComponentAtNode to root.unmount:

// Before
unmountComponentAtNode(container);
 
// After
root.unmount();

We’ve also removed the callback from render, since it usually does not have the expected result when using Suspense:

// Before
const container = document.getElementById('app');
render(<App tab="home" />, container, () => {
  console.log('rendered');
});
 
// After
function AppWithCallbackAfterRender() {
  useEffect(() => {
    console.log('rendered');
  });
 
  return <App tab="home" />;
}
 
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<AppWithCallbackAfterRender />);

How to Upgrade for the New JSX Transform

CRA: v4.0.0 +

eslint-plugin-react

{
  // ...
  "rules": {
    // ...
    "react/jsx-uses-react": "off",
    "react/react-in-jsx-scope": "off"
  }
}

custom babel

@babel/plugin-transform-react-jsx

# for npm users
npm update @babel/core @babel/plugin-transform-react-jsx
 
# for yarn users
yarn upgrade @babel/core @babel/plugin-transform-react-jsx

@babel/preset-react

# for npm users
npm update @babel/core @babel/preset-react
 
# for yarn users
yarn upgrade @babel/core @babel/preset-react

babel config

// If you are using @babel/preset-react
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]
}
 
// If you're using @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "runtime": "automatic"
    }]
  ]
}

Bulk-Removing Unnecessary Imports After Upgrading to v17

react-codemod

// general react
import React from 'react';
 
function App() {
  return <h1>Hello World</h1>;
}
 
// transpile;
function App() {
  return <h1>Hello World</h1>;
}
// custom hook
import React from 'react';
 
function App() {
  const [text, setText] = React.useState('Hello World');
  return <h1>{text}</h1>;
}
 
import { useState } from 'react';
 
function App() {
  const [text, setText] = useState('Hello World');
  return <h1>{text}</h1>;
}

Trouble Shooting

Click event using window.addEventListener is fired prematurely

export function useOutsideClick({
  callback,
}: UseOutsideClickProps) {
  const [element, setElement] = useState<HTMLElement>();
  const ref = useCallback((el) => setElement(el), []);
 
  useEffect(() => {
    const handler = (event) => {
      if (element && !element.contains(event.target)) {
        callback(event);
      }
    }
 
    if (element) {
      document.addEventListener('click', handler);
    }
 
    return () => {
      document.removeEventListener('click', handler);
    };
  }, [element, callback]);
 
  return ref;
}
 
// Actual usage example
const [isPreviewTypeListVisible, setPreviewTypeListVisible] = useState(false);
 
// Before
useEffect(() => {
  const handleDocumentClick = () => isPreviewTypeListVisible && setPreviewTypeListVisible(false);
  document.addEventListener('click', handleDocumentClick);
 
  return () => document.removeEventListener('click', handleDocumentClick);
}, [isPreviewTypeListVisible]);
 
// After
const ref = useOutsideClick({
  callback: () => setPreviewTypeListVisible(false),
});
 
<div
  ref={ref}
  className={cx(styles.container, className)}
>
	<button
	  type="button"
	  onClick={() => setPreviewTypeListVisible(!isPreviewTypeListVisible)}
	>
</div>

Property 'createRoot' does not exist on type '@types/react-dom/index")'. ts(2339)

  1. Check the react-dom version. You need to install v18 or higher.
  2. Even after installing v18 or higher, an issue can occur where an external library references a different React version.
    1. This happens when the library specifies the React version as a peerDependency.
  3. In that case, pin the version using resolutions in package.json.
"resolutions": {
  ...,
  "react": "18.2.0"
}

TypeError: Cannot read properties of null (reading 'useMemo')' error Redux in my react redux

  1. Install react-redux version 8 or higher.
  2. Reference: TypeError: Cannot read properties of null (reading 'useMemo')
  3. Reference: Overload 1 of 2, '(props: ProviderProps | Readonly<ProviderProps>): Provider',
  4. For now, since updating react-redux also requires updating redux, this is temporarily handled with ts-ignore.
<QueryClientProvider client={queryClient}>
    {/* @ts-ignore */}
    <Provider store={store}>
    </Provider>
</QueryClientProvider>

'ReactNode' is not assignable to type 'React.ReactNode'

  1. This is caused by an external library dependency. As above, pin the version using resolutions.
  2. Reference: React18 : Type{} is not assignable to type 'ReactNode' 해결
"resolutions": {
  "@types/react": "18.2.0"
},

index.js:1 Error: createRoot(...): Target container is not a DOM element

  1. This occurs when the target element is not a DOM object.
// Operation error in useMemo, forwardRef
if (!element) return;

No overload matches this call. Overload 1 of 2, '(props: ProviderProps | Readonly<ProviderProps>): Provider', gave the following error....

  • Starting from React 18, children must be declared explicitly.
  • Declaring children
// Option 1. Add the children type directly
interface Props {
  isOpened: boolean;
  handleCloseModal: () => void;
  children: ReactNode;
}
// Option 2. Use the PropsWithChildren type provided by React
function Component(props: PropsWithChildren<Props>);
  • Reference 2 (why children was excluded from the default type in React 18):
    1. Because you cannot determine whether children exist and it is difficult to infer the type of children, it was excluded starting from version 18.
    2. Removal of implicit children

Issues caused by automatic batching

https://github.githubassets.com/favicon.ico

const { mutate } = usePostSettlementInfo({
  onSuccess: () => {
    if (path) { // Without wrapping in flushSync, automatic batching kicks in and the state doesn't update
      ...
    }
  },
});
 
const onSubmit = handleSubmit((data) => {
    ....
    mutate(data);
  });
 
const handleSave = async (path: string) => {
  flushSync(() => {
    setPath(path);
  });
  await onSubmit();
};

Uncaught ReferenceError: React is not defined

// If you are using @babel/preset-react
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]
}
 
// If you're using @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "runtime": "automatic"
    }]
  ]
}

defaultProps no longer supported

Starting from version 18.3, defaultProps is deprecated. (A warning is shown.)

We are currently on version 18.2, so please be careful not to add defaultProps going forward.

Reference: Stop using defaultProps! | Sophia Willows

https://sophiabits.com/favicon.ico

Library Issue

Switching to the react-helmet-async library

Cause

  • Starting from React 17, componentWillMount, componentWillReceiveProps, and componentWillUpdate are no longer supported, and to keep using them you need to migrate via the UNSAFE_ prefix. Because react-helmet was using methods that are not supported in version 17, warnings were raised.
  • Update on Async Rendering – React Blog

Improvement

Untitled

react-hook-form isDirty delay update

// Since the form data was reset after saving with react-hook-form, the state was set to true instead of false, resulting in a situation where the prompt message had to be handled (occurs from v18 onward)
 
<Prompt
  when={isDirty && !submited}
  message={() =>
    `You have unsaved changes, sure you want to go to leave this page? `
  }
/>

Issue where the antd datepicker gets blocked when the input tag is focused in older versions

Untitled

Jest Issue

Updating Jest after upgrading to React 18

  • Encountered the error TypeError: Cannot read property 'current' of undefined

  • GitHub - testing-library/react-hooks-testing-library: 🐏 Simple and complete React hooks testing utilities that encourage good testing practices. 'Change in how waitFor is used'

    // Change in the declaration
    //// Before
    const { result, waitFor } = renderHook(() => useRewards(), {
      wrapper: <App />,
    });
     
    //// After
    import { act, renderHook, waitFor } from '@testing-library/react';
     
    // Add expect inside the waitFor statement
    //// Before
    await waitFor(() => result.current.isSuccess);
     
    //// After
    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    userEvent click malfunction

    // Before
    import userEvent from '@testing-library/user-event';
    userEvent.click(button);
     
    // After
    import { render, screen, waitFor } from '@testing-library/react';
    await waitFor(() => userEvent.click(button));

Reference Pages