Skip to content

Commit c086a9d

Browse files
authored
Always update state using setState updater function (#43)
1 parent e491cf3 commit c086a9d

File tree

2 files changed

+49
-24
lines changed

2 files changed

+49
-24
lines changed

__tests__/index.spec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,40 @@ describe("copy-on-write-store", () => {
7171
expect(log[0].posts).toEqual(log[1].posts);
7272
});
7373

74+
it("doesnt update state if no change was made", () => {
75+
let log = [];
76+
class App extends React.Component {
77+
render() {
78+
return (
79+
<Provider>
80+
<div>
81+
<Consumer>
82+
{state => {
83+
log.push(state);
84+
return null;
85+
}}
86+
</Consumer>
87+
</div>
88+
</Provider>
89+
);
90+
}
91+
}
92+
render(<App />);
93+
// First render is the base state
94+
expect(log).toEqual([baseState]);
95+
log = [];
96+
mutate(draft => {
97+
// Noop, no update made
98+
});
99+
// No update should be processed
100+
expect(log).toEqual([]);
101+
mutate(draft => {
102+
// Update to the current value, no update should be processed
103+
draft.loggedIn = true;
104+
});
105+
expect(log).toEqual([]);
106+
});
107+
74108
it("memoizes selectors", () => {
75109
let log = [];
76110
let updater;

src/index.js

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,19 @@ function identityFn(n) {
2222
}
2323

2424
export default function createCopyOnWriteState(baseState) {
25-
/**
26-
* The current state is stored in a closure, shared by the consumers and
27-
* the provider. Consumers still respect the Provider/Consumer contract
28-
* that React context enforces, by only accessing state in the consumer.
29-
*/
30-
let currentState = baseState;
31-
let providerListener = null;
25+
let updateState = null;
3226
const State = React.createContext(baseState);
3327
// Wraps immer's produce. Only notifies the Provider
3428
// if the returned draft has been changed.
3529
function mutate(fn) {
3630
invariant(
37-
providerListener !== null,
31+
updateState !== null,
3832
`mutate(...): you cannot call mutate when no CopyOnWriteStoreProvider ` +
3933
`instance is mounted. Make sure to wrap your consumer components with ` +
4034
`the returned Provider, and/or delay your mutate calls until the component ` +
4135
`tree is moutned.`
4236
);
43-
const nextState = produce(currentState, draft => fn(draft, currentState));
44-
if (nextState !== currentState) {
45-
currentState = nextState;
46-
providerListener();
47-
}
37+
updateState(fn);
4838
}
4939

5040
/**
@@ -60,28 +50,29 @@ export default function createCopyOnWriteState(baseState) {
6050
}
6151

6252
class CopyOnWriteStoreProvider extends React.Component {
63-
state = this.props.initialState || currentState;
53+
state = this.props.initialState || baseState;
6454

6555
componentDidMount() {
6656
invariant(
67-
providerListener === null,
57+
updateState === null,
6858
`CopyOnWriteStoreProvider(...): There can only be a single ` +
6959
`instance of a provider rendered at any given time.`
7060
);
71-
providerListener = this.updateState;
72-
// Allow a Provider to initialize state from props
73-
if (this.props.initialState) {
74-
currentState = this.props.initialState;
75-
}
61+
updateState = this.updateState;
7662
}
7763

7864
componentWillUnmount() {
79-
providerListener = null;
80-
currentState = baseState;
65+
updateState = null;
8166
}
8267

83-
updateState = () => {
84-
this.setState(currentState);
68+
updateState = fn => {
69+
this.setState(state => {
70+
const nextState = produce(state, draft => fn(draft, state));
71+
if (nextState === state) {
72+
return null;
73+
}
74+
return nextState;
75+
});
8576
};
8677

8778
render() {

0 commit comments

Comments
 (0)