Skip to content

Commit 64969d9

Browse files
committed
Implement deferring state updates (#4760)
* Implement deferring state updates * Fix test
1 parent 101cc88 commit 64969d9

File tree

8 files changed

+230
-90
lines changed

8 files changed

+230
-90
lines changed

hooks/src/index.js

Lines changed: 48 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { options as _options } from 'preact';
2+
import { SKIP_CHILDREN } from '../../src/constants';
23

34
const ObjectIs = Object.is;
45

@@ -26,6 +27,7 @@ let oldAfterDiff = options.diffed;
2627
let oldCommit = options._commit;
2728
let oldBeforeUnmount = options.unmount;
2829
let oldRoot = options._root;
30+
let oldAfterRender = options._afterRender;
2931

3032
// We take the minimum timeout for requestAnimationFrame to ensure that
3133
// the callback is invoked after the next frame. 35ms is based on a 30hz
@@ -60,10 +62,7 @@ options._render = vnode => {
6062
hooks._pendingEffects = [];
6163
currentComponent._renderCallbacks = [];
6264
hooks._list.forEach(hookItem => {
63-
if (hookItem._nextValue) {
64-
hookItem._value = hookItem._nextValue;
65-
}
66-
hookItem._pendingArgs = hookItem._nextValue = undefined;
65+
hookItem._pendingArgs = undefined;
6766
});
6867
} else {
6968
hooks._pendingEffects.forEach(invokeCleanup);
@@ -186,19 +185,13 @@ export function useReducer(reducer, initialState, init) {
186185
const hookState = getHookState(currentIndex++, 2);
187186
hookState._reducer = reducer;
188187
if (!hookState._component) {
188+
hookState._actions = [];
189189
hookState._value = [
190190
!init ? invokeOrReturn(undefined, initialState) : init(initialState),
191191

192192
action => {
193-
const currentValue = hookState._nextValue
194-
? hookState._nextValue[0]
195-
: hookState._value[0];
196-
const nextValue = hookState._reducer(currentValue, action);
197-
198-
if (!ObjectIs(currentValue, nextValue)) {
199-
hookState._nextValue = [nextValue, hookState._value[1]];
200-
hookState._component.setState({});
201-
}
193+
hookState._actions.push(action);
194+
hookState._component.setState({});
202195
}
203196
];
204197

@@ -207,75 +200,55 @@ export function useReducer(reducer, initialState, init) {
207200
if (!currentComponent._hasScuFromHooks) {
208201
currentComponent._hasScuFromHooks = true;
209202
let prevScu = currentComponent.shouldComponentUpdate;
210-
const prevCWU = currentComponent.componentWillUpdate;
211-
212-
// If we're dealing with a forced update `shouldComponentUpdate` will
213-
// not be called. But we use that to update the hook values, so we
214-
// need to call it.
215-
currentComponent.componentWillUpdate = function (p, s, c) {
216-
if (this._force) {
217-
let tmp = prevScu;
218-
// Clear to avoid other sCU hooks from being called
219-
prevScu = undefined;
220-
updateHookState(p, s, c);
221-
prevScu = tmp;
222-
}
223-
224-
if (prevCWU) prevCWU.call(this, p, s, c);
203+
204+
currentComponent.shouldComponentUpdate = (p, s, c) => {
205+
return prevScu
206+
? prevScu.call(this, p, s, c) || hookState._actions.length
207+
: hookState._actions.length;
225208
};
209+
}
210+
}
226211

227-
// This SCU has the purpose of bailing out after repeated updates
228-
// to stateful hooks.
229-
// we store the next value in _nextValue[0] and keep doing that for all
230-
// state setters, if we have next states and
231-
// all next states within a component end up being equal to their original state
232-
// we are safe to bail out for this specific component.
233-
/**
234-
*
235-
* @type {import('./internal').Component["shouldComponentUpdate"]}
236-
*/
237-
// @ts-ignore - We don't use TS to downtranspile
238-
// eslint-disable-next-line no-inner-declarations
239-
function updateHookState(p, s, c) {
240-
if (!hookState._component.__hooks) return true;
241-
242-
/** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
243-
const isStateHook = x => !!x._component;
244-
const stateHooks =
245-
hookState._component.__hooks._list.filter(isStateHook);
246-
247-
const allHooksEmpty = stateHooks.every(x => !x._nextValue);
248-
// When we have no updated hooks in the component we invoke the previous SCU or
249-
// traverse the VDOM tree further.
250-
if (allHooksEmpty) {
251-
return prevScu ? prevScu.call(this, p, s, c) : true;
252-
}
253-
254-
// We check whether we have components with a nextValue set that
255-
// have values that aren't equal to one another this pushes
256-
// us to update further down the tree
257-
let shouldUpdate = hookState._component.props !== p;
258-
stateHooks.forEach(hookItem => {
259-
if (hookItem._nextValue) {
260-
const currentValue = hookItem._value[0];
261-
hookItem._value = hookItem._nextValue;
262-
hookItem._nextValue = undefined;
263-
if (!ObjectIs(currentValue, hookItem._value[0]))
264-
shouldUpdate = true;
265-
}
266-
});
212+
if (hookState._actions.length) {
213+
const initialValue = hookState._value[0];
214+
hookState._actions.some(action => {
215+
hookState._value[0] = hookState._reducer(hookState._value[0], action);
216+
});
267217

268-
return prevScu
269-
? prevScu.call(this, p, s, c) || shouldUpdate
270-
: shouldUpdate;
271-
}
218+
hookState._didUpdate = !ObjectIs(initialValue, hookState._value[0]);
219+
hookState._value = [hookState._value[0], hookState._value[1]];
220+
hookState._didExecute = true;
221+
hookState._actions = [];
222+
}
272223

273-
currentComponent.shouldComponentUpdate = updateHookState;
224+
return hookState._value;
225+
}
226+
227+
options._afterRender = (newVNode, oldVNode) => {
228+
if (newVNode._component && newVNode._component.__hooks) {
229+
const hooks = newVNode._component.__hooks._list;
230+
const stateHooksThatExecuted = hooks.filter(
231+
/** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
232+
// @ts-expect-error
233+
x => x._component && x._didExecute
234+
);
235+
236+
if (
237+
stateHooksThatExecuted.length &&
238+
!stateHooksThatExecuted.some(x => x._didUpdate) &&
239+
oldVNode.props === newVNode.props
240+
) {
241+
newVNode._component.__hooks._pendingEffects = [];
242+
newVNode._flags |= SKIP_CHILDREN;
274243
}
244+
245+
stateHooksThatExecuted.some(hook => {
246+
hook._didExecute = hook._didUpdate = false;
247+
});
275248
}
276249

277-
return hookState._nextValue || hookState._value;
278-
}
250+
if (oldAfterRender) oldAfterRender(newVNode, oldVNode);
251+
};
279252

280253
/**
281254
* @param {import('./internal').Effect} callback

hooks/src/internal.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
VNode as PreactVNode,
55
PreactContext,
66
HookType,
7-
ErrorInfo,
7+
ErrorInfo
88
} from '../../src/internal';
99
import { Reducer, StateUpdater } from '.';
1010

@@ -32,7 +32,8 @@ export interface ComponentHooks {
3232
_pendingEffects: EffectHookState[];
3333
}
3434

35-
export interface Component extends Omit<PreactComponent<any, any>, '_renderCallbacks'> {
35+
export interface Component
36+
extends Omit<PreactComponent<any, any>, '_renderCallbacks'> {
3637
__hooks?: ComponentHooks;
3738
// Extend to include HookStates
3839
_renderCallbacks?: Array<HookState | (() => void)>;
@@ -54,8 +55,6 @@ export type HookState =
5455

5556
interface BaseHookState {
5657
_value?: unknown;
57-
_nextValue?: unknown;
58-
_pendingValue?: unknown;
5958
_args?: unknown;
6059
_pendingArgs?: unknown;
6160
_component?: unknown;
@@ -74,18 +73,19 @@ export interface EffectHookState extends BaseHookState {
7473

7574
export interface MemoHookState<T = unknown> extends BaseHookState {
7675
_value?: T;
77-
_pendingValue?: T;
7876
_args?: unknown[];
7977
_pendingArgs?: unknown[];
8078
_factory?: () => T;
8179
}
8280

8381
export interface ReducerHookState<S = unknown, A = unknown>
8482
extends BaseHookState {
85-
_nextValue?: [S, StateUpdater<S>];
8683
_value?: [S, StateUpdater<S>];
84+
_actions?: any[];
8785
_component?: Component;
8886
_reducer?: Reducer<S, A>;
87+
_didExecute?: boolean;
88+
_didUpdate?: boolean;
8989
}
9090

9191
export interface ContextHookState extends BaseHookState {

0 commit comments

Comments
 (0)