Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve state reference if unchanged and not in transaction #25

Merged
merged 4 commits into from
Mar 1, 2018
Merged

Preserve state reference if unchanged and not in transaction #25

merged 4 commits into from
Mar 1, 2018

Conversation

aduth
Copy link
Contributor

@aduth aduth commented Feb 28, 2018

Fixes #22

This pull request seeks to resolve an issue where redux-optimist returns a new state reference on every dispatch, regardless of whether the original reducer has in-fact returned a new value.

The approach is perhaps not quite as elegant as I'd hope, since it moves logic from what might otherwise be considered part of the baseReducer into the return function of the top-level optimist. After banging my head at several attempts trying to incorporate into baseReducer while also accommodating commitReducer and revertReducer, this is what I landed on.

The idea is that if we're not in the middle of a transaction and the original reducer produces a strictly equal value to the previous state, we shouldn't bother to separate state, which is not only potentially costly (in iterating over the spread assignment of innerState from separateState) but is the root cause for the new reference being returned (destructuring this way will guarantee innerState is always a new value).

I've included a test case which fails in master, and granted you access to edit my branch, in case you'd like to hack away at a refactor while achieving the same intent.

src/index.js Outdated
let separated = separateState(state);
return baseReducer(separated.optimist, separated.innerState, action);
}
let nextState = fn(state, action);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still be separating out the state, I think. otherwise the fn gets called with a different value depending on whether there is a transaction in progress. My suggestion would be to do:

const separated = separateState(state);
const nextState = fn(separated.innerState, action);
if (nextState === separated.innerState) {
  return state;
}
validateState(nextState, action);
return nextState;

@aduth
Copy link
Contributor Author

aduth commented Feb 28, 2018

Thanks for the feedback @ForbesLindesay . I've made updates in 087cba9 to ensure that the original reducer is called with only the innerState value. It seems to be a fair bit more simplified now overall.

@aduth
Copy link
Contributor Author

aduth commented Feb 28, 2018

And for good measure, added an extra unit test in 1682380 to verify the expected behavior that the original reducer is not passed the state with optimist.

src/index.js Outdated
let {optimist, innerState} = separateState(state);
if (state) {
let nextState = fn(innerState, action);
if (nextState === innerState && !optimist.length) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two new problems here:

  1. If optimist.length > 0 we should skip straight to baseReducer, not also run fn first.
  2. It looks like fn is potentially getting called twice here (if it applies a change). The fn should only ever be called once per action.

@aduth
Copy link
Contributor Author

aduth commented Mar 1, 2018

I think there are two new problems here:

  1. If optimist.length > 0 we should skip straight to baseReducer, not also run fn first.
  2. It looks like fn is potentially getting called twice here (if it applies a change). The fn should only ever be called once per action.

Thanks for the continued feedback. This is addressed in 46b6329. I still get a sense there's some redundancy here with baseReducer (validating and returning the new merged state), but it's proven difficult to refactor baseReducer while also accommodating its use in the other reducer handlers.

@ForbesLindesay ForbesLindesay merged commit 458b3c4 into ForbesLindesay:master Mar 1, 2018
@ForbesLindesay
Copy link
Owner

I think it's ok that some of the code is sort of duplicated, as long as we're not running potentially costly reducers unnecessarily.

@aduth aduth deleted the fix/22-unhandled-strict-equal branch March 1, 2018 13:42
@aduth
Copy link
Contributor Author

aduth commented Mar 1, 2018

Thanks @ForbesLindesay . While not entirely relevant to the changes here, considering how the destructuring operates to separate state, an operation which may end up not yielding any changes, the pragmatist in me wonders if it's even worth trying to keep the optimist array in state itself, vs. a variable scoped to the return value of optimist( reducer ), avoiding the need for separating or combining the array from the innerState. Food for thought, in any case.

@ForbesLindesay
Copy link
Owner

The problem with doing that is that it wouldn't play nicely with other clever reducer additions like an "undo"/"redo" reducer. It's important that we keep all our state in redux's store. Options that could be explored would be having the state always be {optimist, value}, but that requires all connect calls to separate out the value key, or passing optimist into the fn anyway, which would save us splitting it out, but does feel less "clean".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

New state object returned on unhandled action dispatch
2 participants