Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions __tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ describe("copy-on-write-store", () => {
expect(log[0].posts).toEqual(log[1].posts);
});

it("doesnt update state if no change was made", () => {
let log = [];
class App extends React.Component {
render() {
return (
<Provider>
<div>
<Consumer>
{state => {
log.push(state);
return null;
}}
</Consumer>
</div>
</Provider>
);
}
}
render(<App />);
// First render is the base state
expect(log).toEqual([baseState]);
log = [];
mutate(draft => {
// Noop, no update made
});
// No update should be processed
expect(log).toEqual([]);
mutate(draft => {
// Update to the current value, no update should be processed
draft.loggedIn = true;
});
expect(log).toEqual([]);
});

it("memoizes selectors", () => {
let log = [];
let updater;
Expand Down
39 changes: 15 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,19 @@ function identityFn(n) {
}

export default function createCopyOnWriteState(baseState) {
/**
* The current state is stored in a closure, shared by the consumers and
* the provider. Consumers still respect the Provider/Consumer contract
* that React context enforces, by only accessing state in the consumer.
*/
let currentState = baseState;
let providerListener = null;
let updateState = null;
const State = React.createContext(baseState);
// Wraps immer's produce. Only notifies the Provider
// if the returned draft has been changed.
function mutate(fn) {
invariant(
providerListener !== null,
updateState !== null,
`mutate(...): you cannot call mutate when no CopyOnWriteStoreProvider ` +
`instance is mounted. Make sure to wrap your consumer components with ` +
`the returned Provider, and/or delay your mutate calls until the component ` +
`tree is moutned.`
);
const nextState = produce(currentState, draft => fn(draft, currentState));
if (nextState !== currentState) {
currentState = nextState;
providerListener();
}
updateState(fn);
}

/**
Expand All @@ -60,28 +50,29 @@ export default function createCopyOnWriteState(baseState) {
}

class CopyOnWriteStoreProvider extends React.Component {
state = this.props.initialState || currentState;
state = this.props.initialState || baseState;

componentDidMount() {
invariant(
providerListener === null,
updateState === null,
`CopyOnWriteStoreProvider(...): There can only be a single ` +
`instance of a provider rendered at any given time.`
);
providerListener = this.updateState;
// Allow a Provider to initialize state from props
if (this.props.initialState) {
currentState = this.props.initialState;
}
updateState = this.updateState;
}

componentWillUnmount() {
providerListener = null;
currentState = baseState;
updateState = null;
}

updateState = () => {
this.setState(currentState);
updateState = fn => {
this.setState(state => {
const nextState = produce(state, draft => fn(draft, state));
if (nextState === state) {
return null;
}
return nextState;
});
};

render() {
Expand Down