Skip to content

Commit

Permalink
Merge pull request #3567 from preactjs/pending-value
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister authored Jun 16, 2022
2 parents c77e628 + e13e9db commit 2a04fb7
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 6 deletions.
32 changes: 27 additions & 5 deletions hooks/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ options._render = vnode => {
hooks._pendingEffects = [];
currentComponent._renderCallbacks = [];
hooks._list.forEach(hookItem => {
if (hookItem._args) hookItem._args = undefined;
hookItem._pendingValue = hookItem._pendingArgs = undefined;
});
} else {
hooks._list.forEach(hookItem => {
if (hookItem._pendingArgs) {
hookItem._args = hookItem._pendingArgs;
}
if (hookItem._pendingValue) {
hookItem._value = hookItem._pendingValue;
}
hookItem._pendingValue = hookItem._pendingArgs = undefined;
});
hooks._pendingEffects.forEach(invokeCleanup);
hooks._pendingEffects.forEach(invokeEffect);
hooks._pendingEffects = [];
Expand All @@ -66,6 +75,18 @@ options.diffed = vnode => {
options._commit = (vnode, commitQueue) => {
commitQueue.some(component => {
try {
if (component.__hooks) {
component.__hooks._list.forEach(hookItem => {
if (hookItem._pendingArgs) {
hookItem._args = hookItem._pendingArgs;
}
if (hookItem._pendingValue) {
hookItem._value = hookItem._pendingValue;
}
hookItem._pendingValue = hookItem._pendingArgs = undefined;
});
}

component._renderCallbacks.forEach(invokeCleanup);
component._renderCallbacks = component._renderCallbacks.filter(cb =>
cb._value ? invokeEffect(cb) : true
Expand Down Expand Up @@ -175,7 +196,7 @@ export function useEffect(callback, args) {
const state = getHookState(currentIndex++, 3);
if (!options._skipEffects && argsChanged(state._args, args)) {
state._value = callback;
state._args = args;
state._pendingArgs = args;

currentComponent.__hooks._pendingEffects.push(state);
}
Expand All @@ -190,7 +211,7 @@ export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++, 4);
if (!options._skipEffects && argsChanged(state._args, args)) {
state._value = callback;
state._args = args;
state._pendingArgs = args;

currentComponent._renderCallbacks.push(state);
}
Expand Down Expand Up @@ -230,9 +251,10 @@ export function useMemo(factory, args) {
/** @type {import('./internal').MemoHookState} */
const state = getHookState(currentIndex++, 7);
if (argsChanged(state._args, args)) {
state._value = factory();
state._args = args;
state._pendingValue = factory();
state._pendingArgs = args;
state._factory = factory;
return state._pendingValue;
}

return state._value;
Expand Down
3 changes: 3 additions & 0 deletions hooks/src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ export type Cleanup = () => void;
export interface EffectHookState {
_value?: Effect;
_args?: any[];
_pendingArgs?: any[];
_cleanup?: Cleanup | void;
}

export interface MemoHookState {
_value?: any;
_pendingValue?: any;
_args?: any[];
_pendingArgs?: any[];
_factory?: () => any;
}

Expand Down
64 changes: 64 additions & 0 deletions hooks/test/browser/useEffect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,4 +529,68 @@ describe('useEffect', () => {
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);
});

it('should not rerun committed effects', () => {
const calls = [];
const App = ({ i }) => {
const [greeting, setGreeting] = useState('hi');

useEffect(() => {
calls.push('doing effect' + greeting);
return () => {
calls.push('cleaning up' + greeting);
};
}, []);

if (i === 2) {
setGreeting('bye');
}

return <p>{greeting}</p>;
};

act(() => {
render(<App />, scratch);
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);

act(() => {
render(<App i={2} />, scratch);
});
});

it('should not schedule effects that have no change', () => {
const calls = [];
let set;
const App = ({ i }) => {
const [greeting, setGreeting] = useState('hi');
set = setGreeting;

useEffect(() => {
calls.push('doing effect' + greeting);
return () => {
calls.push('cleaning up' + greeting);
};
}, [greeting]);

if (greeting === 'bye') {
setGreeting('hi');
}

return <p>{greeting}</p>;
};

act(() => {
render(<App />, scratch);
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);

act(() => {
set('bye');
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);
});
});
64 changes: 64 additions & 0 deletions hooks/test/browser/useLayoutEffect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,68 @@ describe('useLayoutEffect', () => {
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);
});

it('should not rerun committed effects', () => {
const calls = [];
const App = ({ i }) => {
const [greeting, setGreeting] = useState('hi');

useLayoutEffect(() => {
calls.push('doing effect' + greeting);
return () => {
calls.push('cleaning up' + greeting);
};
}, []);

if (i === 2) {
setGreeting('bye');
}

return <p>{greeting}</p>;
};

act(() => {
render(<App />, scratch);
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);

act(() => {
render(<App i={2} />, scratch);
});
});

it('should not schedule effects that have no change', () => {
const calls = [];
let set;
const App = ({ i }) => {
const [greeting, setGreeting] = useState('hi');
set = setGreeting;

useLayoutEffect(() => {
calls.push('doing effect' + greeting);
return () => {
calls.push('cleaning up' + greeting);
};
}, [greeting]);

if (greeting === 'bye') {
setGreeting('hi');
}

return <p>{greeting}</p>;
};

act(() => {
render(<App />, scratch);
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);

act(() => {
set('bye');
});
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);
});
});
40 changes: 39 additions & 1 deletion hooks/test/browser/useMemo.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createElement, render } from 'preact';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { useMemo } from 'preact/hooks';
import { useMemo, useState } from 'preact/hooks';
import { act } from 'preact/test-utils';

/** @jsx createElement */

Expand Down Expand Up @@ -122,4 +123,41 @@ describe('useMemo', () => {
expect(spy2).to.be.calledThrice;
expect(scratch.innerHTML).to.equal('<div><span>2</span><p>2</p></div>');
});

it('should not commit memoization from a skipped render', () => {
const calls = [];
let set;
const App = () => {
const [greeting, setGreeting] = useState('hi');
set = setGreeting;

const value = useMemo(() => {
calls.push('doing memo');
return greeting;
}, [greeting]);
calls.push(`render ${value}`);

if (greeting === 'bye') {
setGreeting('hi');
}

return <p>{value}</p>;
};

render(<App />, scratch);
expect(calls.length).to.equal(2);
expect(calls).to.deep.equal(['doing memo', 'render hi']);

act(() => {
set('bye');
});
expect(calls.length).to.equal(5);
expect(calls).to.deep.equal([
'doing memo',
'render hi',
'doing memo',
'render bye', // We expect a missing "doing memo" here because we return to the previous args value
'render hi'
]);
});
});

0 comments on commit 2a04fb7

Please sign in to comment.