React-reconciler | Fiber Hooks (19.0.0)
React v19 Hooks에서 소개되는 Hook에 대해 알아봅니다.
ReactFiberHooks 공통 로직
Hook
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
memoizedState
- 메모이제이션된 상태를 저장합니다.
baseState
- 초기 상태를 저장합니다.
baseQueue
- 렌더링 중단 또는 연기된
Update
를UpdateQueue
에 저장하며, 이후 렌더링에서 다시 사용됩니다.
queue
- 업데이트를 진행해야 할
Update
linked list입니다.
next
- linked list에서 다음
Hook
객체를 가리킵니다.
Update
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
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
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;
}
- mount 단계에서
Hook
객체를 생성합니다. workInProgressHook
이null
인지 체크하고, linked list 혹은memoizedState
에 할당합니다.- 현재 작업 중인
Hook
을 반환합니다.
updateWorkInProgressHook
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;
}
nextCurrentHook
: 이전 렌더에서 사용된Hook
을 찾습니다.nextWorkInProgressHook
:workInProgressHook
이 없으면 이전 렌더 결과에서Hook
을 찾고 있다면workInProgressHook
연결된next
를 할당합니다.nextWorkInProgressHook
이null
이 아니면workInProgressHook
,currentHook
에 값을 할당합니다.nextWorkInProgressHook
이null
이면currentHook
에 값을 할당하고 이전 렌더 결과(currentHook
)를 바탕을Hook
을 생성합니다.workInProgressHook
이null
인지 체크하고, linked list 혹은memoizedState
에 할당합니다.workInProgressHook
을 반환합니다.
areHookInputsEqual
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
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
use
는 Promise
나 context
와 같은 데이터를 참조합니다. 하위 컴포넌트에서 Promise
객체를 받아 use
훅을 통해 resolve하거나 Suspense
의 fallback UI를 노출시킵니다. useContext
를 대체하기도 합니다.
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;
}
}
}
thenable = previous;
이전에 처리되었다면 해당 내용을 재사용합니다.thenable
상태 처리를 진행합니다.fulfilled
상태라면 resolve된 value를 반환하고rejected
상태라면 에러를 반환합니다.pending
상태라면then
메소드에 성공, 실패 callback을 부여합니다.pending
상태를 한번 더 확인합니다.SuspenseException
를 던집니다.
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));
}
...
- 각 호출마다
thenableIndexCounter
를 사용하여 고유한 인덱스를 만듭니다. thenableState
이null
일 경우 (상태를 저장할) 빈 배열을 생성합니다.trackUsedThenable
를 통해 비동기 작업의 상태를 추적하고 result를 반환합니다.nextWorkInProgressHook
남아있는 Hook이 존재하는지 확인합니다.- 남아있는 Hook이 없다면
currentFiber
상태에 따라 처음 마운트인지, 업데이트인지 체크하고Dispatcher
를 결정합니다. use
메소드에서thenable
(Promise
)의 결과를 반환합니다. (context
사용 결과를 반환할 수도 있습니다.)
useContext
useContext
은 createContext
로 생성된 Context
를 읽고 구독하는 데 사용합니다.
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
은 함수 정의를 캐시하는데 사용합니다.
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
는 렌더링 간 계산 결과를 캐시하는 데 사용됩니다.
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
는 렌더링에 필요하지 않은 값을 참조할 수 있습니다.
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를 생성합니다.
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 기반으로 변형 할 수 있게 합니다.
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
useReducer
의dispatch
함수 역할을 합니다.- action이 발생할 때마다 새로운 update 객체를 생성합니다.
- render phase면
queue.pending
에 추가합니다. - render phase가 아니면 concurrent 업데이트로 처리하고 fiber를 스케줄링합니다.
mountReducer
init
초기화 함수가 있다면 실행하고 아니면initialArg
초기 값을 지정합니다.queue
,dispatch
함수를 선언하고 반환합니다.
updateReducer
hook.queue.pending
가 있다면hook.baseQueue
에 병합시킵니다.hook.baseQueue
없으면 현재 상태baseState
를memoizedState
에 할당하고 반환합니다.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.pending
에update
가 추가됩니다.- 현재 작업 중인
hook
에queue.pending
이 있다면 작업을 모두 실행시킵니다. useState
의 rerender 동작에서 재활용됩니다.
useState
useState
는 상태와 상태를 변경할 수 있는 dispatch 함수를 제공합니다.
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
useState
의setState
역할을 합니다.Update
를 선언합니다.- render phase면
queue.pending
에 추가합니다. eagerState
상태를 미리 계산하고,currentState
와Object.is
로 비교합니다.- 변경되었다면
ConcurrentQueue
에 추가하고 스케줄링합니다. useTransition
의startTransition
에서 재사용됩니다.
mountStateImpl
initialState
가 함수일 경우 실행한 후 값을hook.memoizedState
,hook.baseState
에 할당합니다.queue
를 선언하고hook.queue
에 할당합니다.useTransition
의mountTransition
에서 재사용됩니다.
mountState
hook
,dispatch(setState)
를 생성하고 반환합니다.
updateState
basicStateReducer
를 reducer로 사용하여useReducer
에서 update 시 사용되는updateReducer
를 호출합니다.useTransition
의updateTransition
에서 재사용됩니다.
rerenderState
basicStateReducer
를 reducer로 사용하여useReducer
에서 rerender 시 사용되는rerenderReducer
를 호출합니다.useTransition
의rerenderTransition
에서 재사용됩니다.
useTransition
useTransition
은 UI를 차단하지 않고 상태를 업데이트할 수 있습니다.
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 일부 업데이트를 지연시킬 수 있습니다.
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
우선 순위를 변경하고 현재 컴포넌트의 렌더링 우선순위를 조정합니다.- 그렇지 않으면
value
를memoizedState
에 할당합니다.
updateDeferredValueImpl
value
와prevValue
를Object.is
로 비교하여 동일하면 그대로 반환합니다.isCurrentTreeHidden
이true
라면, 다시 초기화(mountDeferredValueImpl
)합니다.- 현재 렌더링의 우선순위가 응급하다면 (
SyncLane
,InputContinuousLane
,DefaultLane
) 값 지연 처리를 설정하고 이전 값을 유지합니다. - 현재 렌더링의 우선순위가 응급하지 않다면 상태를 업데이트하고 새로운 값을 반환합니다.
mountDeferredValue
Hook
을 생성하고mountDeferredValueImpl
를 호출하여 초기 값을 반환합니다.
updateDeferredValue
prevValue
(currentHook.memoizedState
)를updateDeferredValueImpl
의 인자로 넘긴 값을 반환합니다.
rerenderDeferredValue
currentHook
존재 여부에 따라mountDeferredValueImpl
,updateDeferredValueImpl
를 호출하고 반환합니다.
useEffect
useEffect
는 외부 시스템과 컴포넌트를 동기화하는 데 사용됩니다.
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
에 의해 생성된 새로운EffectInstance
를updateQueue.lastEffect
에 삽입합니다.
updateEffectImpl
- update 시
nextDeps
,prevDeps
를 비교하여 동일하다면HookHasEffect
HookFlag를 제거하여 linked list에 추가하되create
함수가 동작하지 않도록 하고, 동일하지 않다면HookHasEffect
HookFlag를 포함하여 linked list에 추가하여create
함수가 동작하도록 합니다.
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;
}
...
}
...
}
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.tag
가 HasEffect | Passive
를 갖고 있지 않다면 실행되지 않습니다.
HookEffectTags를 참조하세요.
useImperativeHandle
useImperativeHandle
은 Parent Component의 ref
를 통해 Child Component의 ref
를 조작할 수 있게 합니다.
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.current
에create()
함수를 호출하여 생성된 인스턴스를 할당하고refObject.current = null;
clean-up 함수를 반환합니다.
mountImperativeHandle
- 의존성이 있다면
ref
를 의존성에 추가하고Layout
HookFlag로mountEffectImpl
를 호출합니다.
updateImperativeHandle
- 의존성이 있다면
ref
를 의존성에 추가하고Layout
HookFlag로updateEffectImpl
를 호출합니다.
useImperativeHandle이 useLayoutEffect와 동일한 시점에 실행되어야 하는 이유
(Layout
HookFlag 사용 이유)
useImperativeHandle가 useEffect와 동일한 시점에 실행된다면 Parent Component의 useEffect 내부에서 parent ref
로 child ref
를 조작할 수 없습니다.
그러므로 useImperativeHandle는 useLayoutEffect와 동일한 시점에 child ref
를 참조하고 조작할 수 있음을 보장해야 합니다.
HookEffectTags를 참조하세요.
useLayoutEffect
useLayoutEffect
은 브라우저가 화면을 그리기 전에 실행되는 useEffect입니다. (성능을 저하시킬 수 있으므로 가능하면 useEffect를 사용하는 것이 권장됩니다.)
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 용도)
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를 구독할 수 있게 합니다.
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
- 업데이트를 스케줄링합니다.