Dependency Array를 어떻게 비교할까?

January 30, 2022

0. 작성하는 이유

  • useEffect의 Dependency Array는 어떻게 비교를 해서 호출할까?
  • componentDidUpdate 처럼 사용하고 싶은데... 어떻게 하지?
  • referenceType일 경우, 별도의 작업없이 사용할 수 없을까? (custrom hook)

1. Component Render(ReRender) Flow

  • 초기 Render : init ComponentuseEffect
  • Re-Render: init Component → clean up useEffectuseEffect
const NumberComp = () => {
    console.log('init Component')

    const [state, setState] = useState(0)

    const handleClick = () => {
        setState((prev) => prev + 1)
    }

    useEffect(() => {
        console.log('useEffect')

        return () => {
            console.log('clean up useEffect')
        }
    }, [state])

    return (
        <div>
            <button onClick={handleClick}>Change State</button>
        </div>
    )
}

2. react-dom 코드

  • useEffect 실행코드(update 조건 충족시)와 dependency Check부분을 확인해보자

    function areHookInputsEqual(nextDeps, prevDeps) {
    	...
    	for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (objectIs(nextDeps[i], prevDeps[i])) {
          continue;
        }
        return false;
      }
    
      return true;
    }
    
    function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
      ...
    
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    
      ...
      hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
    }
  • Object.is 함수를 활용하여, prev/next dependency reference 체크를 통해 Effect가 발생하는것을 확인할 수 있다!

    const foo = {
        foo: 1,
    }
    const bar = foo
    Object.is(foo, { foo: 1 }) // false
    Object.is(foo, bar) // true
    Object.is(1, 1) // bar

3. Dependency Type에 따라 다르게 작동하는 Effect...

  1. Number, String, Bool - Primitive 타입일 경우

    • State가 변경이 된다면, 함수가 새롭게 호출된다.
    • State가 변경이 안되었다면,(값이 이전 값과 동일하다면) 함수가 호출되지 않는다.
    const Component = () => {
        const [state, setState] = useState(0)
    
        const handleClick = () => {
            setState((prev) => prev + 1)
        }
    
        const handleEqual = () => {
            setState((prev) => prev)
        }
    
        useEffect(() => {
            console.log('changing State call useEffect initialization')
        }, [])
    
        useEffect(() => {
            console.log('changing State call useEffect')
        }, [state])
    
        return (
            <div>
                <h1>Primitive</h1>
                <button onClick={handleClick}>
                    Change State(If state dont change, useEffect no call)
                </button>
                <button onClick={handleEqual}>
                    Equal State(useEffect no call)
                </button>
            </div>
        )
    }
  2. Array, Object - reference 타입일 경우

    • Imutable하게 관리하기 때문에, 매번 새롭게 생성이 된다 → 함수가 새롭게 호출된다.

    • Object의 경우, JSON.stringigy(to JSON string)의 형태로 변경여부를 체크할 수 있지만, (JSON Object의 경우, 지원하지 않는 타입(undefined, Functions, Symbol...)이 있으므로, 주의)

      const Component = () => {
      
      	const [state, setState] = useState({foo: {bar: 1}, test: 1})
      
        const changeState = () => {
          setState(prev => ({ ...prev, foo : {bar : 2}, bar: 1 }))
        }
      
        const handleImmutable = () => {
          setState(prev => ({foo: {bar: 1}, test: 1}))
        }
      
        const handleMutable = () => {
          setState(prev => {
            delete prev.foo.bar
            return prev
          })
        }
      
        useEffect(() =>{
          console.log('change State call useEffect initialization')
        }, [])
      
        useEffect(() => {
          console.log('change State call useEffect')
        }, [JSON.stringify(state)])
      
      return (
          <div>
            <h1>reference</h1>
            <button onClick={changeState}> Change State</button>
            <button onClick={handleImmutable}> Immutable State(Every useEffect call)</button>
            <button onClick={handleMutable}> mutable State(If use mutable Object, useEffect no call)</button>
          </div>
        )
  3. Function일 경우

    • 함수의 경우, 매번 새롭게 생성되기 때문에 함수가 매번 호출된다

    • useCallback을 통해 함수를 Memorization을 해주면, 함수가 변경될때만 호출되게 된다.

      const Component = () => {
          const [state, setState] = useState(0)
      
          const changeState = () => {
              setState((prev) => prev + 1)
          }
      
          const changeFunction = () => {
              console.log('call changeFunction')
          }
      
          const momeChangeFunction = useCallback(() => {
              console.log('call momeChangeFunction')
          }, [state])
      
          useEffect(() => {
              console.log('change Function call useEffect')
              return () => {
                  console.log('clean up useEffect')
              }
          }, [changeFunction])
      
          useEffect(() => {
              console.log('change Function call useEffect initialization')
              return () => {
                  console.log('clean up initialization')
              }
          }, [])
      
          return (
              <div>
                  <h1>Function</h1>
                  <button onClick={changeState}> Change State</button>
              </div>
          )
      }

4. referenceType일 경우, 간단하게 사용할 수 없을까? (use-deep-compare-effect)

  • 그 전의 값을 알고 있어야 하기 때문에! useRef를 활용해서 Memorization 하자

  • useEffect의 호출을 직접 다룰 수는 없지만, useEffect안에 있는 Effect를 다루자

    function useDeepCompareMemoize(value) {
      const ref = React.useRef(value)
      const signalRef = React.useRef(0)
    
      if (!deepEqual(value, ref.current)) {
        ref.current = value
        signalRef.current += 1
      }
    
      // eslint-disable-next-line react-hooks/exhaustive-deps
      return React.useMemo(() => ref.current, [signalRef.current])
    }
    
    React.useEffect(callback, useDeepCompareMemoize(dependencies)
    

참고


© 2023, Customized by Joon