How do you add/remove to a redux store generated with normalizr?
Most of the time I use normalizr for data which I get from an API, because I don't have any control over the (usually) deep nested data structures. Let's differentiate Entities and Result and their usage.
Entities
All the pure data is in the entities object after it has been normalized (in your case articles
and users
). I would recommend either to use a reducer for all entities or a reducer for each entity type. The entity reducer(s) should be responsible to keep your (server) data in sync and to have a single source of truth.
const initialState = {
articleEntities: {},
userEntities: {},
};
Result
The results are only references to your entities. Imagine the following scenario: (1) You fetch from an API recommended articles
with ids: ['1', '2']
. You save the entities in your article entity reducer. (2) Now you fetch all articles written by a specific author with id: 'X'
. Again you sync the articles in the article entity reducer. The article entity reducer is the single source of truth for all your article data - thats it. Now you want to have another place to differentiate the articles ((1) recommended articles and (2) articles by author X). You can easily keep these in another use case specific reducer. The state of that reducer might look like this:
const state = {
recommended: ['1', '2' ],
articlesByAuthor: {
X: ['2'],
},
};
Now you can easily see that the article by author X is a recommended article as well. But you keep only one single source of truth in your article entity reducer.
In your component you can simply map entities + recommended /articlesByAuthor to present the entity.
Disclaimer: I can recommend a blog post I wrote, which shows how a real world app uses normalizr to prevent problems in state management: Redux Normalizr: Improve your State Management
I've implemented a small deviation of a generic reducer which can be found over the internet. It is capable of deleting items from cache. All you have to do is make sure that on each delete you send an action with deleted field:
export default (state = entities, action) => {
if (action.response && action.response.entities)
state = merge(state, action.response.entities)
if (action.deleted) {
state = {...state}
Object.keys(action.deleted).forEach(entity => {
let deleted = action.deleted[entity]
state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key))
.reduce((p, id) => ({...p, [id]: state[entity][id]}), {})
})
}
return state
}
usage example in action code:
await AlarmApi.remove(alarmId)
dispatch({
type: 'ALARM_DELETED',
alarmId,
deleted: {alarms: [alarmId]},
})
The following is directly from a post by the redux/normalizr creator here:
So your state would look like:
{
entities: {
plans: {
1: {title: 'A', exercises: [1, 2, 3]},
2: {title: 'B', exercises: [5, 1, 2]}
},
exercises: {
1: {title: 'exe1'},
2: {title: 'exe2'},
3: {title: 'exe3'}
}
},
currentPlans: [1, 2]
}
Your reducers might look like
import merge from 'lodash/object/merge';
const exercises = (state = {}, action) => {
switch (action.type) {
case 'CREATE_EXERCISE':
return {
...state,
[action.id]: {
...action.exercise
}
};
case 'UPDATE_EXERCISE':
return {
...state,
[action.id]: {
...state[action.id],
...action.exercise
}
};
default:
if (action.entities && action.entities.exercises) {
return merge({}, state, action.entities.exercises);
}
return state;
}
}
const plans = (state = {}, action) => {
switch (action.type) {
case 'CREATE_PLAN':
return {
...state,
[action.id]: {
...action.plan
}
};
case 'UPDATE_PLAN':
return {
...state,
[action.id]: {
...state[action.id],
...action.plan
}
};
default:
if (action.entities && action.entities.plans) {
return merge({}, state, action.entities.plans);
}
return state;
}
}
const entities = combineReducers({
plans,
exercises
});
const currentPlans = (state = [], action) {
switch (action.type) {
case 'CREATE_PLAN':
return [...state, action.id];
default:
return state;
}
}
const reducer = combineReducers({
entities,
currentPlans
});
So what's going on here? First, note that the state is normalized. We never have entities inside other entities. Instead, they refer to each other by IDs. So whenever some object changes, there is just a single place where it needs to be updated.
Second, notice how we react to CREATE_PLAN by both adding an appropriate entity in the plans reducer and by adding its ID to the currentPlans reducer. This is important. In more complex apps, you may have relationships, e.g. plans reducer can handle ADD_EXERCISE_TO_PLAN in the same way by appending a new ID to the array inside the plan. But if the exercise itself is updated, there is no need for plans reducer to know that, as ID has not changed.
Third, notice that the entities reducers (plans and exercises) have special clauses watching out for action.entities. This is in case we have a server response with “known truth” that we want to update all our entities to reflect. To prepare your data in this way before dispatching an action, you can use normalizr. You can see it used in the “real world” example in Redux repo.
Finally, notice how entities reducers are similar. You might want to write a function to generate those. It's out of scope of my answer—sometimes you want more flexibility, and sometimes you want less boilerplate. You can check out pagination code in “real world” example reducers for an example of generating similar reducers.
Oh, and I used { ...a, ...b } syntax. It's enabled in Babel stage 2 as ES7 proposal. It's called “object spread operator” and equivalent to writing Object.assign({}, a, b).
As for libraries, you can use Lodash (be careful not to mutate though, e.g. merge({}, a, b} is correct but merge(a, b) is not), updeep, react-addons-update or something else. However if you find yourself needing to do deep updates, it probably means your state tree is not flat enough, and that you don't utilize functional composition enough. Even your first example:
case 'UPDATE_PLAN':
return {
...state,
plans: [
...state.plans.slice(0, action.idx),
Object.assign({}, state.plans[action.idx], action.plan),
...state.plans.slice(action.idx + 1)
]
};
can be written as
const plan = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_PLAN':
return Object.assign({}, state, action.plan);
default:
return state;
}
}
const plans = (state = [], action) => {
if (typeof action.idx === 'undefined') {
return state;
}
return [
...state.slice(0, action.idx),
plan(state[action.idx], action),
...state.slice(action.idx + 1)
];
};
// somewhere
case 'UPDATE_PLAN':
return {
...state,
plans: plans(state.plans, action)
};
Years late to the party, but here goes —
You can easily manage normalized reducer state with no boilerplate by using normalized-reducer. You pass in a schema describing the relationships, and it gives you back the reducer, actions, and selectors to manage that slice of state.
import makeNormalizedSlice from 'normalized-reducer';
const schema = {
user: {
articles: {
type: 'article', cardinality: 'many', reciprocal: 'author'
}
},
article: {
author: {
type: 'user', cardinality: 'one', reciprocal: 'articles'
}
}
};
const {
actionCreators,
selectors,
reducer,
actionTypes,
emptyState
} = makeNormalizedSlice(schema);
The actions allow you to do basic CRUD logic as well as more complex ones such as relational attachments/detachments, cascading deletion, and batch actions.
Continuing the example, the state would look like:
{
"entities": {
"user": {
"1": {
"id": "1",
"name": "Dan",
"articles": ["1", "2"]
}
},
"article": {
"1": {
"id": "1",
"author": "1",
"title": "Some Article",
},
"2": {
"id": "2",
"author": "1",
"title": "Other Article",
}
}
},
"ids": {
"user": ["1"],
"article": ["1", "2"]
}
}
Normalized Reducer also integrates with normalizr:
import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'
const denormalizedData = {...}
const normalizrSchema = {...}
const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);
Another example of normalizr integration