Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you add/remove to a redux store generated with normalizr?

Looking the examples from the README:

Given the "bad" structure:

[{   id: 1,   title: 'Some Article',   author: {     id: 1,     name: 'Dan'   } }, {   id: 2,   title: 'Other Article',   author: {     id: 1,     name: 'Dan'   } }] 

It's extremely easy to add a new object. All I have to do is something like

return {   ...state,   myNewObject } 

In the reducer.

Now given the structure of the "good" tree, I have no idea how I should approach it.

{   result: [1, 2],   entities: {     articles: {       1: {         id: 1,         title: 'Some Article',         author: 1       },       2: {         id: 2,         title: 'Other Article',         author: 1       }     },     users: {       1: {         id: 1,         name: 'Dan'       }     }   } } 

Every approach I've thought of requires some complex object manipulation, which makes me feel like I'm not on the right track because normalizr is supposed to be making my life easier.

I can't find any examples online of someone working with the normalizr tree in this way. The official example does no adding and removing so it was no help either.

Could someone let me know how to add/remove from a normalizr tree the right way?

like image 414
m0meni Avatar asked Jan 22 '16 19:01

m0meni


People also ask

Should I keep all component's state in Redux store?

There is no “right” answer for this. Some users prefer to keep every single piece of data in Redux, to maintain a fully serializable and controlled version of their application at all times. Others prefer to keep non-critical or UI state, such as “is this dropdown currently open”, inside a component's internal state.

How do I update state Redux toolkit?

you need to ensure that you either mutate the state argument or return a new state, but not both. So you can do: setUser: (state, payload) => { //state here is an immer draft, do not use that to copy current state console. log("before", current(state)); //init state state.

What is extra reducer in Redux toolkit?

extraReducers allows createSlice to respond to other action types besides the types it has generated. As case reducers specified with extraReducers are meant to reference "external" actions, they will not have actions generated in slice. actions .


2 Answers

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)   }; 
like image 69
m0meni Avatar answered Oct 09 '22 02:10

m0meni


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

like image 45
Robin Wieruch Avatar answered Oct 09 '22 02:10

Robin Wieruch