본문 바로가기

[React] render() 내에서 history.push(replace)를 하면 안되는 이유

스토어의 상태값 여부를 검사하여 상태값이 없으면 다른 라우터로 이동하는 로직을 상위 컴포넌트에 추가하였다.

const InfoChecker = ({children}) => {
  const {info} = useExampleStore()
  const history = useHistory()
  
  if (!info) {
    history.replace('/')
    return null
  }
  
  return children
}

const Info = () => {
  const {info, clearInfo} = useExampleStore()
  
  useEffect(() => {
    return () => {
      clearInfo()
    }
  }, [])
  
  return (
    <div>{info.name}</div>
  )
}

const Routes = () => {
  const {info} = useExampleStore()

  return (
    <InfoChecker>
      <Route exact path={'/'} render={() => <Home />} />
      <Route exact path={'/name'} render={() => <Info />} />
      <Redirect to={'/'} />
    </InfoChecker>
  )
}

이 때 아래처럼 Warning을 받게 된다.

 

Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

 

Render 메서드 내부에 state를 변경하지 말라는 Warning이었다. Render내부에서 state를 바꾸면 모든 setState에 대해 컴포넌트가 다시 렌더링되므로 무한 루프가 발생하므로 권장하지 않는다.

 

그런데 위 코드에서는 state를 아래처럼 렌더링 도중에 바꾸는 코드가 없다.

const Example = () => {
  const [isShow, setIsShow] = setState(false)
  
  const toggle = () => setIsShow(prev => !prev)
  
  return (
    <>
      <button onClick={toggle()}>toggle</button>
    </>
  )
}

크롬 디버거로 추적해보면 history.replace('/') 에서 Warning이 발생하고 있었다.

 

react-router가 페이지 이동을 할 때 props를 변경시키기 때문 + history는 mutable하므로 history.push(replace)는 컴포넌트의 props의 변경을 감지하고 리렌더링을 진행하므로 위와 같은 상황이 되버려 Warning이 발생하고 있는 것이었다.

 

출처. Why we should not have browserHistory.push('/some-path') in render() method ?
React runs by checking if props have changed, then re-renders. React Router changes the page through props. If you changed the state inside your render, the component would be caught in an infinite loop (rendering, updating the state, re-rendering, updating, etc etc).
출처. reactrouter.com
history is mutable
The history object is mutable. Therefore it is recommended to access thelocationfrom the render props of <Route>, not from history.location. This ensures your assumptions about React are correct in lifecycle hooks.

 

따라서 아래처럼 Redirect를 리턴하여 다른 페이지를 navigating하도록 한다.

const InfoChecker = ({children}) => {
  const {info} = useExampleStore()
  const history = useHistory()
  
  if (!info) {
    return <Redirect to={'/'} />
  }
  
  return children
}