Skip to content

Commit 7bee9fb

Browse files
committed
Initial hooks implementation
Includes: - useState - useContext - useEffect - useRef - useReducer - useCallback - useMemo - useAPI
1 parent 37c7fe0 commit 7bee9fb

File tree

9 files changed

+2075
-31
lines changed

9 files changed

+2075
-31
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
prepareToReadContext,
8080
calculateChangedBits,
8181
} from './ReactFiberNewContext';
82+
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
8283
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
8384
import {
8485
getMaskedContext,
@@ -193,27 +194,17 @@ function forceUnmountCurrentAndReconcile(
193194
function updateForwardRef(
194195
current: Fiber | null,
195196
workInProgress: Fiber,
196-
type: any,
197+
Component: any,
197198
nextProps: any,
198199
renderExpirationTime: ExpirationTime,
199200
) {
200-
const render = type.render;
201+
const render = Component.render;
201202
const ref = workInProgress.ref;
202-
if (hasLegacyContextChanged()) {
203-
// Normally we can bail out on props equality but if context has changed
204-
// we don't do the bailout and we have to reuse existing props instead.
205-
} else if (workInProgress.memoizedProps === nextProps) {
206-
const currentRef = current !== null ? current.ref : null;
207-
if (ref === currentRef) {
208-
return bailoutOnAlreadyFinishedWork(
209-
current,
210-
workInProgress,
211-
renderExpirationTime,
212-
);
213-
}
214-
}
215203

204+
// The rest is a fork of updateFunctionComponent
216205
let nextChildren;
206+
prepareToReadContext(workInProgress, renderExpirationTime);
207+
prepareToUseHooks(current, workInProgress, renderExpirationTime);
217208
if (__DEV__) {
218209
ReactCurrentOwner.current = workInProgress;
219210
ReactCurrentFiber.setCurrentPhase('render');
@@ -222,7 +213,10 @@ function updateForwardRef(
222213
} else {
223214
nextChildren = render(nextProps, ref);
224215
}
216+
nextChildren = finishHooks(render, nextProps, nextChildren, ref);
225217

218+
// React DevTools reads this flag.
219+
workInProgress.effectTag |= PerformedWork;
226220
reconcileChildren(
227221
current,
228222
workInProgress,
@@ -406,6 +400,7 @@ function updateFunctionComponent(
406400

407401
let nextChildren;
408402
prepareToReadContext(workInProgress, renderExpirationTime);
403+
prepareToUseHooks(current, workInProgress, renderExpirationTime);
409404
if (__DEV__) {
410405
ReactCurrentOwner.current = workInProgress;
411406
ReactCurrentFiber.setCurrentPhase('render');
@@ -414,6 +409,7 @@ function updateFunctionComponent(
414409
} else {
415410
nextChildren = Component(nextProps, context);
416411
}
412+
nextChildren = finishHooks(Component, nextProps, nextChildren, context);
417413

418414
// React DevTools reads this flag.
419415
workInProgress.effectTag |= PerformedWork;
@@ -921,6 +917,7 @@ function mountIndeterminateComponent(
921917
const context = getMaskedContext(workInProgress, unmaskedContext);
922918

923919
prepareToReadContext(workInProgress, renderExpirationTime);
920+
prepareToUseHooks(null, workInProgress, renderExpirationTime);
924921

925922
let value;
926923

@@ -964,6 +961,9 @@ function mountIndeterminateComponent(
964961
// Proceed under the assumption that this is a class instance
965962
workInProgress.tag = ClassComponent;
966963

964+
// Throw out any hooks that were used.
965+
resetHooks();
966+
967967
// Push context providers early to prevent context stack mismatches.
968968
// During mounting we don't know the child context yet as the instance doesn't exist.
969969
// We will invalidate the child context in finishClassComponent() right after rendering.
@@ -1001,6 +1001,7 @@ function mountIndeterminateComponent(
10011001
} else {
10021002
// Proceed under the assumption that this is a function component
10031003
workInProgress.tag = FunctionComponent;
1004+
value = finishHooks(Component, props, value, context);
10041005
if (__DEV__) {
10051006
if (Component) {
10061007
warningWithoutStack(

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import type {FiberRoot} from './ReactFiberRoot';
1919
import type {ExpirationTime} from './ReactFiberExpirationTime';
2020
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
2121
import type {SuspenseState} from './ReactFiberSuspenseComponent';
22+
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
2223

2324
import {
2425
enableSchedulerTracing,
2526
enableProfilerTimer,
2627
} from 'shared/ReactFeatureFlags';
2728
import {
29+
FunctionComponent,
30+
ForwardRef,
2831
ClassComponent,
2932
HostRoot,
3033
HostComponent,
@@ -180,6 +183,22 @@ function safelyDetachRef(current: Fiber) {
180183
}
181184
}
182185

186+
function safelyCallDestroy(current, destroy) {
187+
if (__DEV__) {
188+
invokeGuardedCallback(null, destroy, null);
189+
if (hasCaughtError()) {
190+
const error = clearCaughtError();
191+
captureCommitPhaseError(current, error);
192+
}
193+
} else {
194+
try {
195+
destroy();
196+
} catch (error) {
197+
captureCommitPhaseError(current, error);
198+
}
199+
}
200+
}
201+
183202
function commitBeforeMutationLifeCycles(
184203
current: Fiber | null,
185204
finishedWork: Fiber,
@@ -235,13 +254,145 @@ function commitBeforeMutationLifeCycles(
235254
}
236255
}
237256

257+
function destroyRemainingEffects(firstToDestroy, stopAt) {
258+
let effect = firstToDestroy;
259+
do {
260+
const destroy = effect.value;
261+
if (destroy !== null) {
262+
destroy();
263+
}
264+
effect = effect.next;
265+
} while (effect !== stopAt);
266+
}
267+
268+
function destroyMountedEffects(current) {
269+
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
270+
if (oldUpdateQueue !== null) {
271+
const oldLastEffect = oldUpdateQueue.lastEffect;
272+
if (oldLastEffect !== null) {
273+
const oldFirstEffect = oldLastEffect.next;
274+
destroyRemainingEffects(oldFirstEffect, oldFirstEffect);
275+
}
276+
}
277+
}
278+
238279
function commitLifeCycles(
239280
finishedRoot: FiberRoot,
240281
current: Fiber | null,
241282
finishedWork: Fiber,
242283
committedExpirationTime: ExpirationTime,
243284
): void {
244285
switch (finishedWork.tag) {
286+
case FunctionComponent:
287+
case ForwardRef: {
288+
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
289+
if (updateQueue !== null) {
290+
// Mount new effects and destroy the old ones by comparing to the
291+
// current list of effects. This could be a bit simpler if we avoided
292+
// the need to compare to the previous effect list by transferring the
293+
// old `destroy` method to the new effect during the render phase.
294+
// That's how I originally implemented it, but it requires an additional
295+
// field on the effect object.
296+
//
297+
// This supports removing effects from the end of the list. If we adopt
298+
// the constraint that hooks are append only, that would also save a bit
299+
// on code size.
300+
const newLastEffect = updateQueue.lastEffect;
301+
if (newLastEffect !== null) {
302+
const newFirstEffect = newLastEffect.next;
303+
let oldLastEffect = null;
304+
if (current !== null) {
305+
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
306+
if (oldUpdateQueue !== null) {
307+
oldLastEffect = oldUpdateQueue.lastEffect;
308+
}
309+
}
310+
if (oldLastEffect !== null) {
311+
const oldFirstEffect = oldLastEffect.next;
312+
let newEffect = newFirstEffect;
313+
let oldEffect = oldFirstEffect;
314+
315+
// Before mounting the new effects, unmount all the old ones.
316+
do {
317+
if (oldEffect !== null) {
318+
if (newEffect.inputs !== oldEffect.inputs) {
319+
const destroy = oldEffect.value;
320+
if (destroy !== null) {
321+
destroy();
322+
}
323+
}
324+
oldEffect = oldEffect.next;
325+
if (oldEffect === oldFirstEffect) {
326+
oldEffect = null;
327+
}
328+
}
329+
newEffect = newEffect.next;
330+
} while (newEffect !== newFirstEffect);
331+
332+
// Unmount any remaining effects in the old list that do not
333+
// appear in the new one.
334+
if (oldEffect !== null) {
335+
destroyRemainingEffects(oldEffect, oldFirstEffect);
336+
}
337+
338+
// Now loop through the list again to mount the new effects
339+
oldEffect = oldFirstEffect;
340+
do {
341+
const create = newEffect.value;
342+
if (oldEffect !== null) {
343+
if (newEffect.inputs !== oldEffect.inputs) {
344+
const newDestroy = create();
345+
newEffect.value =
346+
typeof newDestroy === 'function' ? newDestroy : null;
347+
} else {
348+
newEffect.value = oldEffect.value;
349+
}
350+
oldEffect = oldEffect.next;
351+
if (oldEffect === oldFirstEffect) {
352+
oldEffect = null;
353+
}
354+
} else {
355+
const newDestroy = create();
356+
newEffect.value =
357+
typeof newDestroy === 'function' ? newDestroy : null;
358+
}
359+
newEffect = newEffect.next;
360+
} while (newEffect !== newFirstEffect);
361+
} else {
362+
let newEffect = newFirstEffect;
363+
do {
364+
const create = newEffect.value;
365+
const newDestroy = create();
366+
newEffect.value =
367+
typeof newDestroy === 'function' ? newDestroy : null;
368+
newEffect = newEffect.next;
369+
} while (newEffect !== newFirstEffect);
370+
}
371+
} else if (current !== null) {
372+
// There are no effects, which means all current effects must
373+
// be destroyed
374+
destroyMountedEffects(current);
375+
}
376+
377+
const callbackList = updateQueue.callbackList;
378+
if (callbackList !== null) {
379+
updateQueue.callbackList = null;
380+
for (let i = 0; i < callbackList.length; i++) {
381+
const update = callbackList[i];
382+
// Assume this is non-null, since otherwise it would not be part
383+
// of the callback list.
384+
const callback: () => mixed = (update.callback: any);
385+
update.callback = null;
386+
callback();
387+
}
388+
}
389+
} else if (current !== null) {
390+
// There are no effects, which means all current effects must
391+
// be destroyed
392+
destroyMountedEffects(current);
393+
}
394+
break;
395+
}
245396
case ClassComponent: {
246397
const instance = finishedWork.stateNode;
247398
if (finishedWork.effectTag & Update) {
@@ -496,6 +647,25 @@ function commitUnmount(current: Fiber): void {
496647
onCommitUnmount(current);
497648

498649
switch (current.tag) {
650+
case FunctionComponent:
651+
case ForwardRef: {
652+
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
653+
if (updateQueue !== null) {
654+
const lastEffect = updateQueue.lastEffect;
655+
if (lastEffect !== null) {
656+
const firstEffect = lastEffect.next;
657+
let effect = firstEffect;
658+
do {
659+
const destroy = effect.value;
660+
if (destroy !== null) {
661+
safelyCallDestroy(current, destroy);
662+
}
663+
effect = effect.next;
664+
} while (effect !== firstEffect);
665+
}
666+
}
667+
break;
668+
}
499669
case ClassComponent: {
500670
safelyDetachRef(current);
501671
const instance = current.stateNode;

packages/react-reconciler/src/ReactFiberDispatcher.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,23 @@
88
*/
99

1010
import {readContext} from './ReactFiberNewContext';
11+
import {
12+
useState,
13+
useReducer,
14+
useEffect,
15+
useCallback,
16+
useMemo,
17+
useRef,
18+
useAPI,
19+
} from './ReactFiberHooks';
1120

1221
export const Dispatcher = {
1322
readContext,
23+
useState,
24+
useReducer,
25+
useEffect,
26+
useCallback,
27+
useMemo,
28+
useRef,
29+
useAPI,
1430
};

0 commit comments

Comments
 (0)