In issue 303 in the Redux repo, Dan Abramov gives an example of a function that can wrap a store’s subscribe method in order to pass the previous state to the subscriber.
function observeStore(store, select, onChange) {
let currentState;
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState);
}
}
let unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
For some reason, this doesn't work for me. The first time my onChange handler is called currentState is undefined, as I’d expect. However, on each subsequent state change, the properties of currentState are equivalent in value (==) to those of nextState. The two state objects aren’t the same object, though because nextState === currentState evaluates to false.
I’m likely missing something really obvious. How do I capture the previous state in a closure?
The problem is that my reducer wasn’t actually creating a new instance of the state, i.e. I was mutating the state in place, rather than returning a new copy. My reducer code was something like:
const newState = Object.assign({}, state);
newState.my.prop = …;
return newState;
However, Object.assign and ES2015 destructuring assignment only do shallow copies, meaning they only affect one level deep. In my app, the state I was updating was three levels deep. I used a quick, dirty, ugly hack in my reducer to test my theory:
const newState = JSON.parse(JSON.stringify(state)); // <-- Yuck!
newState.my.prop = …;
return newState;
Don’t try that at home. This was my first clue, though. A proper implementation would use something like Facebook’s Immutable library to ensure each mutation results in a fresh clone.
Related
I'm using ngrx/component-store and loving it so far. Having prior store knowledge building my own simple ones, the only real headache I've had so far is when I've had to update an array and figured out I have to always create a new one for the internal compare() pipe to realize the array got updated.
Anyway, reading through the documentation it talks about updater methods and patchState. To me they do exactly the same thing, but their creation is slightly different. You would call patchState inside of a method while this.updater() returns a method giving you a function you can expose in your service. Anytime I'm updating my state it's always after a network call. I assume there are plenty of scenarios where you'd want to update your state without a network call so this is why you would want to have an updater available to your component to call. The question is if an updater and patchState are really doing the same thing then is it a better practice to call an updater in an effect or use patchState, or maybe am I putting too much logic in my effect?
On a side note, the docs say an updater method is supposed to be a pure function. If you're using it to your push an object onto an array then is it really pure?
// adding the selectors so people know what components are subscribing to
readonly approvals$ = this.select(state => state.requestApprovals);
readonly registration$ = this.select(state => state);
readonly updateAssessment = this.effect(($judgement: Observable<{id: string, isApproved: boolean}>) => {
return $judgement.pipe(
switchMap((evaluation) => {
const state = this.get();
return this.requestApproval.patch(state.id, state.companyName, evaluation.id, evaluation.isApproved).pipe(
tapResponse(
(result) => {
// is it better to call patchState()?
this.patchState((state) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == result.id) {
state.requestApprovals[i].isApproved = result.isApproved;
}
}
// the take away is you must assign a whole new array object when you update it.
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
// or this updater?
// this.applyDecisionPatch(evaluation);
},
// oh look! another updater reassigning my array to the state so
// it propagates to subscribers to reset the UI
() => { this.reverseDecision(); }
)
);
})
);
});
// this is private to make sure this can only be called after a network request
private readonly applyDecisionPatch = this.updater((state, value: {id: string, isApproved: boolean}) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == value.id) {
state.requestApprovals[i].isApproved = value.isApproved;
}
}
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
Note: There's no tag for ngrx-component-store so couldn't tag it.
An updater can be compared to a reducer.
All the options to modify the state should change it in an immutable way.
A library like ngrx-immer can be used to make this easier.
The main difference is that updater receives the current state, and you can change the state based on it. E.g. a conditional update, or can be used with #ngrx/entity
While with setState and patchState, you just set state properties.
setState updates the whole state object, whereas patchState only sets the given properties and doesn't touch the rest of the state object.
These two methods are also easier to use when you just want to set the state, because you don't have to create an updater function.
To answer the side question, push is not immutable. Instead of creating a new instance, it updates the array instance.
Is there any inconvenient at all if I design my reducers to, instead of reading only the partial state, had access to the full state tree?
So instead of writing this:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
I destructure state inside doSomethingWithA, c or processB reducers, separately:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state, action), // calc next state based on a
b: processB(state, action), // calc next state based on b
c: c(state, action) // calc next state based on a, b and c
}
}
Would I'd be using more RAM? Is there any performance inconvenient? I understand that in javascript, a reference is always passed as parameter, that's why we should return a new object if we want to update the state or use Immutable.JS to enforce immutability, so... again, would it be of any inconvenient at all?
No, there's nothing wrong with that. Part of the reason for writing update logic as individual functions instead of separate Flux "stores" is that it gives you explicit control over chains of dependencies. If the logic for updating state.b depends on having state.a updated first, you can do that.
You may want to read through the Structuring Reducers section in the Redux docs, particularly the Beyond combineReducers topic. It discusses other various reducer structures besides the typical combineReducers approach. I also give some examples of this kind of structure in my blog post Practical Redux, Part 7: Form Change Handling, Data Editing, and Feature Reducers.
Let's pretend I have a long-running function working on computing my new state.
Meanwhile another action comes in and changes the state while the first one did not finish and is working on stuff.
If I am imagining things correctly there is no actions queue and the state might be resolved in some unpredictable manner.
Should I be worried about this at all?
I don't mean real threads, just a concept for the lack of better wording. Actions are asynchronous and state keys are being accessed by reference.
I was concerned about the same thing so I just did some digging. It looks like two threads concurrently calling dispatch() (if it were possible) could raise an exception. But it shouldn't be possible and that error message points to a particular, different cause. The "actions queue" is in the browser's own event loop. That event loop runs async/interaction callbacks (from which we call dispatch()) one-at-a-time.
That's the responsibility of your own action creators and your own reducers, and heavily related to how you structure your actions and reducers conceptually. The Redux FAQ question on structuring "business logic" is very relevant here:Redux FAQ
Thunk action creators have access to getState, so it's very common to have a thunk check the current state and only dispatch under certain conditions, such as this example:
// An example of conditional dispatching based on state
const MAX_TODOS = 5;
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState();
if(state.todos.length < MAX_TODOS) {
dispatch({type : "ADD_TODO", text : todoText});
}
}
}
Your reducer can also have sanity checks as well:
function todosReducer(state, action) {
switch(action.type) {
case "ADD_TODO": {
if(state.todos.length >= state.maxTodos) {
return state;
}
return {
...state,
todos : state.todos.concat(action.newTodo)
}
}
default : return state;
}
}
Personally, I don't like to have my reducers just blindly merge in whatever data's in the action, unless it's very small (like, say, the name of the currently selected tab or something). I prefer to have a reasonable amount of logic in my action creator to set up the action, a minimal-ish amount of data included in the action itself, and a sufficiently smart reducer to do the work based on that action.
When using immutablejs with Redux, we will get a regular javascript object back from combineReducers, meaning it won't be an immutable data structure even if everything within it is. Doesn't this mean that using immutablejs will be in vain since a whole new state object will be created on every action anyhow?
Example:
const firstReducer = (state = Immutable.Map({greeting : 'Hey!'})) => {
return state
}
const secondReducer = (state = Immutable.Map({foo : 'bar'})) => {
return state
}
const rootReducer = combineReducers({
firstReducer, secondReducer
})
a whole new state object will be created on every action
Yes but the slices of that state that are assigned by combineReducers are not recreated. It's sort of analogous to doing this:
const person = { name: 'Rob' };
const prevState = { person };
const nextState = { person };
I created a new state object (nextState), but its person key is still set to the same exact object as was prevState's person key. There is only one instance of the string 'Rob' in memory.
The problem is when I mutate the person object, I'm changing it for multiple states:
const person = { name: 'Rob' };
const prevState = { person };
person.name = 'Dan'; // mutation
const nextState = { person };
console.log(prevState.person.name); // 'Dan'
Coming back to Redux, once all reducers have been called for the first time, they will have initialized their slices of the application's state, and your application's entire overall state would basically be equal to this:
{
firstReducer: Immutable.Map({greeting : 'Hey!'}),
secondReducer: Immutable.Map({foo : 'bar'}),
}
Note that this is a normal object. It has properties that hold Immutable objects.
When an action is dispatched and goes through each reducer, the way you've got it, the reducer simply returns the existing Immutable object back again, it doesn't create a new one. Then the new state is set to an object with a property firstReducer simply pointing back to the same Immutable object that the previous state was pointing to.
Now, what if we didn't use Immutable for firstReducer:
const firstReducer = (state = {greeting : 'Hey!'}) => {
return state
}
Same idea, that object that is used as the default for state when the reducer is first called is just passed from the previous state to the next state. There is only ever one object in memory that has the key greeting and value Hey!. There are many state objects, but they simply have a key firstReducer that points to the same object.
This is why we need to make sure we don't accidentally mutate it, but rather replace it when we change anything about it. You can accomplish this without Immutable of course by just being careful, but using Immutable makes it more foolproof. Without Immutable, it's possible to screw up and do this:
const firstReducer = (state = {greeting : 'Hey!'}, action) => {
switch (action.type) {
case 'CAPITALIZE_GREETING': {
const capitalized = state.greeting.toUpperCase();
state.greeting = capitalized; // BAD!!!
return state;
}
default: {
return state;
}
}
}
The proper way would be to create a new state slice:
const firstReducer = (state = {greeting : 'Hey!'}, action) => {
switch (action.type) {
case 'CAPITALIZE_GREETING': {
const capitalized = state.greeting.toUpperCase();
const nextState = Object.assign({}, state, {
greeting: capitalized,
};
return nextState;
}
default: {
return state;
}
}
}
The other benefit Immutable gives us is that if our reducer's slice of the state happened to have a lot of other data besides just greeting, Immutable can potentially do some optimizations under the hood so that it doesn't have to recreate every piece of data if all we did was change a single value, and yet still ensure immutability at the same time. That's useful because it can help cut down on the amount of stuff being put in memory every time an action is dispatched.
combineReducers() that comes with Redux indeed gives you a plain root object.
This isn’t a problem per se—you can mix plain objects with Immutable objects as long as you don’t mutate them. So you can live with this just fine.
You may also use an alternative combineReducers() that returns an Immutable Map instead. This is completely up to you and doesn’t make any big difference except that it lets you use Immutable everywhere.
Don’t forget that combineReducers() is actually easy to implement on your own.
Doesn't this mean that using immutablejs will be in vain since a whole new state object will be created on every action anyhow?
I’m not sure what you mean by “in vain”. Immutable doesn’t magically prevent objects from being created. When you set() in an Immutable Map, you still create a new object, just (in some cases) more efficiently. This doesn’t make a difference for the case when you have two keys in it anyway.
So not using Immutable here doesn’t really have any downsides besides your app being slightly less consistent in its choice of data structures.
In this example I'm using an action named ADD_TODO
import { createStore, combineReducers } from 'redux';
function todos(state, action) {
state = state || [];
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.text ]);
default:
return state;
}
}
function counter(state, action){
state = state || 0;
switch (action.type){
case 'INCREMENT':
return state+1;
case 'DECREMENT':
return state-1;
case 'ADD_TODO':
return state+100;
default:
return state;
}
}
var app = combineReducers({
todos: todos,
counter: counter
});
var store = createStore(app);
store.dispatch({ type: 'ADD_TODO': text: 'buy eggs' });
This cause both the "todos" and "counter" reducers to trigger.
Should I make all reducers have unique actions unless I actually intended it?
How can we implement this with multiple reducers that almost do the same thing? Multiple counters for example can have "INCREMENT" and a "DECREMENT" actions.
Should name spacing actions solve it?
eg: "POINT_INCREMENT", "POINT_DECREMENT".
There's nothing inherently wrong with having different reducers respond to the same action -- for example, if you refresh the entire state at once. But yeah, if you have two counters that correspond to different things, you probably want to come up with a naming scheme to differentiate. But I would think the action names probably should have some noun to indicate what they apply to.
This cause both the "todos" and "counter" reducers to trigger. Should I make all reducers have unique actions unless I actually intended it?
Yes, probably they should have different unique actions.
From your example it becomes not really clear what you actually intend.
Should the counter count the amount of todo's ?
In that case it can actually be sensible that a "ADD_ITEM" action would both update the counter and also add a todo item. In that case please refer to the answer of acjay
How can we implement this with multiple reducers that almost do the same thing? Multiple counters for example can have "INCREMENT" and a "DECREMENT" actions.
When displaying a list of counters in the same app, each counter can be assigned a unique identifier (id).
An action should pass along the id of the counter.
export const toggleTodo = id => ({
type: 'INCREMENT',
id
})
The reducer should then check by id which counter to update.
See this example of a todo list in the official redux docs.
https://redux.js.org/basics/example
Redux actions are in a way globals. There are different strategies to workaround this problem on a larger scale.
https://kickstarter.engineering/namespacing-actions-for-redux-d9b55a88b1b1