useState 동작원리와 setState가 여러개인 경우

HANITZ·2024년 2월 2일
0

React

목록 보기
6/8
post-thumbnail

React에서 가장 대표적인 Hook인 useState를 디버깅하면서 이해해 보려한다.

간단한 input을 만들어 useState로 상태변화를 주었다.

먼저 mount될 때의 useState부터 보겠다

// react/src/ReactHooks.js
function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function resolveDispatcher() {
  var dispatcher = ReactCurrentDispatcher.current;

  {
    if (dispatcher === null) {
      error('Invalid hook call...');
    }
  } // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.

  return dispatcher;
}

useState는 dispatcher에서 오는데 이 dispatcher는 reconciler에서 주입받아서 사용된다.


 HooksDispatcherOnUpdateInDEV = {
   .
   .
   .
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      mountHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

      try {
        return mountState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
  };

function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  hook.queue = queue;
  var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

useState: function (initialState) {
      currentHookNameInDev = 'useState';
      updateHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;

      try {
        return updateState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    }

function updateState(initialState) {
  return updateReducer(basicStateReducer);
}

function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;

  if (queue === null) {
    throw new Error('Should have a queue. This is likely a bug in React. Please file an issue.');
  }

  queue.lastRenderedReducer = reducer;
  var current = currentHook; 

  var baseQueue = current.baseQueue;
  var pendingQueue = queue.pending;

  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      var baseFirst = baseQueue.next;
      var pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    {
      if (current.baseQueue !== baseQueue) {
        error('Internal error: Expected work-in-progress queue to be a clone. ' + 'This is a bug in React.');
      }
    }

    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    var first = baseQueue.next;
    var newState = current.baseState;
    var newBaseState = null;
    var newBaseQueueFirst = null;
    var newBaseQueueLast = null;
    var update = first;

    do {
      var updateLane = update.lane;

      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        var clone = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: null
        };

        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        currentlyRenderingFiber$1.lanes = mergeLanes(currentlyRenderingFiber$1.lanes, updateLane);
        markSkippedUpdateLanes(updateLane);
      } else {
        if (newBaseQueueLast !== null) {
          var _clone = {
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: null
          };
          newBaseQueueLast = newBaseQueueLast.next = _clone;
        } 


        if (update.hasEagerState) {
          newState = update.eagerState;
        } else {
          var action = update.action;
          newState = reducer(newState, action);
        }
      }

      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    } 
    if (!objectIs(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  } 
  var lastInterleaved = queue.interleaved;

  if (lastInterleaved !== null) {
    var interleaved = lastInterleaved;

    do {
      var interleavedLane = interleaved.lane;
      currentlyRenderingFiber$1.lanes = mergeLanes(currentlyRenderingFiber$1.lanes, interleavedLane);
      markSkippedUpdateLanes(interleavedLane);
      interleaved = interleaved.next;
    } while (interleaved !== lastInterleaved);
  } else if (baseQueue === null) {
    queue.lanes = NoLanes;
  }

  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

초기 mount에서 useState는 mountState를 반환하고 이후로는 updateState를 반환한다.

updateState에서 update연결리스트의 마지막 값을 memoizedState로 반환시킨다.

function dispatchSetState(fiber, queue, action) {
  {
    if (typeof arguments[3] === 'function') {
      error("State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().');
    }
  }

  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    var alternate = fiber.alternate;

    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {

      var lastRenderedReducer = queue.lastRenderedReducer;

      if (lastRenderedReducer !== null) {
        var prevDispatcher;

        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }

        try {
          var currentState = queue.lastRenderedState;
          var eagerState = lastRenderedReducer(currentState, action); 

          update.hasEagerState = true;
          update.eagerState = eagerState;

          if (objectIs(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update, lane);
            return;
          }
        } catch (error) {// Suppress the error. It will throw again in the render phase.
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }

    var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

    if (root !== null) {
      var eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane);
}
  • dispatchSetState는 상태를 변경시켜주면서 컴포넌트를 리렌더링시키는 함수이다.

  • enqueueConcurrentHookUpdate, enqueueRenderPhaseUpdate 함수를 통해 update를 state hook의 queue에 추가시킨다.

  • isRenderPhaseUpdate는 render phase도중에 setState가 발생하는 경우를 고려한 조건이다.

  • if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes) 조건은 렌더링 이전의
    상태와 변경할 상태가 동일한 경우를 고려한 조건이다. 동일한 상태값이면 이전 상태를 그대로 가져오면서 렌더링 시간을 단축시킬 수 있다.

  • scheduleUpdateOnFiber함수를 실행하면서 리랜더링시킨다.

  • 리렌더링 된 컴포넌트는 다시 useState 훅을 불러오면서 상태값을 변경시켜 컴포넌트에 반영한다.


useState가 가진 몇가지 특징들을 직접 디버깅하면서 이해할 수 있었다.

리렌더링

먼저, 리액트의 몇 안되는 리렌더링 조건에 useState가 있어서 어떤 식으로 작동하는건지 궁금했다.

초기렌더링에서도 확인했지만 reconciler에서 scheduler로 update를 넘기면서 렌더링을 유발했는데 dispatchSetState 함수에서 scheduleUpdateOnFiber 함수로 같은 작업이 있음을 확인했다.

setState

한 작업 중에 setState 함수를 여러번 발생시켜도 리렌더링은 한번만 발생한다는 점이 신기해서 직접 여러번 setState를 발생시켜 봤다.

input 값의 변화에 3개의 setState를 한번에 주었다.

"a"라는 input을 추가하면서 3개의 update로 이루어진 원형 연결리스트인 queue가 만들어졌다.

마지막 update의 next에 첫 update를 연결하면서 첫번째 update부터 탐색할 수 있도록 설정되어 있다.

while문을 반복하면서 update의 상태값을 계속 현재 상태와 비교하며 업데이트 해준다.

마지막 값에 도달하면 while문을 벗어나고 memoizedState에 변경된 값이 들어가고 렌더링이 진행된다.

결국 setState가 여러번 발생하더라도 update들은 queue에 쌓이게되고 모든 setState가 실행되고 새롭게 렌더링 될때 updateReducer에서 한번에 처리가 되는 것으로 한번만 리렌더링 되는 것을 알 수 있었다.

0개의 댓글

관련 채용 정보