BlogReactReconcilerFiber Hooks

React-reconciler | Fiber Hooks (19.0.0)

React v19 Hooks에서 소개되는 Hook에 대해 알아봅니다.

ReactFiberHooks 공통 로직

Hook

react/packages/react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

memoizedState

  • 메모이제이션된 상태를 저장합니다.

baseState

  • 초기 상태를 저장합니다.

baseQueue

  • 렌더링 중단 또는 연기된 UpdateUpdateQueue에 저장하며, 이후 렌더링에서 다시 사용됩니다.

queue

  • 업데이트를 진행해야 할 Update linked list입니다.

next

  • linked list에서 다음 Hook 객체를 가리킵니다.

Update

react/packages/react-reconciler/src/ReactFiberHooks.js
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

lane

  • 처리될 우선순위를 나타냅니다.

revertLane

  • 업데이트를 무효화합니다.
  • 비동기적인 상태 업데이트 시 사용됩니다.

action

  • dispatch에 전달될 action입니다.

hasEagerState

  • 미리 계산된 상태 값이 있는지 여부입니다.

eagerState

  • 미리 계산된 상태 값입니다.

next

  • linked list에서 다음 Update 객체를 가리킵니다.

UpdateQueue

react/packages/react-reconciler/src/ReactFiberHooks.js
export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

pending

  • 대기 중인 업데이트입니다.

lanes

  • 모든 업데이트의 우선순위를 나타냅니다.

dispatch

  • 상태 업데이트를 트리거하는 메소드입니다.

lastRenderedReducer

  • 가장 마지막으로 렌더링에 사용된 리듀서 함수입니다.

lastRenderedState

  • 가장 마지막으로 렌더링에 사용된 상태 값입니다.

mountWorkInProgressHook

react/packages/react-reconciler/src/ReactFiberHooks.js
let currentlyRenderingFiber: Fiber = (null: any);
let workInProgressHook: Hook | null = null;
 
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
 
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
  1. mount 단계에서 Hook 객체를 생성합니다.
  2. workInProgressHooknull인지 체크하고, linked list 혹은 memoizedState에 할당합니다.
  3. 현재 작업 중인 Hook을 반환합니다.

updateWorkInProgressHook

react/packages/react-reconciler/src/ReactFiberHooks.js
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
 
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
 
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
 
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
 
    currentHook = nextCurrentHook;
  } else {
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
      } else {
        throw new Error('Rendered more hooks than during the previous render.');
      }
    }
 
    currentHook = nextCurrentHook;
 
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
 
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}
  1. nextCurrentHook: 이전 렌더에서 사용된 Hook을 찾습니다.
  2. nextWorkInProgressHook: workInProgressHook이 없으면 이전 렌더 결과에서 Hook을 찾고 있다면 workInProgressHook 연결된 next를 할당합니다.
  3. nextWorkInProgressHooknull이 아니면 workInProgressHook, currentHook에 값을 할당합니다.
  4. nextWorkInProgressHooknull이면 currentHook에 값을 할당하고 이전 렌더 결과(currentHook)를 바탕을 Hook을 생성합니다.
  5. workInProgressHooknull인지 체크하고, linked list 혹은 memoizedState에 할당합니다.
  6. workInProgressHook을 반환합니다.

areHookInputsEqual

react/packages/react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  if (prevDeps === null) {
    return false;
  }
 
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

이전 의존성 배열(prevDeps)과 새로운 의존성 배열(nextDeps)을 비교하여 동일한지 여부를 반환합니다.

Dispatcher

react/packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: mountActionState,
  useActionState: mountActionState,
  useOptimistic: mountOptimistic,
  useMemoCache,
  useCacheRefresh: mountRefresh,
};
 
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: updateActionState,
  useActionState: updateActionState,
  useOptimistic: updateOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};
 
const HooksDispatcherOnRerender: Dispatcher = {
  readContext,
  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  useDebugValue: updateDebugValue,
  useDeferredValue: rerenderDeferredValue,
  useTransition: rerenderTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: rerenderActionState,
  useActionState: rerenderActionState,
  useOptimistic: rerenderOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};

다음과 같이 mount, update, rerender 상황 별로 정의된 Hook들을 알아봅니다.

use

ℹ️

usePromisecontext와 같은 데이터를 참조합니다. 하위 컴포넌트에서 Promise 객체를 받아 use 훅을 통해 resolve하거나 Suspense의 fallback UI를 노출시킵니다. useContext를 대체하기도 합니다.

react/packages/react-reconciler/src/ReactFiberThenable.js
let suspendedThenable: Thenable<any> | null = null;
 
...
 
export function trackUsedThenable<T>(
  thenableState: ThenableState,
  thenable: Thenable<T>,
  index: number,
): T {
  const trackedThenables = getThenablesFromState(thenableState); // prod에서는 thenableState 그대로 반환
  const previous = trackedThenables[index];
  if (previous === undefined) {
    trackedThenables.push(thenable);
  } else {
    if (previous !== thenable) {
      thenable.then(noop, noop);
      thenable = previous;
    }
  }
 
  switch (thenable.status) {
    case 'fulfilled': {
      const fulfilledValue: T = thenable.value;
      return fulfilledValue;
    }
    case 'rejected': {
      const rejectedError = thenable.reason;
      checkIfUseWrappedInAsyncCatch(rejectedError);
      throw rejectedError;
    }
    default: {
      if (typeof thenable.status === 'string') {
        thenable.then(noop, noop);
      } else {
        const root = getWorkInProgressRoot();
        if (root !== null && root.shellSuspendCounter > 100) {
          throw new Error(
            'async/await is not yet supported in Client Components, only ' +
              'Server Components. This error is often caused by accidentally ' +
              "adding `'use client'` to a module that was originally written " +
              'for the server.',
          );
        }
 
        const pendingThenable: PendingThenable<T> = (thenable: any);
        pendingThenable.status = 'pending';
        pendingThenable.then(
          fulfilledValue => {
            if (thenable.status === 'pending') {
              const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
              fulfilledThenable.status = 'fulfilled';
              fulfilledThenable.value = fulfilledValue;
            }
          },
          (error: mixed) => {
            if (thenable.status === 'pending') {
              const rejectedThenable: RejectedThenable<T> = (thenable: any);
              rejectedThenable.status = 'rejected';
              rejectedThenable.reason = error;
            }
          },
        );
      }
 
      switch ((thenable: Thenable<T>).status) {
        case 'fulfilled': {
          const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
          return fulfilledThenable.value;
        }
        case 'rejected': {
          const rejectedThenable: RejectedThenable<T> = (thenable: any);
          const rejectedError = rejectedThenable.reason;
          checkIfUseWrappedInAsyncCatch(rejectedError);
          throw rejectedError;
        }
      }
 
      suspendedThenable = thenable;
      throw SuspenseException;
    }
  }
}
  1. thenable = previous; 이전에 처리되었다면 해당 내용을 재사용합니다.
  2. thenable 상태 처리를 진행합니다. fulfilled 상태라면 resolve된 value를 반환하고 rejected 상태라면 에러를 반환합니다. pending 상태라면 then 메소드에 성공, 실패 callback을 부여합니다.
  3. pending 상태를 한번 더 확인합니다.
  4. SuspenseException를 던집니다.
react/packages/react-reconciler/src/ReactFiberHooks.js
let currentlyRenderingFiber: Fiber = (null: any);
let workInProgressHook: Hook | null = null;
let thenableIndexCounter: number = 0;
let thenableState: ThenableState | null = null; // Array<Thenable<any>>
 
...
 
function useThenable<T>(thenable: Thenable<T>): T {
  const index = thenableIndexCounter;
  thenableIndexCounter += 1;
  if (thenableState === null) {
    thenableState = createThenableState();
  }
  const result = trackUsedThenable(thenableState, thenable, index);
 
  const workInProgressFiber = currentlyRenderingFiber;
  const nextWorkInProgressHook =
    workInProgressHook === null
      ? workInProgressFiber.memoizedState
      : workInProgressHook.next;
 
  if (nextWorkInProgressHook !== null) {
  } else {
    const currentFiber = workInProgressFiber.alternate;
 
    ReactSharedInternals.H =
      currentFiber === null || currentFiber.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
    
  }
  return result;
}
 
function use<T>(usable: Usable<T>): T {
  if (usable !== null && typeof usable === 'object') {
    if (typeof usable.then === 'function') {
      const thenable: Thenable<T> = (usable: any);
      return useThenable(thenable);
    } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
      const context: ReactContext<T> = (usable: any);
      return readContext(context);
    }
  }
 
  throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
 
...
  1. 각 호출마다 thenableIndexCounter를 사용하여 고유한 인덱스를 만듭니다.
  2. thenableStatenull일 경우 (상태를 저장할) 빈 배열을 생성합니다.
  3. trackUsedThenable를 통해 비동기 작업의 상태를 추적하고 result를 반환합니다.
  4. nextWorkInProgressHook 남아있는 Hook이 존재하는지 확인합니다.
  5. 남아있는 Hook이 없다면 currentFiber 상태에 따라 처음 마운트인지, 업데이트인지 체크하고 Dispatcher를 결정합니다.
  6. use 메소드에서 thenable(Promise)의 결과를 반환합니다. (context 사용 결과를 반환할 수도 있습니다.)

useContext

ℹ️

useContextcreateContext로 생성된 Context를 읽고 구독하는 데 사용합니다.

react/packages/react-reconciler/src/ReactFiberNewContext.js
let lastContextDependency: ContextDependency<mixed> | null = null;
 
...
 
export function readContext<T>(context: ReactContext<T>): T {
  return readContextForConsumer(currentlyRenderingFiber, context);
}
 
function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;
 
  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    memoizedValue: value,
    next: null,
  };
 
  if (lastContextDependency === null) {
    if (consumer === null) {
      throw new Error(
        'Context can only be read while React is rendering. ' +
          'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
          'In function components, you can read it directly in the function body, but not ' +
          'inside Hooks like useReducer() or useMemo().',
      );
    }
 
    lastContextDependency = contextItem;
    consumer.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
    };
    consumer.flags |= NeedsPropagation;
  } else {
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return value;
}

lastContextDependency

  • 이전 Fiber를 렌더링할 때 의존된 context입니다.

isPrimaryRenderer

  • ReactDOM을 사용하는지 여부를 나타내는 플래그입니다. Legacy SSR, React Native, test 환경에서는 false일 수 있습니다.

readContextForConsumer

  • lastContextDependency가 없다면 현재 렌더링 중인 Fiber(consumer)에 대해 dependencies를 생성합니다.
  • lastContextDependency가 있다면 linked list에 contextItem을 추가합니다.

useCallback

ℹ️

useCallback은 함수 정의를 캐시하는데 사용합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
 
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

mountCallback

  • mount 시 memoizedState에 캐시할 함수와 새로운 의존성 배열(nextDeps)을 저장합니다.

updateCallback

  • update할 때 memoizedState에 저장된 이전 의존성 배열(prevDeps)과 비교하여 같으면 이전 상태를 return하고 같지 않으면 새로 저장합니다.

useMemo

ℹ️

useMemo는 렌더링 간 계산 결과를 캐시하는 데 사용됩니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
 
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
 
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
 
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

mountMemo

  • mount 시 memoizedState에 캐시할 nextValue와 새로운 의존성 배열(nextDeps)을 저장합니다.

updateMemo

  • update할 때 memoizedState에 저장된 이전 의존성 배열(prevDeps)과 비교하여 같으면 이전 상태를 return하고 같지 않으면 새로 저장합니다.

useRef

ℹ️

useRef는 렌더링에 필요하지 않은 값을 참조할 수 있습니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
 
function updateRef<T>(initialValue: T): {current: T} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

mountRef

  • mount 시 memoizedState에 초기 값을 할당합니다.

updateRef

  • update할 때 memoizedState에 저장된 값을 반환합니다.

useId

ℹ️

useId는 접근성 속성에 전달할 수 있는 고유한 ID를 생성합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
let localIdCounter: number = 0;
let globalClientIdCounter: number = 0;
...
 
function mountId(): string {
  const hook = mountWorkInProgressHook();
 
  const root = ((getWorkInProgressRoot(): any): FiberRoot);
 
  const identifierPrefix = root.identifierPrefix;
 
  let id;
  if (getIsHydrating()) {
    const treeId = getTreeId();
 
    id = ':' + identifierPrefix + 'R' + treeId;
 
    const localId = localIdCounter++;
    if (localId > 0) {
      id += 'H' + localId.toString(32);
    }
 
    id += ':';
  } else {
    const globalClientId = globalClientIdCounter++;
    id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
  }
 
  hook.memoizedState = id;
  return id;
}
 
function updateId(): string {
  const hook = updateWorkInProgressHook();
  const id: string = hook.memoizedState;
  return id;
}

identifierPrefix

  • createRoot, hydrateRoot, renderToPipeableStream에서 옵션으로 넘길 수 있으며 useId에서 생성되는 ID에 접두어로 사용됩니다. 한 페이지에 여러 React App이 있을 경우 주로 사용됩니다.

mountId

  • getIsHydrating()(Server Side)일 때 대문자 R로 시작하고 트리 ID를 포함하며 컴포넌트에 여러 useId가 있는 경우 H + 순차번호를 추가합니다.
  • getIsHydrating()(Client Side)이 아닐 때 소문자 r로 시작하며 전역 카운터를 32진수로 변환한 값을 사용합니다.

updateId

  • mountId에서 생성된 id를 반환합니다.

useReducer

ℹ️

useReducer는 state를 action 기반으로 변형 할 수 있게 합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
let currentlyRenderingFiber: Fiber = (null: any);
 
function isRenderPhaseUpdate(fiber: Fiber): boolean {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}
 
function dispatchReducerAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const lane = requestUpdateLane(fiber);
 
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };
 
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      startUpdateTimerByLane(lane);
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}
 
function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
 
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
 
  hook.memoizedState = hook.baseState = initialState;
  
  const queue: UpdateQueue<S, A> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  };
 
  hook.queue = queue;
 
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
 
  return [hook.memoizedState, dispatch];
}
 
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
 
function updateReducerImpl<S, A>(
  hook: Hook,
  current: Hook,
  reducer: (S, A) => S,
): [S, Dispatch<A>] {
  const queue = hook.queue;
 
  if (queue === null) {
    throw new Error(
      'Should have a queue. You are likely calling Hooks conditionally, ' +
        'which is not allowed. (https://react.dev/link/invalid-hook-call)',
    );
  }
 
  queue.lastRenderedReducer = reducer;
 
  let baseQueue = hook.baseQueue;
 
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
 
  const baseState = hook.baseState;
  if (baseQueue === null) {
    hook.memoizedState = baseState;
  } else {
    const first = baseQueue.next;
    let newState = baseState;
 
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast: Update<S, A> | null = null;
    let update = first;
    let didReadFromEntangledAsyncAction = false;
 
    do {
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
 
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);
 
      if (shouldSkipUpdate) {
        const clone: Update<S, A> = {
          lane: updateLane,
          revertLane: update.revertLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        const revertLane = update.revertLane;
        if (revertLane === NoLane) {
          if (newBaseQueueLast !== null) {
            const clone: Update<S, A> = {
              lane: NoLane,
              revertLane: NoLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }
          if (updateLane === peekEntangledActionLane()) {
            didReadFromEntangledAsyncAction = true;
          }
        } else {
          if (isSubsetOfLanes(renderLanes, revertLane)) {
            update = update.next;
 
            if (revertLane === peekEntangledActionLane()) {
              didReadFromEntangledAsyncAction = true;
            }
            continue;
          } else {
            const clone: Update<S, A> = {
              lane: NoLane,
              revertLane: update.revertLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            if (newBaseQueueLast === null) {
              newBaseQueueFirst = newBaseQueueLast = clone;
              newBaseState = newState;
            } else {
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
 
            currentlyRenderingFiber.lanes = mergeLanes(
              currentlyRenderingFiber.lanes,
              revertLane,
            );
            markSkippedUpdateLanes(revertLane);
          }
        }
 
        const action = update.action;
        if (update.hasEagerState) {
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);
 
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }
 
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
 
      if (didReadFromEntangledAsyncAction) {
        const entangledActionThenable = peekEntangledActionThenable();
        if (entangledActionThenable !== null) {
          throw entangledActionThenable;
        }
      }
    }
 
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
 
    queue.lastRenderedState = newState;
  }
 
  if (baseQueue === null) {
    queue.lanes = NoLanes;
  }
 
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
 
function rerenderReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
 
  if (queue === null) {
    throw new Error(
      'Should have a queue. You are likely calling Hooks conditionally, ' +
        'which is not allowed. (https://react.dev/link/invalid-hook-call)',
    );
  }
 
  queue.lastRenderedReducer = reducer;
 
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  const lastRenderPhaseUpdate = queue.pending;
  let newState = hook.memoizedState;
  if (lastRenderPhaseUpdate !== null) {
    queue.pending = null;
 
    const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;
    let update = firstRenderPhaseUpdate;
    do {
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== firstRenderPhaseUpdate);
 
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
 
    hook.memoizedState = newState;
 
    if (hook.baseQueue === null) {
      hook.baseState = newState;
    }
 
    queue.lastRenderedState = newState;
  }
  return [newState, dispatch];
}

isRenderPhaseUpdate

  • 현재 fiber 혹은 fiber.alternate가 작업 중인 fiber와 같다면 render phase로 true를 반환합니다.

dispatchReducerAction

  • useReducerdispatch 함수 역할을 합니다.
  • action이 발생할 때마다 새로운 update 객체를 생성합니다.
  • render phase면 queue.pending에 추가합니다.
  • render phase가 아니면 concurrent 업데이트로 처리하고 fiber를 스케줄링합니다.

mountReducer

  • init 초기화 함수가 있다면 실행하고 아니면 initialArg 초기 값을 지정합니다.
  • queue, dispatch 함수를 선언하고 반환합니다.

updateReducer

  • hook.queue.pending가 있다면 hook.baseQueue에 병합시킵니다.
  • hook.baseQueue 없으면 현재 상태 baseStatememoizedState에 할당하고 반환합니다.
  • hook.baseQueue를 순회하며 update를 skip할지 결정(OffscreenLane)하고 비동기 작업을 예약하며 eagerState, reducer를 사용하여 업데이트합니다.
  • 예약된 비동기 작업을 처리하고 반환합니다.
  • useState의 update 동작에서 재활용됩니다.
  • 이전 작업의 baseState, hook.queue 기반으로 작업이 되기 때문에 아래와 같은 로직에서 count는 2가 됩니다.
// 컴포넌트
function MyButton () {
  const [count, setCount] = useState(0);
 
  function handleClick () {
    setCount(prev => prev += 1); // 1번 작업이 hook.queue.next에 할당
    setCount(prev => prev += 2); // 2번 작업이 hook.queue.next.next에 할당
  }
 
  return (
    <button  onClick={handleClick} />
  );
}
 
...
 
// reconciler
function updateReducerImpl<S, A>(
  hook: Hook,
  current: Hook,
  reducer: (S, A) => S,
): [S, Dispatch<A>] {
 
  let baseQueue = hook.baseQueue;
  const baseState = hook.baseState;
  const first = baseQueue.next;
  ...
  let newState = baseState;
  let newBaseState = null;
  let update = first;
do {
  ... // queue를 비워냄. 마지막엔 2번 작업이 실행, newBaseState += 2
  update = update.next;
} while (update !== null && update !== first);
  ...
  hook.baseState = newBaseState;
  ...
}

rerenderReducer

  • dispatch 메소드에서 render phase라면 queue.pendingupdate가 추가됩니다.
  • 현재 작업 중인 hookqueue.pending이 있다면 작업을 모두 실행시킵니다.
  • useState의 rerender 동작에서 재활용됩니다.

useState

ℹ️

useState는 상태와 상태를 변경할 수 있는 dispatch 함수를 제공합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
 
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const lane = requestUpdateLane(fiber);
  const didScheduleUpdate = dispatchSetStateInternal(
    fiber,
    queue,
    action,
    lane,
  );
  if (didScheduleUpdate) {
    startUpdateTimerByLane(lane);
  }
}
 
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };
 
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
 
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
 
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
 
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false;
          }
        } catch (error) {
        } finally {
        }
      }
    }
 
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}
 
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    initialState = initialStateInitializer();
  }
  
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}
 
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}
 
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}
 
function rerenderState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return rerenderReducer(basicStateReducer, initialState);
}

basicStateReducer

  • state와 action을 받아 새로운 상태를 반환합니다.

dispatchSetStateInternal

  • useStatesetState 역할을 합니다.
  • Update를 선언합니다.
  • render phase면 queue.pending에 추가합니다.
  • eagerState 상태를 미리 계산하고, currentStateObject.is로 비교합니다.
  • 변경되었다면 ConcurrentQueue에 추가하고 스케줄링합니다.
  • useTransitionstartTransition에서 재사용됩니다.

mountStateImpl

  • initialState가 함수일 경우 실행한 후 값을 hook.memoizedState, hook.baseState에 할당합니다.
  • queue를 선언하고 hook.queue에 할당합니다.
  • useTransitionmountTransition에서 재사용됩니다.

mountState

  • hook, dispatch(setState)를 생성하고 반환합니다.

updateState

  • basicStateReducer를 reducer로 사용하여 useReducer에서 update 시 사용되는 updateReducer를 호출합니다.
  • useTransitionupdateTransition에서 재사용됩니다.

rerenderState

  • basicStateReducer를 reducer로 사용하여 useReducer에서 rerender 시 사용되는 rerenderReducer를 호출합니다.
  • useTransitionrerenderTransition에서 재사용됩니다.

useTransition

ℹ️

useTransition은 UI를 차단하지 않고 상태를 업데이트할 수 있습니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function dispatchOptimisticSetState<S, A>(
  fiber: Fiber,
  throwIfDuringRender: boolean,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const transition = requestCurrentTransition();
 
  const update: Update<S, A> = {
    lane: SyncLane,
    revertLane: requestTransitionLane(transition),
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };
 
  if (isRenderPhaseUpdate(fiber)) {
    if (throwIfDuringRender) {
      throw new Error('Cannot update optimistic state while rendering.');
    }
  } else {
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
    if (root !== null) {
      startUpdateTimerByLane(SyncLane);
      scheduleUpdateOnFiber(root, fiber, SyncLane);
    }
  }
}
 
function startTransition<S>(
  fiber: Fiber,
  queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
  pendingState: S,
  finishedState: S,
  callback: () => mixed,
  options?: StartTransitionOptions,
): void {
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority),
  );
 
  const prevTransition = ReactSharedInternals.T;
  const currentTransition: BatchConfigTransition = {};
 
  ReactSharedInternals.T = currentTransition;
  dispatchOptimisticSetState(fiber, false, queue, pendingState);
 
  try {
    const returnValue = callback();
    const onStartTransitionFinish = ReactSharedInternals.S;
    if (onStartTransitionFinish !== null) {
      onStartTransitionFinish(currentTransition, returnValue);
    }
 
    if (
      returnValue !== null &&
      typeof returnValue === 'object' &&
      typeof returnValue.then === 'function'
    ) {
      const thenable = ((returnValue: any): Thenable<mixed>);
 
      const thenableForFinishedState = chainThenableValue(
        thenable,
        finishedState,
      );
      dispatchSetStateInternal(
        fiber,
        queue,
        (thenableForFinishedState: any),
        requestUpdateLane(fiber),
      );
    } else {
      dispatchSetStateInternal(
        fiber,
        queue,
        finishedState,
        requestUpdateLane(fiber),
      );
    }
  } catch (error) {
    const rejectedThenable: RejectedThenable<S> = {
      then() {},
      status: 'rejected',
      reason: error,
    };
    dispatchSetStateInternal(
      fiber,
      queue,
      rejectedThenable,
      requestUpdateLane(fiber),
    );
  } finally {
    setCurrentUpdatePriority(previousPriority);
 
    ReactSharedInternals.T = prevTransition;
  }
}
 
function mountTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
 
  const start = startTransition.bind(
    null,
    currentlyRenderingFiber,
    stateHook.queue,
    true,
    false,
  );
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [false, start];
}
 
function updateTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [booleanOrThenable] = updateState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  const isPending =
    typeof booleanOrThenable === 'boolean'
      ? booleanOrThenable
      : useThenable(booleanOrThenable);
  return [isPending, start];
}
 
function rerenderTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [booleanOrThenable] = rerenderState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  const isPending =
    typeof booleanOrThenable === 'boolean'
      ? booleanOrThenable
      : useThenable(booleanOrThenable);
  return [isPending, start];
}

dispatchOptimisticSetState

  • UI를 차단하지 않고 동기적으로 실행되어야 하기 때문에 SyncLane 우선순위로 Update를 생성합니다.
  • 렌더링 중이지 않으면 작업을 스케줄링합니다.
  • SyncLane은 동기적으로 실행되고 상태를 변경하기 때문에 리렌더링일 때 작업을 진행하지 않습니다.

startTransition

  • 우선순위 및 전환 상태를 할당합니다.
  • dispatchOptimisticSetState 메소드로 pending=true;로 변경합니다.
  • const returnValue = callback(); 실행 결과를 할당합니다.
  • 결과가 thenable일 경우 비동기적으로 상태를 업데이트합니다.
  • 결과가 thenable이 아니면 즉시 상태를 pending=false; 업데이트합니다.
  • 에러 발생 시, 거부된 thenable로 상태를 업데이트합니다.
  • 우선순위 및 전환 상태를 복구합니다.

mountTransition

  • hook을 생성하고 pending=false;startTransition을 반환합니다.

updateTransition

  • 현재 상태(pending, startTransition)를 가져오고 반환합니다.

rerenderTransition

  • 현재 상태(pending, startTransition)를 가져오고 반환합니다.

useDeferredValue

ℹ️

useDeferredValue는 UI 일부 업데이트를 지연시킬 수 있습니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
  if (
    initialValue !== undefined &&
    !includesSomeLane(renderLanes, DeferredLane)
  ) {
    hook.memoizedState = initialValue;
 
    const deferredLane = requestDeferredLane();
    currentlyRenderingFiber.lanes = mergeLanes(
      currentlyRenderingFiber.lanes,
      deferredLane,
    );
    markSkippedUpdateLanes(deferredLane);
 
    return initialValue;
  } else {
    hook.memoizedState = value;
    return value;
  }
}
 
function updateDeferredValueImpl<T>(
  hook: Hook,
  prevValue: T,
  value: T,
  initialValue?: T,
): T {
  if (is(value, prevValue)) {
    return value;
  } else {
    if (isCurrentTreeHidden()) {
      const resultValue = mountDeferredValueImpl(hook, value, initialValue);
 
      if (!is(resultValue, prevValue)) {
        markWorkInProgressReceivedUpdate();
      }
 
      return resultValue;
    }
 
    const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
    if (shouldDeferValue) {
      const deferredLane = requestDeferredLane();
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        deferredLane,
      );
      markSkippedUpdateLanes(deferredLane);
 
      return prevValue;
    } else {
      markWorkInProgressReceivedUpdate();
      hook.memoizedState = value;
      return value;
    }
  }
}
 
function mountDeferredValue<T>(value: T, initialValue?: T): T {
  const hook = mountWorkInProgressHook();
  return mountDeferredValueImpl(hook, value, initialValue);
}
 
function updateDeferredValue<T>(value: T, initialValue?: T): T {
  const hook = updateWorkInProgressHook();
  const resolvedCurrentHook: Hook = (currentHook: any);
  const prevValue: T = resolvedCurrentHook.memoizedState;
  return updateDeferredValueImpl(hook, prevValue, value, initialValue);
}
 
function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
  const hook = updateWorkInProgressHook();
  if (currentHook === null) {
    return mountDeferredValueImpl(hook, value, initialValue);
  } else {
    const prevValue: T = currentHook.memoizedState;
    return updateDeferredValueImpl(hook, prevValue, value, initialValue);
  }
}

mountDeferredValueImpl

  • initialValue가 주어지고 현재 렌더링이 지연되지 않았다면 초기 값을 상태로 설정합니다.
  • DeferredLane 우선 순위를 변경하고 현재 컴포넌트의 렌더링 우선순위를 조정합니다.
  • 그렇지 않으면 valuememoizedState에 할당합니다.

updateDeferredValueImpl

  • valueprevValueObject.is로 비교하여 동일하면 그대로 반환합니다.
  • isCurrentTreeHiddentrue라면, 다시 초기화(mountDeferredValueImpl)합니다.
  • 현재 렌더링의 우선순위가 응급하다면 (SyncLane, InputContinuousLane, DefaultLane) 값 지연 처리를 설정하고 이전 값을 유지합니다.
  • 현재 렌더링의 우선순위가 응급하지 않다면 상태를 업데이트하고 새로운 값을 반환합니다.

mountDeferredValue

  • Hook을 생성하고 mountDeferredValueImpl를 호출하여 초기 값을 반환합니다.

updateDeferredValue

  • prevValue (currentHook.memoizedState)를 updateDeferredValueImpl의 인자로 넘긴 값을 반환합니다.

rerenderDeferredValue

  • currentHook 존재 여부에 따라 mountDeferredValueImpl, updateDeferredValueImpl를 호출하고 반환합니다.

useEffect

ℹ️

useEffect는 외부 시스템과 컴포넌트를 동기화하는 데 사용됩니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
type EffectInstance = {
  resource: mixed,
  destroy: void | (() => void) | ((resource: mixed) => void),
};
 
export const ResourceEffectIdentityKind: 0 = 0;
export const ResourceEffectUpdateKind: 1 = 1;
export type EffectKind = typeof ResourceEffectIdentityKind | typeof ResourceEffectUpdateKind;
 
export type Effect = SimpleEffect | ResourceEffectIdentity | ResourceEffectUpdate;
 
export type SimpleEffect = {
  tag: HookFlags,
  inst: EffectInstance,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
  next: Effect,
};
 
...
 
export type FunctionComponentUpdateQueue = {
  lastEffect: Effect | null,
  events: Array<EventFunctionPayload<any, any, any>> | null,
  stores: Array<StoreConsistencyCheck<any>> | null,
  memoCache: MemoCache | null,
};
 
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
  return {
    lastEffect: null,
    events: null,
    stores: null,
    memoCache: null,
  };
}
 
function pushEffectImpl(effect: Effect): Effect {
  let componentUpdateQueue: null | FunctionComponentUpdateQueue =
    (currentlyRenderingFiber.updateQueue: any);
 
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
  }
 
  const lastEffect = componentUpdateQueue.lastEffect;
  if (lastEffect === null) {
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect;
  }
  return effect;
}
 
function pushSimpleEffect(
  tag: HookFlags,
  inst: EffectInstance,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): Effect {
  const effect: Effect = {
    tag,
    create,
    deps,
    inst,
    next: (null: any),
  };
  return pushEffectImpl(effect);
}
 
function createEffectInstance(): EffectInstance {
  return {destroy: undefined, resource: undefined};
}
 
function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags,
    createEffectInstance(),
    create,
    nextDeps,
  );
}
 
function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState;
  const inst = effect.inst;
 
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
 
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushSimpleEffect(
          hookFlags,
          inst,
          create,
          nextDeps,
        );
        return;
      }
    }
  }
 
  currentlyRenderingFiber.flags |= fiberFlags;
 
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags,
    inst,
    create,
    nextDeps,
  );
}
 
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  mountEffectImpl(
    PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}
 
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

EffectInstance

  • resource (DOM 참조, data fetching 등), destroy(clean-up function)을 관리합니다.

createEffectInstance

  • EffectInstance 객체를 생성합니다.

SimpleEffect

  • tag: HookEffectTags 값을 갖습니다.
  • inst: EffectInstance 값을 갖는 객체입니다.
  • create: (useEffect 1번째 매개변수) set-up, clean-up 함수를 나타냅니다.
  • deps: (useEffect 2번째 매개변수) 의존성들을 나타냅니다.
  • next: 다음 effect를 가르킵니다.

createFunctionComponentUpdateQueue

  • Fiber(컴포넌트)의 updateQueue가 비어있을 때 새롭게 생성합니다.

pushEffectImpl

  • currentlyRenderingFiber(컴포넌트)의 updateQueue를 가져오고 updateQueue.lastEffect의 linked list (next)에 삽입합니다.

mountEffectImpl

  • mount 시 createEffectInstance에 의해 생성된 새로운 EffectInstanceupdateQueue.lastEffect에 삽입합니다.

updateEffectImpl

  • update 시 nextDeps, prevDeps를 비교하여 동일하다면 HookHasEffect HookFlag를 제거하여 linked list에 추가하되 create 함수가 동작하지 않도록 하고, 동일하지 않다면 HookHasEffect HookFlag를 포함하여 linked list에 추가하여 create 함수가 동작하도록 합니다.
react/packages/react-reconciler/src/ReactFiberCommitWorks.js
function commitPassiveMountOnFiber(
  finishedRoot: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
  endTime: number,
): void {
  ...
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraversePassiveMountEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
        committedTransitions,
        endTime,
      );
      if (flags & Passive) {
        commitHookPassiveMountEffects(
          finishedWork,
          HookPassive | HookHasEffect,
        );
      }
      break;
    }
    ...
  }
  ...
}
react/packages/react-reconciler/src/ReactFiberCommitEffects.js
export function commitHookPassiveMountEffects(
  finishedWork: Fiber,
  hookFlags: HookFlags,
) {  
  commitHookEffectListMount(hookFlags, finishedWork);
}
 
export function commitHookEffectListMount(
  flags: HookFlags,
  finishedWork: Fiber,
) {
  try {
    const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
    const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
      const firstEffect = lastEffect.next;
      let effect = firstEffect;
      do {
        if ((effect.tag & flags) === flags) {
          ...
        }
        effect = effect.next;
      } while (effect !== firstEffect);
    }
  }
  ...
}

effect.tagHasEffect | Passive를 갖고 있지 않다면 실행되지 않습니다.

HookEffectTags를 참조하세요.

useImperativeHandle

ℹ️

useImperativeHandle은 Parent Component의 ref를 통해 Child Component의 ref를 조작할 수 있게 합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function imperativeHandleEffect<T>(
  create: () => T,
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
): void | (() => void) {
  if (typeof ref === 'function') {
    const refCallback = ref;
    const inst = create();
    const refCleanup = refCallback(inst);
 
    return () => {
      if (typeof refCleanup === 'function') {
        refCleanup();
      } else {
        refCallback(null);
      }
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
 
    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}
 
function mountImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null;
 
  let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
 
  mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}
 
function updateImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null;
 
  updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

imperativeHandleEffect

  • 함수인 경우 create() 함수를 호출하여 생성된 인스턴스를 refCallback에 전달합니다. 만약 refCallback이 함수를 반환하면 이를 호출하거나, 반환되지 않으면 refCallback(null)을 호출하여 정리합니다.
  • 객체인 경우 ref.currentcreate() 함수를 호출하여 생성된 인스턴스를 할당하고 refObject.current = null; clean-up 함수를 반환합니다.

mountImperativeHandle

  • 의존성이 있다면 ref를 의존성에 추가하고 Layout HookFlag로 mountEffectImpl를 호출합니다.

updateImperativeHandle

  • 의존성이 있다면 ref를 의존성에 추가하고 Layout HookFlag로 updateEffectImpl를 호출합니다.
⚠️

useImperativeHandle이 useLayoutEffect와 동일한 시점에 실행되어야 하는 이유
(Layout HookFlag 사용 이유)

useImperativeHandleuseEffect와 동일한 시점에 실행된다면 Parent Component의 useEffect 내부에서 parent ref로 child ref를 조작할 수 없습니다. 그러므로 useImperativeHandleuseLayoutEffect와 동일한 시점에 child ref를 참조하고 조작할 수 있음을 보장해야 합니다.

HookEffectTags를 참조하세요.

useLayoutEffect

ℹ️

useLayoutEffect은 브라우저가 화면을 그리기 전에 실행되는 useEffect입니다. (성능을 저하시킬 수 있으므로 가능하면 useEffect를 사용하는 것이 권장됩니다.)

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
 
function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
  • useEffect와 내부 구현은 같지만 HookFlag가 Layout으로 변경되어 paint 이전에 create함수가 실행됩니다.

HookEffectTags를 참조하세요.

useInsertionEffect

ℹ️

useInsertionEffect은 useLayoutEffect가 실행되기 전에 DOM에 요소를 삽입합니다. (CSS-in-JS Library 용도)

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
 
function updateInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
  • useEffect와 내부 구현은 같지만 HookFlag가 Insertion으로 변경되어 useLayoutEffect 실행 시점 이전에 create함수가 실행됩니다.

HookEffectTags를 참조하세요.

useSyncExternalStore

ℹ️

useSyncExternalStore는 외부 store를 구독할 수 있게 합니다.

react/packages/react-reconciler/src/ReactFiberFlags.js
function mountSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  const fiber = currentlyRenderingFiber;
  const hook = mountWorkInProgressHook();
 
  let nextSnapshot;
  const isHydrating = getIsHydrating();
  if (isHydrating) {
    if (getServerSnapshot === undefined) {
      throw new Error(
        'Missing getServerSnapshot, which is required for ' +
          'server-rendered content. Will revert to client rendering.',
      );
    }
    nextSnapshot = getServerSnapshot();
  } else {
    nextSnapshot = getSnapshot();
 
    const root: FiberRoot | null = getWorkInProgressRoot();
 
    if (root === null) {
      throw new Error(
        'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
      );
    }
 
    const rootRenderLanes = getWorkInProgressRootRenderLanes();
    if (!includesBlockingLane(rootRenderLanes)) {
      pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
    }
  }
 
  hook.memoizedState = nextSnapshot;
  const inst: StoreInstance<T> = {
    value: nextSnapshot,
    getSnapshot,
  };
  hook.queue = inst;
 
  mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
 
  fiber.flags |= PassiveEffect;
  pushSimpleEffect(
    HookHasEffect | HookPassive,
    createEffectInstance(),
    updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
    null,
  );
 
  return nextSnapshot;
}
 
function updateSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  const fiber = currentlyRenderingFiber;
  const hook = updateWorkInProgressHook();
 
  let nextSnapshot;
  const isHydrating = getIsHydrating();
  if (isHydrating) {
    if (getServerSnapshot === undefined) {
      throw new Error(
        'Missing getServerSnapshot, which is required for ' +
          'server-rendered content. Will revert to client rendering.',
      );
    }
    nextSnapshot = getServerSnapshot();
  } else {
    nextSnapshot = getSnapshot();
  }
  const prevSnapshot = (currentHook || hook).memoizedState;
  const snapshotChanged = !is(prevSnapshot, nextSnapshot);
  if (snapshotChanged) {
    hook.memoizedState = nextSnapshot;
    markWorkInProgressReceivedUpdate();
  }
  const inst = hook.queue;
 
  updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
    subscribe,
  ]);
 
  if (
    inst.getSnapshot !== getSnapshot ||
    snapshotChanged ||
    (workInProgressHook !== null && workInProgressHook.memoizedState.tag & HookHasEffect)
  ) {
    fiber.flags |= PassiveEffect;
    pushSimpleEffect(
      HookHasEffect | HookPassive,
      createEffectInstance(),
      updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
      null,
    );
 
    const root: FiberRoot | null = getWorkInProgressRoot();
 
    if (root === null) {
      throw new Error(
        'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
      );
    }
 
    if (!isHydrating && !includesBlockingLane(renderLanes)) {
      pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
    }
  }
 
  return nextSnapshot;
}
 
function pushStoreConsistencyCheck<T>(
  fiber: Fiber,
  getSnapshot: () => T,
  renderedSnapshot: T,
): void {
  fiber.flags |= StoreConsistency;
  const check: StoreConsistencyCheck<T> = {
    getSnapshot,
    value: renderedSnapshot,
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
 
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.stores = [check];
  } else {
    const stores = componentUpdateQueue.stores;
    if (stores === null) {
      componentUpdateQueue.stores = [check];
    } else {
      stores.push(check);
    }
  }
}
 
function updateStoreInstance<T>(
  fiber: Fiber,
  inst: StoreInstance<T>,
  nextSnapshot: T,
  getSnapshot: () => T,
): void {
  inst.value = nextSnapshot;
  inst.getSnapshot = getSnapshot;
 
  if (checkIfSnapshotChanged(inst)) {
    forceStoreRerender(fiber);
  }
}
 
function subscribeToStore<T>(
  fiber: Fiber,
  inst: StoreInstance<T>,
  subscribe: (() => void) => () => void,
): any {
  const handleStoreChange = () => {
    if (checkIfSnapshotChanged(inst)) {
      forceStoreRerender(fiber);
    }
  };
  return subscribe(handleStoreChange);
}
 
function checkIfSnapshotChanged<T>(inst: StoreInstance<T>): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}
 
function forceStoreRerender(fiber: Fiber) {
  const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, fiber, SyncLane);
  }
}

mountSyncExternalStore

  • getIsHydrating(), server side라면 getServerSnapshot을 호출하고 nextSnapshot에 할당합니다.
  • getIsHydrating(), client side라면 getSnapshot을 호출하고 nextSnapshot에 할당합니다.
  • 현재 렌더링 중인 Fiber의 우선순위가 Blocking Lane에 포함되지 않으면 스토어의 일관성을 확인하기 위한 pushStoreConsistencyCheck를 실행합니다.
  • subscribe 메소드가 변경되는 것을 감지하기 위해 mountEffect를 실행합니다.
  • 이전 스냅샷과 비교를 위한 updateStoreInstance를 매개변수로 pushSimpleEffect를 실행합니다.
  • nextSnapshot을 반환합니다.

updateSyncExternalStore

  • getIsHydrating(), server side라면 getServerSnapshot을 호출하고 nextSnapshot에 할당합니다.
  • getIsHydrating(), client side라면 getSnapshot을 호출하고 nextSnapshot에 할당합니다.
  • subscribe 메소드가 변경되는 것을 감지하기 위해 updateEffect를 실행합니다.
  • getSnapshot메소드가 변경되었거나 snapshot이 변경되고 HookEffectTag가 있다면 이전 스냅샷과 비교를 위한 updateStoreInstance를 매개변수로 pushSimpleEffect를 실행합니다.
    • 현재 렌더링 중인 Fiber의 우선순위가 Blocking Lane에 포함되지 않으면 스토어의 일관성을 확인하기 위한 pushStoreConsistencyCheck를 실행합니다.
  • nextSnapshot을 반환합니다.

pushStoreConsistencyCheck

  • 해당 메소드는 mount 시, update 시 snapshot이 변경될 때만 실행됩니다.
  • StoreConsistencyCheck 객체를 생성하고, fiber.updateQueue.stores에 추가합니다.

updateStoreInstance

  • checkIfSnapshotChanged 스냅샷이 변경 됨을 확인하고, forceStoreRerender 리렌더링합니다.

subscribeToStore

  • checkIfSnapshotChanged 스냅샷이 변경 됨을 확인하고, forceStoreRerender 리렌더링하는 메소드를 subscribe 메소드의 콜백으로 넘깁니다.

checkIfSnapshotChanged

  • 스냅샷이 변경되었는지 확인합니다.

forceStoreRerender

  • 업데이트를 스케줄링합니다.

Copyright ⓒ FE-dudu. All rights reserved.