Redux: Normalizing Global State - reactjs

Assume the following situation:
1. Page has many posts
2. Post has many comments
I have the following reducers:
1. PagesReducer
2. PostsReducer
3. PostReducer
4. CommentsReducer
I have the following state right now:
pagesByTitle: {
dorrisPage: {
isFetching: false,
data: {
_id: "..."
title: "dorrisPage",
},
posts: [
{
isFetching: false,
data: {
_id: "..",
body: ".."
},
comments: [..]
}
]
}
}
The above structure looked okay initially, but I realized that I had to pass down the action for child states. For example, if I dispatched an action called
ADD_COMMENT
I would pass the action down to PagesReducer, PostsReducer, PostReducer, and CommentsReducer, and finally the CommentsReducer will handle that action. I think this is when I realized why normalizing states is recommended in Redux.
Can you help me with the following questions?
Is my motivation for normalizing states correct in this context?
What's the best way to normalize the example states?

you should avoid nesting.
quote from redux docs:
In a more complex app, you’re going to want different entities to
reference each other. We suggest that you keep your state as
normalized as possible, without any nesting. Keep every entity in an
object stored with an ID as a key, and use IDs to reference it from
other entities, or lists. Think of the app’s state as a database. This
approach is described in normalizr's documentation in detail.
For normalize state you can use normalizr
pages:{
items:{
1:{id: 1,title: "dorrisPage", posts:[33,57]}
2:{id: 2, title: "anotherPage",posts:[57]}
},
isFetching: false,
itemIds:[1,2,..]
},
posts:{
items:{
33:{id: 33,title: "Post33", comments:[1,2]}
57:{id: 57, title: "Post57", comments:[]}
},
isFetching: false,
itemIds:[33,57,..]
}
comments:{
items:{
1:{id: 1, user: "user1", text:"fds"}
2:{id: 2, user: "user2", text:"fds2"}
},
isFetching: false,
itemIds:[1,2,..]
}
"itemIds" is neccessary for items ordering
then reducers may look like this
export const posts = (state = initialState, action) => {
switch (action.type) {
case type.FETCH_POSTS_REQUEST:
case type.FETCH_POSTS_FAILURE:
return {...state, isFetching: action.isFetching};
case type.FETCH_POSTS_SUCCESS:
return {...state,
isFetching: action.isFetching,
items: action.entities.posts, itemsIds: action.result
};
case type.DELETE_POST:
return {...state,
items: omit(state.items, action.id),
itemsIds: state.itemsIds.filter(id=>id !== action.id)
};
case type.UPDATE_POST:
return {...state, items: {...state.items,
[action.post.id]: {...state.items[action.post.id],...action.post}}};
default:
return state;
}
}
much easier to query post by id:
const mapStateToProps = (state,ownProps) =({
post:state.posts.items[ownProps.id]
})
for computing derived data from redux store, you can use Reselect for creating memoized, composable selector functions
video tutorial

Related

Can you have slices inside slices? How to share state logic between slices?

EDIT: For complete reference, Im trying to do this with Redux
https://codesandbox.io/s/github/SenBoyer/codm-tracker-unfinished
For quick visual reference:
https://jsfiddle.net/59r31Lmt/
Im trying to make it so when you click a camo square it becomes active, when you click all the camo squares, the gold unlocks, and when you unlock gold for ALL of the guns, the diamond "unlocks"/becomes active for all of them.
This is the slice I'm using
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
sandIsActive: false,
jungleIsActive: false,
splinterIsActive: false,
tigerIsActive: false,
dragonIsActive: false,
reptileIsActive: false,
goldIsActive: false,
diamondIsActive: false,
};
const camoBarSlice = createSlice({
name: "camoBar",
initialState,
reducers: {
setSandActive: (state, action) => {
return {
...state,
sandIsActive: !state.isActive,
};
},
setJungleActive: (state, action) => {
return {
...state,
jungleIsActive: !state.isActive,
};
},
setSplinterActive: (state, action) => {
return {
...state,
splinterIsActive: !state.isActive,
};
},
setTigerActive: (state, action) => {
return {
...state,
tigerIsActive: !state.isActive,
};
},
setDragonActive: (state, action) => {
return {
...state,
dragonIsActive: !state.isActive,
};
},
setReptileActive: (state, action) => {
return {
...state,
reptileIsActive: !state.isActive,
};
},
setGoldActive: (state, action) => {
return {
...state,
goldIsActive: !state.isActive,
};
},
},
});
So when
sandIsActive: true,
jungleIsActive: true,
splinterIsActive: true,
tigerIsActive: true,
dragonIsActive: true,
reptileIsActive: true,
setGoldActive() runs and:
goldIsActive: true
then when goldIsActive is true for all 20ish guns, setDiamondActive() runs and:
diamondIsActive: true
for every gun.
How do I share the state logic between all the guns so that it will know to run the function setDiamondActive when they all become active?
Do I have to make an individual slice for each gun with the same logic? Then what though? How do I pass the logic?
Do I have to add a state value for every single square on the whole website because it's not possible to share state values across other slices? You need to keep it all in one slice? So it would have to be something like:
const initialState = {
ak47SandIsActive: false,
ak47JungleIsActive: false,
ak47SplinterIsActive: false,
ak47TigerIsActive: false,
ak47DragonIsActive: false,
ak47ReptileIsActive: false,
ak47GoldIsActive: false,
ak47DiamondIsActive: false,
m13SandIsActive: false,
m13JungleIsActive: false,
m13SplinterIsActive: false,
m13TigerIsActive: false,
m13DragonIsActive: false,
m13ReptileIsActive: false,
m13GoldIsActive: false,
m13DiamondIsActive: false,
};
then set an onClick on each and every square with the relevant dispatch action?
Can I put a slice inside another slice? So that I can have an "outer reducer" that controls the diamond square, while an inside reducer controls the entirety of the gun logic? Is there anything close to that or that could accomplish the same thing?
Basically how do I use redux to keep track of the state here? The logic in the slice I posted works for one gun at a time but how do I use it to control the logic of 20+ guns here?
I'll just make a few assumptions about your code:
You want to know when each gun is active.
You want to know when some guns are active/inactive.
You have complex interactions between these guns' states.
To that end, my advice is to combine everything in one slice. If you need to access data from one slice in another, then, realistically, it's one slice, because the scope is the same. Think of slices as scopes.
First, we need a function to determine when diamond should be toggled on:
export const isDiamondActive = (state) => {
const targetKeys = ['sand', 'jungle', reptile'...];
targetKeys.forEach(targetKey => {
if(state[targetKey] === false) {
return false;
}
}
return true;
}
This function simply loops through all of the keys that, if they're all true, should tell us that diamond should also be active. Remember this function.
Also, with RTK, you no longer need to worry about copying "past state" as you had to with Redux. Just write your logic and don't think about that, so:
setReptileActive: (state, action) => {
return {
...state,
reptileIsActive: !state.isActive,
};
},
becomes:
setReptileActive: (state, action) => {
state[reptileIsActive] = !state[reptileIsActive];
},
Or, better yet, delete all of the above reducers and refactor it in one function:
toggledGunStatus: (state, action) => {
const { id } = action;
//If the did we were provided isn't in the state, stop.
if(!(id in state)) {
return;
}
state[id] = !state[id];
}
With all of this in mind, we have two options. I'd argue that the check for isDiamondActive should be made inside the toggleGunStatus reducer, because they're closely tied together. So, a simple:
const diamondActive = isDiamondActive(state);
if(diamondActive) {
state['diamond'] = diamondActive;
}
However, things can get messy when we're talking about complex logic, so, perhaps you'd want a listener middleware setup for this - it will simply listen for the toggledGunStatus action and, in response to it, run that isDiamondActive check and dispatch/change state further.
As a side-note, it strikes me that your system will be a bit more complex. Each gun will hold a lot of data, not just the isActive flag. Consider looking into the entity adapter, it offers pre-built CRUD operations for collections of items. In essence, your slice should be an array of objects, where each object is a gun. If you were using TS, you could elegantly define all of this as:
export type Gun = {
id: string;
isActive: boolean;
}
export const entityAdapter = createEntityAdapter<Gun>();
..
const initialState: InitialState /* complex, can ignore */ = {
guns: entityAdapter.getInitialState();
}
And now, every time you work with a selector or so, you're working with a collection. Much cleaner and "future-proof".

Update deeply nested state object in redux without spread operator

I've been breaking my head for a week or something with this !!
My redux state looks similar to this
{
data: {
chunk_1: {
deep: {
message: "Hi"
}
},
chunk_2: {
something: {
something_else: {...}
}
},
... + more
},
meta: {
session: {...},
loading: true (or false)
}
}
I have an array of keys like ["path", "to", "node"] and some data which the last node of my deeply nested state object should be replaced with, in my action.payload.
Clearly I can't use spread operator as shown in the docs (coz, my keys array is dynamic and can change both in values and in length).
I already tried using Immutable.js but in vain.. Here's my code
// Importing modules ------
import {fromJS} from "immutable";
// Initializing State ---------
const InitialState = fromJS({ // Immutable.Map() doesn't work either
data: { ... },
meta: {
session: {
user: {},
},
loading: false,
error: "",
},
});
// Redux Root Reducer ------------
function StoreReducer(state = InitialState, action) {
switch (action.type) {
case START_LOADING:
return state.setIn(["meta"], (x) => {
return { ...x, loading: true };
});
case ADD_DATA: {
const keys = action.payload.keys; // This is a valid array of keys
return state.updateIn(keys, () => action.payload); // setIn doesn't work either
}
}
Error I get..
Uncaught TypeError: state.setIn (or state.updateIn) is not a function
at StoreReducer (reducers.js:33:1)
at k (<anonymous>:2235:16)
at D (<anonymous>:2251:13)
at <anonymous>:2464:20
at Object.dispatch (redux.js:288:1)
at e (<anonymous>:2494:20)
at serializableStateInvariantMiddleware.ts:172:1
at index.js:20:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:258:1)
at Object.dispatch (<anonymous>:3665:80)
What I want ?
The correct way to update my redux state (deeply nested object) with a array containing the keys.
Please note that you are using an incredibly outdated style of Redux. We are not recommending hand-written switch..case reducers or the immutable library since 2019. Instead, you should be using the official Redux Toolkit with createSlice, which allows you to just write mutating logic in your case reducers (and thus also just using any helper library if you want to use one).
Please read Why Redux Toolkit is how to use Redux today.
you could use something like that:
import { merge, set } from 'lodash';
export default createReducer(initialState, {
...
[updateSettingsByPath]: (state, action) => {
const {
payload: { path, value },
} = action;
const newState = merge({}, state);
set(newState, path, value);
return newState; },
...}

ReactJS - Proper way for using immutability-helper in reducer

I have the following object which is my initial state in my reducer:
const INITIAL_STATE = {
campaign_dates: {
dt_start: '',
dt_end: '',
},
campaign_target: {
target_number: '',
gender: '',
age_level: {
age_start: '',
age_end: '',
},
interest_area: [],
geolocation: {},
},
campaign_products: {
survey: {
name: 'Survey',
id_product: 1,
quantity: 0,
price: 125.0,
surveys: {},
},
reward: {
name: 'Reward',
id_product: 2,
quantity: 0,
price: 125.0,
rewards: {},
},
},
}
And my reducer is listening for an action to add a reward to my object of rewards:
case ADD_REWARD:
return {
...state, campaign_products: {
...state.campaign_products,
reward: {
...state.campaign_products.reward,
rewards: {
...state.campaign_products.reward.rewards,
question: action.payload
}
}
}
}
So far so good (despite the fact that every object added is named "question")... its working but its quite messy. I've tried to replace the reducer above using the immutability-helper, to something like this but the newObh is being added to the root of my state
case ADD_REWARD:
const newObj = update(state.campaign_products.reward.rewards, { $merge: action.payload });
return { ...state, newObj }
return { ...state, newObj }
First, you must understand how the object shorthand works. If you're familiar with the syntax before ES2015, the above code translates to:
return Object.assign({}, state, {
newObj: newObj
});
Note how the newObj becomes a key and a value at the same time, which is probably not what you want.
I assume the mentioned immutability-helper is this library: https://www.npmjs.com/package/immutability-helper. Given the documentation, it returns a copy of the state with updated property based on the second argument.
You're using it on a deep property so that it will return a new value for that deep property. Therefore you still have to merge it in the state, so you have to keep the approach you've labelled as messy.
What you want instead is something like:
const nextState = update(state, {
$merge: {
campaign_products: {
reward: {
rewards: action.payload
}
}
}
});
return nextState;
Note how the first argument is the current state object, and $merge object is a whole object structure where you want to update the property. The return value of update is state with updated values based on the second argument, i.e. the next state.
Side note: Working with deep state structure is difficult, as you've discovered. I suggest you look into normalizing the state shape. If applicable, you can also split the reducers into sub-trees which are responsible only for the part of the state, so the state updates are smaller.

Using redux with react, why is fetching new data deletes part of my previous state?

I have multiple requests happening one after the other, I handle them with actions and reducers but some of them seem to delete elements of my state when completed.
Can someone explain to me why this is the case?
Here is my reducer:
...
case FETCH_BLOG:
return { ...state, blogs: action.payload.data };
case FETCH_ITEM_LIST:
return { ...state.item, done: true, popular: [ ...state.popular ], nearby: [ ...state.nearby ], item: { ...state.item }, second_item: { ...state.second_item }, items: action.payload.data.item , new_item: action.payload.data.new_item, item_places: action.payload.data.item_places, stories: action.payload.data.stories };
case FETCH_ITEM_NEARBY:
return { ...state, nearby: action.payload.data.nearby, loading: false, count: action.payload.data.count, done: true };
case FETCH_ITEM_NEARBY_START:
return { ...state, loading: true };
case FETCH_ITEM_POPULAR:
return { ...state, popular: action.payload.data.popular };
...
I thought using ...state would keep the previous state and just add elements to it but it seems like it overrides it somehow.
I call my actions in this order, and I can see that after some actions are finished, part of my previous state is deleted.
this.props.itemNearbyFetch();
this.props.itemPopularFetch();
this.props.itemListFetch();
this.props.blogFetch();
in your FETCH_ITEM_LIST action you are only spreading the item into state, which would get rid of any attributes of the state object and replace with whatever attributes your item has. I think you are misunderstanding how the spread operator works.
What you want to do is something like
case FETCH_ITEM_LIST:
return { ...state,
done: true,
items: action.payload.data.items,
new_item: action.payload.data.new_item,
item_places: action.payload.data.item_places,
stories: action.payload.data.stories
};
In the previous example you would now have anything that was previously on state like popular and nearby be copied, stories, items, and item_places from the payload data are returned to the new state.
doing item:{...state.item} is redundant as it is handled when you do {...state, ...}

Updating nested redux state

I have a reducer that receives an action with a payload that I need to update the state with. The problem is the data I need to update in the state is nested data.
I've added my reducer below with some comment and what i tried to do so far.
export default function(state=data, action){
switch (action.type) {
case 'UPDATE_CONTACT_INFO':
let appointment = state[action.appointmentIndex]; // This is the appointment that needs to be updated
appointment.notification.contactInfo = action.payload; // this is the data that needs to be updated with the payload. I tried updating it like this but then not sure how to add it to the state.
return state; // Somehow need to update the state with the new state
break;
default:
return state;
}
}
Below is my initial data structure which I pass into the reducer as the default state.
data = [
{
date: 'Friday, January 6',
time: '4:00 PM-5:00 PM',
notification:
{
contactInfo: [
{
displayMethod:"Phone Call",
method:"Phone",
value:"3473686552"
},
{
displayMethod:"Email",
method:"Email",
value:"memedoe#gmail.com"
}
]
}
},
{
date: 'Saturday, January 7',
time: '2:00 PM-6:00 PM',
notification:
{
contactInfo: [
{
displayMethod:"Phone Call",
method:"Phone",
value:"2123686552"
},
{
displayMethod:"Email",
method:"Email",
value:"johndoe#gmail.com"
}
]
}
}
];
The action.payload in the reducer data is the same structure as contactInfo array in one of the appointments. [Object, Object]
With redux you never update the state. You will have to return a new state object, with the updated data.
In order to do that, you need to use either Object.assign() or the ES6 spread operator {...}
I have provided links to both:
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator
Read up on the reducers here:
http://redux.js.org/docs/basics/Reducers.html
Pay specific attention to We Dont Mutate the state point.
All problems of this type may be solved using react-addons-update package. Read here.
This case can be solved that way:
export default function(state=data, action){
switch (action.type) {
case 'UPDATE_CONTACT_INFO':
return update(state, {[action.appointmentIndex]:{notification: {contactInfo: {$set: action.payload}}}});
default:
return state;
}
}
You need to use object.assign to change the data in your store
const newstateobject = Object.assign({}, state[someIndex], {notification: Object.assign({}, state[someindex].notification, {contactInfo: action.payload})});
return Object.assign({}, state, {[someindex]: newstateobject);

Resources