react-router v6의 useNavigate 후크가 낭비되는 재렌더링을 트리거하는 이유와 해결 방법

내 React.js 프로젝트 중 하나에서 성능을 최적화하는 동안 뚜렷한 이유 없이 구성 요소가 다시 렌더링되는 것을 우연히 발견했습니다. 몇 가지 실험 후 범인이 발견되었습니다.

import { useNavigate } from "react-router-dom"; // v6

...

const Component = () => {
    const navigate = useNavigate();
    ...
}

구성 요소에서 useNavigate 후크를 사용하면 경로가 변경되지 않은 경우에도 navigate()를 호출하거나 <Link />를 클릭할 때마다 다시 렌더링됩니다. React.memo()로 이를 방지할 수 없습니다.

데모는 다음과 같습니다.



첫 번째 블록은 호출하지 않고useNavigate 한 번만 렌더링됩니다. 두 번째는 후크를 사용하고 모든 경로 "변경"에서 두 번 다시 렌더링됩니다(왜 두 번, 아마도 useNavigate가 다시 비난을 받는지 명확하지 않습니다 🤷). 세 번째는 useNavigate 의 "안정적인"버전을 사용합니다. 자세한 내용은 아래에서 설명합니다.

특히 react-router v5의 useHistory가 다시 렌더링을 일으키지 않았기 때문에 이것은 예상치 못한 동작이라고 말하고 싶습니다. 이 동작에 대한 GitHub의 긴 글discussion이 있습니다. 그것은 버그가 아니라 예상되는 동작이라는 입장으로 귀결됩니다.

에 대한 댓글 #7634







timdorr
에 댓글을 달았습니다.



useNavigate는 현재 위치가 변경되면 변경됩니다. 상대 탐색에 따라 달라집니다. memo로 래핑하면 상위 구성 요소에서 다시 렌더링되는 것만 방지됩니다. 구성 요소 내의 후크로 인해 다시 렌더링이 발생하면 아무 것도 할 수 없습니다memo.


View on GitHub


useNavigate subscribes to contexts 경로 변경이 트리거될 때 변경되기 때문에 발생합니다(동일하게 유지되더라도).

let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();


일반적으로 경로 변경은 보기 변경을 의미하고 어쨌든 새 구성 요소 세트를 렌더링해야 하기 때문에 큰 문제는 아닙니다. 여러 메뉴 요소를 다시 렌더링하는 것은 문제가 되지 않습니다.

그러나 뷰를 변경하지 않고 경로에서 매개변수를 변경하거나 경로 변경과 독립적인 상수 구성 요소가 많으면 고통스러울 수 있습니다.

이 문제를 해결하는 방법에는 여러 가지가 있습니다.
  • useNavigate 가능한 가장 작은/최저 수준 구성 요소에 후크를 사용합니다. 다시 렌더링하지 않아도 되지만 비용이 적게 듭니다.
  • 가능한 경우 구성 요소에서 후크 사용을 분리합니다. 예를 들어 내 구성 요소 중 일부는 팝업 및 해당 구성 요소로 전달되는 알림navigate 기능을 트리거할 수 있습니다. 팝업 및 알림 구성 요소 자체에 대한 후크를 이동할 수 있지만 그렇지 않으면 간단한 설정이 불필요하게 복잡해집니다.
  • 후크를 별도의 컨텍스트에 넣고 useRef 후크에서 변경 가능한 개체를 활용하여 후크를 "안정화"합니다. 이것은 this approach 의 단순화된 버전입니다.

  • // StableNavigateContext.tsx
    
    import { 
      createContext,
      useContext,
      useRef, 
      MutableRefObject 
    } from "react";
    import { 
      useNavigate, 
      NavigateFunction 
    } from "react-router-dom";
    
    const StableNavigateContext = createContext<MutableRefObject<
      NavigateFunction
    > | null>(null);
    
    const StableNavigateContextProvider = ({ children }) => {
      const navigate = useNavigate();
      const navigateRef = useRef(navigate);
    
      return (
        <StableNavigateContext.Provider value={navigateRef}>
          {children}
        </StableNavigateContext.Provider>
      );
    };
    
    const useStableNavigate = (): NavigateFunction => {
      const navigateRef = useContext(StableNavigateContext);
      if (navigateRef.current === null)
        throw new Error("StableNavigate context is not initialized");
    
      return navigateRef.current;
    };
    
    export {
      StableNavigateContext,
      StableNavigateContextProvider,
      useStableNavigate
    };
    
    
    // App.tsx
    
    import { BrowserRouter } from "react-router-dom";
    import { 
      StableNavigateContextProvider 
    } from "./StableNavigateContext";
    
    export default function App() {
      return (
        <BrowserRouter>
          <StableNavigateContextProvider>
            // ...
          </StableNavigateContextProvider>
        </BrowserRouter>
      );
    }
    
    
    // Component file
    
    import { useStableNavigate } from "./StableNavigateContext";
    
    const Component = () => {
      const navigate = useStableNavigate();
      // ...
    };
    

    useLocation 후크에 대해 유사한 접근 방식을 사용하거나 original solution 에서와 같이 하나의 컨텍스트에서 결합할 수 있습니다. 그러나 구성 요소는 더 이상 경로 변경 시 다시 렌더링되지 않으므로 상태가 오래될 수 있습니다.

    좋은 웹페이지 즐겨찾기