홍승아블로그

React v18 migration

React v16 to v18 마이그레이션 가이드(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 />);

새 JSX 변환을 위해서 업그레이드 방법

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"
    }]
  ]
}

v17 버전 업한 이후에 불필요한 import 일괄삭제

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;
}

// 실 사용예
const [isPreviewTypeListVisible, setPreviewTypeListVisible] = useState(false);

// 수정 전
useEffect(() => {
  const handleDocumentClick = () => isPreviewTypeListVisible && setPreviewTypeListVisible(false);
  document.addEventListener('click', handleDocumentClick);

  return () => document.removeEventListener('click', handleDocumentClick);
}, [isPreviewTypeListVisible]);

// 수정 후
const ref = useOutsideClick({
  callback: () => setPreviewTypeListVisible(false),
});

<div
  ref={ref}
  className={cx(styles.container, className)}
>
	<button
	  type="button"
	  onClick={() => setPreviewTypeListVisible(!isPreviewTypeListVisible)}
	>
</div>

‘@types/react-dom/index”)’ 형식에 ‘createRoot’ 속성이 없습니다.ts(2339)

  1. react-dom 버전 확인. v18이상 설치 필요
  2. v18이상 설치 후에도 외부 라이브러리에서 다른 react 버전을 참조하는 이슈가 발생
    1. 라이브러리에서 peerDependencies로 react 버전을 명시한 경우
  3. 이 때는 package.json에 resolution 으로 버전을 명시
"resolutions": {
  ...,
  "react": "18.2.0"
}

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

  1. react-redux 8버전 이상 설치
  2. 참고: TypeError: Cannot read properties of null (reading ‘useMemo’)
  3. 참고: Overload 1 of 2, ‘(props: ProviderProps | Readonly<ProviderProps>): Provider‘,
  4. 현재는 react-redux 업데이트 시 redux도 같이 업데이트 해야해서 ts-ignore로 임시처리
<QueryClientProvider client={queryClient}>
    {/* @ts-ignore */}
    <Provider store={store}>
    </Provider>
</QueryClientProvider>

‘ReactNode’ is not assignable to type ‘React.ReactNode’

  1. 외부 라이브러리 의존성으로.. 위와 같이 resolution 으로 버전을 명시
  2. 참고: 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. 대상 element가 DOM 객체가 아닌 경우 발생
// useMemo, forwardRef 에서 동작오류
if (!element) return;

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

  • react18 이상부터 children을 명시적으로 선언해야 함.
  • children 명시
// 방법 1. children 타입을 직접 추가
interface Props {
  isOpened: boolean;
  handleCloseModal: () => void;
  children: ReactNode;
}
// 방법 2. 리액트에서 제공해주는 PropsWithChildren 타입을 사용
function Component(props: PropsWithChildren<Props>);
  • 참고 2(children이 react18에서 기본 타입에서 제외된 이유) :
    1. 자식 유무에 대해 파악 할 수 없고, 자식의 타입 추정이 어려워 18버전 부터는 제외
    2. Removal of implicit children

automatic batching 인한 이슈

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

const { mutate } = usePostSettlementInfo({
  onSuccess: () => {
    if (path) { // flushSync 감싸지 않을 경우 자동배치 처리되다보니 state 업데이트 안되는 이슈
      ...
    }
  },
});

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 미지원

18.3 이후 버전부터는 defaultProp가 deprecated 됩니다. (warning 표시)

현재는 18.2 버전으로 되어있고 추후 defaultProps 추가하지 않도록 유의해 주세요.

참고: Stop using defaultProps! | Sophia Willows

https://sophiabits.com/favicon.ico

Library Issue

react-helmet-async 라이브러리로 교체

원인

  • 리액트 17버전 이후부터는 componentWillMount, componentWillReceiveProps, and componentWillUpdate 지원하지 않으며, 추가를 위해서는 UNSAFE_ PREFIX를 통해서 개선이 가능합니다. 17버전에서 지원하지 않는 메서드를 react-helmet를 사용하고 있어서 경고가 발생하였습니다.
  • Update on Async Rendering – React Blog

개선 사항

Untitled

react-hook-form isDirty delay update

// react-hook-form으로 저장완료 이후에 form 데이터값을 reset 처리했으니 상태가 false가 아닌 true가 설정되서 prompt message 처리를 해야하는 상황(v18 이후부터 발생)

<Prompt
  when={isDirty && !submited}
  message={() =>
    `You have unsaved changes, sure you want to go to leave this page? `
  }
/>

antd datepiacker 이전 버전에서 input tag 포커싱 될 시 블락되는 이슈

Untitled

Jest Issue

react18 업데이트 이후에 jest 버전 업데이트

  • 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. ‘waitFor 사용법 변경’

    // 선언부 변경
    //// 수정 전
    const { result, waitFor } = renderHook(() => useRewards(), {
      wrapper: <App />,
    });
    
    //// 수정 후
    import { act, renderHook, waitFor } from '@testing-library/react';
    
    // waitFor 문에 expect 추가
    //// 수정 전
    await waitFor(() => result.current.isSuccess);
    
    //// 수정 후
    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    userEvent click 오동작

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

참고페이지

이전글
미사용 의존성 제거
다음글
next14 migration