preact icon indicating copy to clipboard operation
preact copied to clipboard

Bugfix: nested hook state updates return the first state

Open developit opened this issue 2 years ago • 4 comments

This mostly affects useReducer, where dispatching from within the reducer will drop the resulting state update: https://codepen.io/developit/pen/YzEOWGx?editors=0010

developit avatar Feb 25 '22 17:02 developit

Size Change: +11 B (0%)

Total Size: 42.3 kB

Filename Size Change
hooks/dist/hooks.js 1.15 kB +2 B (0%)
hooks/dist/hooks.module.js 1.17 kB +3 B (0%)
hooks/dist/hooks.umd.js 1.24 kB +6 B (0%)
ℹ️ View Unchanged
Filename Size Change
compat/dist/compat.js 3.46 kB 0 B
compat/dist/compat.module.js 3.45 kB 0 B
compat/dist/compat.umd.js 3.52 kB 0 B
debug/dist/debug.js 3.01 kB 0 B
debug/dist/debug.module.js 3 kB 0 B
debug/dist/debug.umd.js 3.09 kB 0 B
devtools/dist/devtools.js 231 B 0 B
devtools/dist/devtools.module.js 240 B 0 B
devtools/dist/devtools.umd.js 307 B 0 B
dist/preact.js 3.98 kB 0 B
dist/preact.min.js 4.01 kB 0 B
dist/preact.module.js 4 kB 0 B
dist/preact.umd.js 4.04 kB 0 B
jsx-runtime/dist/jsxRuntime.js 317 B 0 B
jsx-runtime/dist/jsxRuntime.module.js 327 B 0 B
jsx-runtime/dist/jsxRuntime.umd.js 395 B 0 B
test-utils/dist/testUtils.js 437 B 0 B
test-utils/dist/testUtils.module.js 439 B 0 B
test-utils/dist/testUtils.umd.js 515 B 0 B

compressed-size-action

github-actions[bot] avatar Feb 25 '22 17:02 github-actions[bot]

Coverage Status

Coverage increased (+0.0007%) to 99.626% when pulling ed678310182ea0bf1ee5875d97f3c7055fde1ba6 on fix-nested-state-hook into 3957522a9b5393681ca66ad879e046c2f978a3e9 on master.

coveralls avatar Feb 25 '22 17:02 coveralls

I'm no longer convinced this change fixes the linked reproduction. Here's a change that should fix it:


hookState._reducer = reducer;
if (!hookState._component) {
	let actions = [];
	let dispatch = action => {
		if (actions.push(action) !== 1) return;
		let value = hookState._value[0];
		let original = value;
		while (actions.length) {  // don't yet remove the action from the queue, to ensure its length is >=1
			value = hookState._reducer(value, actions[0]);
			actions.shift(); // remove the action now that it has been applied
		}
		hookState._value = [value, dispatch];
		if (value !== original) hookState._component.setState({});
	};
	hookState._value = [
		!init ? invokeOrReturn(undefined, initialState) : init(initialState),
		dispatch
	];
	hookState._component = currentComponent;
}

developit avatar Feb 25 '22 17:02 developit

If I'm reading the codepen correctly, I thought dispatching from within a reducer was explicitly disallowed. Academically, reducers are suppose to be pure without side effects I believe. More practically I prefer the simplicity of being able to reason about reducers without having to worry about nested dispatches. I think it keeps our code lean and clean and pushes consumers to similar patterns that are easier to reason about.

Typically to share code between action handlers in reducers I've just created helper functions that can used in both places. Or if I need to do some sort of side effect in two actions I'd put that in a helper and invoke it the same place I call dispatch. Wrapping useReducer and dispatch is another way to share that code.

andrewiggins avatar Mar 09 '22 06:03 andrewiggins