Skip to content

Commit 9c9bd24

Browse files
matthewpmattheol
andauthored
useState: Prevent updating when the value hasn't changed (#392)
* fix: useState updater * Prevent updating when the value hasn't changed Closes #273 * Use a changeset * Use Object.is for comparison Co-authored-by: Mateusz Olsztyński <[email protected]>
1 parent f6fcc6e commit 9c9bd24

File tree

6 files changed

+50
-2
lines changed

6 files changed

+50
-2
lines changed

.changeset/khaki-elephants-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"haunted": patch
3+
---
4+
5+
Prevent a setState that doesn't change the state from resulting in a rerender

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@rocket/cli": "^0.9.8",
4545
"@rocket/launch": "^0.5.4",
4646
"@rocket/search": "^0.4.1",
47+
"@types/mocha": "^9.1.1",
4748
"@web/dev-server-esbuild": "^0.3.0",
4849
"@web/test-runner": "^0.13.18",
4950
"cem-plugin-jsdoc-function": "^0.0.5",

src/use-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class HauntedControllerHost implements ReactiveControllerHost {
4545
requestUpdate(): void {
4646
if (!this._updatePending) {
4747
this._updatePending = true;
48-
microtask.then(() => this.kick(this.count + 1));
48+
microtask.then(() => this.kick(this.count += 1));
4949
}
5050
}
5151

src/use-state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ const useState = hook(class<T> extends Hook {
2929
}
3030

3131
updater(value: NewState<T>): void {
32+
const [previousValue] = this.args;
3233
if (typeof value === 'function') {
3334
const updaterFn = value as (previousState?: T) => T;
34-
const [previousValue] = this.args;
3535
value = updaterFn(previousValue);
3636
}
3737

38+
if (Object.is(previousValue, value)) {
39+
return;
40+
}
41+
3842
this.makeArgs(value);
3943
this.state.update();
4044
}

test/use-state.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,29 @@ describe('useState', () => {
2424
await nextFrame();
2525
expect(span.textContent).to.equal('33');
2626
});
27+
28+
it('Updater function should only trigger rerender if state has changed', async () => {
29+
const tag = 'use-state-callback-two';
30+
let setter, runs = 0;
31+
32+
function App() {
33+
runs++;
34+
let [age, setAge] = useState(() => 8);
35+
setter = setAge;
36+
return html`<span>${age}</span>`;
37+
}
38+
39+
customElements.define(tag, component(App));
40+
41+
const el = await fixture<HTMLElement>(html`<use-state-callback-two></use-state-callback-two>`);
42+
43+
let span = el.shadowRoot.firstElementChild;
44+
expect(span.textContent).to.equal('8');
45+
46+
setter(8);
47+
48+
await nextFrame();
49+
50+
expect(runs).to.equal(1);
51+
});
2752
});

0 commit comments

Comments
 (0)