
useState는 어떻게 구현이 되었을까?
면접 질문으로 받은 질문이다.
정말 단순한 예상으로는 아래와 같이 구현이 되었을 것이라 생각을 하지만 실제 구현은 어떻게 되었는지 모르고 있다.
const useState = (state) => {
let state = value;
const setState = (newValue) => {
if(Object.is(state, newValue) return;
state = newValue;
// render를 불러일으키는 함수
}
return [state, setState];
};
Behind the hood implementation of useState react hook
위 포스트에서는 다음과 같이 simplified 한 구현을 보이고 있다.
모듈 레벨의 clojure 인 것으로 추측된다.
let componentHooks = []
let currentHookIndex = 0
// How useState works inside React (simplified).
function useState(initialState) {
let pair = componentHooks[currentHookIndex]
if (pair) {
// This is not the first render,
// so the state pair already exists.
// Return it and prepare for next Hook call.
currentHookIndex++
return pair
}
// This is the first time we're rendering,
// so create a state pair and store it.
pair = [initialState, setState]
function setState(nextState) {
// When the user requests a state change,
// put the new value into the pair.
pair[0] = nextState
updateDOM()
}
// Store the pair for future renders
// and prepare for the next Hook call.
componentHooks[currentHookIndex] = pair
currentHookIndex++
return pair
}
그래도 무언가 부족한 것이 있는 것 같다.
React내의 구현
React 라이브러리에서는 다음과 같이 구현되어 있다.
export function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher()
return dispatcher.useState(initialState)
}
// react-reconciler\src\ReactFiberHook.new.js
// useState의 원형
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
**// 기본적인 State는 hook에 저장된다.**
**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;
**// bind 를 이용해 마지막 action 인자를 전달하지 않는다.
// 이렇게 bind로 래핑한 dispatch 액션을 전달하게 된다.
// 마지막 인자는 지연평가의 형태로 라이브러리를 사용하는
// 개발자가 state값을 업데이트하기 위해 사용된다.**
**const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));**
return [hook.memoizedState, dispatch];
}
// 2번째 요소로 주어지는 setState dispatcher
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
if (__DEV__) {
if (typeof arguments[3] === 'function') {
console.error(
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',
);
}
}
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
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)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
if (__DEV__) {
prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
//
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
간소화한다면 다음과 같이 나타낼 수 있을 것 같다.
const fiber= {
memoizedState: null;
}
const lastHook = fiber.memoizedState;
const useState = (initialValue) => {
const hook = createStateHook();
const dispatcher = createStateDispatcher.bind(null, fiber, hook);
return [hook, dispatcher];
};
const createStateHook = () => {
const hook = {
// 현재 state
memoizedState: null,
// 초기 state
baseState: null,
// 다음 hook
next: null,
};
if(lastHook) {
fiber.memoizedState = lastHook = hook;
} else {
lastHook = lastHook.next = hook;
}
return lastHook;
};
const createStateDispatcher = (renderingFiber, hook, action) => {
if(Object.is(action, hook.memoizedState)) {
// render dom
}
};
이제 React의 렌더링 과정을 알아볼 때가 된 것 같다.
Fiber가 무엇인지 궁금해졌다.