Skip to content

Commit

Permalink
fix: computed props w/ dependencies on store state (#874)
Browse files Browse the repository at this point in the history
  • Loading branch information
no-stack-dub-sack authored Oct 13, 2023
1 parent 11903f8 commit 282fa1c
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 8 deletions.
13 changes: 7 additions & 6 deletions src/computed-properties.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import equal from 'fast-deep-equal/es6';
import { areInputsEqual } from './lib';

export function createComputedPropertyBinder(parentPath, key, def, _r) {
let runOnce = false;
export function createComputedPropertyBinder(key, def, _r) {
let hasRunOnce = false;
let prevInputs = [];
let prevValue;
let prevStoreState;
Expand All @@ -28,10 +28,11 @@ export function createComputedPropertyBinder(parentPath, key, def, _r) {
const inputs = def.stateResolvers.map((resolver) =>
resolver(parentState, storeState),
);

if (
runOnce &&
(storeState === prevStoreState ||
areInputsEqual(inputs, prevInputs) ||
hasRunOnce &&
((storeState === prevStoreState &&
areInputsEqual(inputs, prevInputs)) ||
// We don't want computed properties resolved every time an action
// is handled by the reducer. They need to remain lazy, only being
// computed when used by a component or getState call;
Expand All @@ -49,7 +50,7 @@ export function createComputedPropertyBinder(parentPath, key, def, _r) {

prevInputs = inputs;
prevStoreState = storeState;
runOnce = true;
hasRunOnce = true;
return prevValue;
},
});
Expand Down
8 changes: 6 additions & 2 deletions src/extract-data-from-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,19 @@ export default function extractDataFromModel(
} else if (value[computedSymbol]) {
const parent = get(parentPath, _dS);
const bindComputedProperty = createComputedPropertyBinder(
parentPath,
key,
value,
_r,
);
bindComputedProperty(parent, _dS);
_cP.push({ key, parentPath, bindComputedProperty });
} else if (value[reducerSymbol]) {
_cR.push({ key, parentPath, reducer: value.fn, config: value.config });
_cR.push({
key,
parentPath,
reducer: value.fn,
config: value.config,
});
} else if (value[effectOnSymbol]) {
const def = { ...value };

Expand Down
53 changes: 53 additions & 0 deletions tests/computed.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,56 @@ test('computed properties operate against their original store state', () => {
expect(stateAtAPointInTime.count).toBe(1);
expect(store.getState().count).toBe(2);
});

test('issue #873: computed properties without state resolvers update when they have dependencies that depend on store state', () => {
// ARRANGE
const store = createStore({
foo: {
status: 'Ok',
setStatus: action((state, payload) => {
state.status = payload;
})
},
bar: {
status: 'Ok',
setStatus: action((state, payload) => {
state.status = payload;
})
},
baz: {
errorMessage: computed(
[
(_, storeState) => storeState.foo.status,
(_, storeState) => storeState.bar.status
],
(fooStatus, barStatus) => {
if (fooStatus === 'Error' || barStatus === 'Error') {
return 'Uh oh, error in app!';
}

return '';
}
),
hasError: computed((state) => Boolean(state.errorMessage))
}
});

let state = store.getState();

// ASSERT
expect(state.foo.status).toBe('Ok');
expect(state.bar.status).toBe('Ok');
expect(state.baz.errorMessage).toBe('');
expect(state.baz.hasError).toBe(false);

// ACT
store.getActions().foo.setStatus('Error');

state = store.getState();

// ASSERT
expect(state.foo.status).toBe('Error');
expect(state.bar.status).toBe('Ok');
expect(state.baz.errorMessage).toBe('Uh oh, error in app!');
expect(state.baz.hasError).toBe(true); // would have failed before #874
});

0 comments on commit 282fa1c

Please sign in to comment.