Redux Toolkit - assign entire array to state - redux

How can I assign an entire array to my intialState object using RTK?
Doing state = payload or state = [...state, ...payload] doesn't update anything.
Example:
const slice = createSlice({
name: 'usersLikedPosts',
initialState: [],
reducers: {
getUsersLikedPosts: (state, { payload }) => {
if (payload.length > 0) {
state = payload
}
},
},
})
payload looks like this:
[
0: {
uid: '1',
title: 'testpost'
}
]
update
Doing this works but I don't know if this is a correct approach. Can anyone comment?
payload.forEach((item) => state.push(item))

immer can only observe modifications to the object that was initially passed into your function through the state argument. It is not possible to observe from outside the function if that variable was reassigned, as it only exists in the scope within the function.
You can, however, just return a new value instead of modifying the old one, if you like that better. (And in this case, it is probably a bit more performant than doing a bunch of .push calls)
So
return [...state, ...payload]
should do what you want.

Related

Actions in multiple slices in Redux toolkit

The Redux toolkit docs mention using actions (or rather action types) in multiple reducers
First, Redux action types are not meant to be exclusive to a single slice. Conceptually, each slice reducer "owns" its own piece of the Redux state, but it should be able to listen to any action type and update its state appropriately. For example, many different slices might want to respond to a "user logged out" action by clearing data or resetting back to initial state values. Keep that in mind as you design your state shape and create your slices.
But, “keeping that in mind”, what is the best way to achieve this, given that the toolkit puts the slice name at the start of each action type? And that you export a function from that slice and you call that single function to dispatch the action? What am I missing? Does this have to be done in some way that doesn’t use createSlice?
It looks like this is what extraReducers is for:
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers allows createSlice to respond to other action types besides the types it has generated.
It is a little strange that the action dispatcher should know which reducer the action belongs. I'm not sure the motivation of having reducers and extraReducers, but you can use extraReducers to allow several slices to respond to the same action.
I've found that using the extraReducers functionality when creating a slice with createSlice is the best way to do it.
In my case I've implemented this by creating a 'SliceFactory' class for each related feature. I've used it to do exactly what is in the example and reset relevant slices on user logout by listening for a LOGOUT_USER action.
Reference:
extraReducers: https://redux-toolkit.js.org/api/createSlice#extrareducer
Original article I used for the factory: https://robkendal.co.uk/blog/2020-01-27-react-redux-components-apis-and-handler-utilities-part-two
import { createSlice } from '#reduxjs/toolkit';
import { LOGOUT_USER } from '../redux/actions';
class CrudReducerFactory {
constructor(slice, state = null, initialState = {}) {
state = state || slice;
this.initialState = initialState;
const reducerResult = createSlice({
name: slice,
initialState: initialState[state],
reducers: this._generateReducers(),
extraReducers: (builder) => {
builder.addCase(LOGOUT_USER, (state, action) => {
return { ...this.initialState };
});
},
});
this.reducer = reducerResult.reducer;
this.actions = reducerResult.actions;
}
_generateReducers = () => {
return {
// Create One
requestCreateOne: (state, action) => {
state.isLoading = true;
},
requestCreateOneSuccess: (state, action) => {
state.isLoading = false;
state.one = action.payload;
},
requestCreateOneError: (state, action) => {
state.isLoading = false;
},
// ...snip...
};
};
}
export default CrudReducerFactory;
This is instantiated like so:
const factory = new CrudReducerFactory('users', 'users', { foo: 'bah', one: null, isLoading: false } );
The first argument is the name of the slice, the second is the slice of state and the third is the initial state.
You can then use factory.reducer and factory.actions to use accordingly.

Redux reducers, object key value

I have hashMap in my redux store, I want change isChecked value for children id: 2. Is it good to make it on state like this (operating on state)?
My hashMap
const childrens = {
1: { name: "Test", isChecked: false },
2: { name: "test2", isChecked: false }
};
Here is my reducer
export const childrensReducer = (state = childrens, action) => {
switch (action.type) {
case "SELECT_CHILDREN":
const id = 2;
state[id].isChecked = !state[id].isChecked;
return { ...state };
}
};
The problem is that you are mutating the state in the reducer with this line:
state[id].isChecked = !state[id].isChecked;
Why immutability is required by redux can be found in official docs:
https://redux.js.org/faq/immutable-data
One way to do is: ( I expect you send id through action.id )
case "SELECT_CHILDREN":
return {
...state,
[action.id]: {
...state[action.id],
isChecked: !state[action.id].isChecked
}
};
These kind of state operations are easier when an array is used for state.
It's not a good practice to mutate the state like you did.
There are different approaches of changing the state. Take a look at the below link to get some more information and examples.
https://www.freecodecamp.org/news/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5/
Its not a good practise to mutate state, since react depends on immutability for a lot of its features.
Consider for example lifecycle methods or rerender after comparing state/props(PureComponents)
The problem with mutating state is that when these values are passed as props to children and you try to take some decision on them based on whether the state has updated, the previous props and the current props both will hold the same value and hence the comparisons may fail leading to buggy application
The correct way to update state is
case "SELECT_CHILDREN":
const id = 2;
return {
...state,
[id]: {
...state[id],
isChecked: !state[id].isChecked
}
};

How do I pass a new key as an array with immer?

Say I have some initial state like
const initialState = {
loading: false,
updating: false,
saving: false,
data: {},
error: null
};
And I want add to data as the result of an action but the data I want to add is going to be an array. How do I go about this?
I've tried
export default produce((draft, action) => {
switch (action.type) {
case UPDATE_STATE.SUCCESS:
draft.data.new_Array.push(action.payload);
draft.loading = false;
break;
default:
}
}, initialState);
but this errors.
If I start the initial state as
const initialState = {
loading: false,
updating: false,
saving: false,
data: {
newArray: []
},
error: null
};
any update to the state before I make the array key overwrites the initial state and removes the key. ie
export default produce((draft, action) => {
switch (action.type) {
case OTHER_UPDATE_STATE.SUCCESS:
draft.data = payload.action;
draft.loading = false;
break;
default:
}
}, initialState);
can anyone help?
There's one thing I noticed you'll run into trouble, draft.data.new_Array.push(action.payload);
Make sure you don't modify the existing data structure. The reason is that redux relies on the memory reference of an object, if the object reference doesn't change, it might fool the redux that nothing actually happened in the past.
In your case, i have a feeling that nothing will ever get triggered.
One way to modify the reference is to create an new object, ex.
return [...data, newElementObject]

redux: how to update related info in store

I have a (ngrx) store for an array of Speaker object and for the SelectedSpeaker. The reducer looks like:
export const speakers = (state: any = [], { type, payload }) => {
switch (type) {
case SpeakerActions.TOGGLEFAVORITE:
return state.map(speaker => {
return speaker.id === payload.id ? _.assign({}, speaker, {isFavorite: !speaker.isFavorite}) : speaker;
});
}
}
I left out the unimportant code. The reducer for currentSpeaker looks like:
export const selectedSpeaker = (state: any = [], { type, payload }) => {
switch (type) {
case SelectedSpeakerActions.SELECT:
return payload;
}
}
Now my question, if I dispatch a SpeakerActions.TOGGLEFAVORITE for a speaker and this happens to be the SelectedSpeaker, how do I update the SelectedSpeaker in this case? Note this all works as part of an Angular2 project, for what that worth.
Generally, Redux state should be fully normalized - you shouldn't have some state in two places, since it creates exactly the problem you are seeing.
Probably the best solution in your case is for selectedSpeaker just to contain the id of the selected speaker, not the speaker itself. e.g. something like
export const selectedSpeaker = (state: any = [], { type, payload }) => {
switch (type) {
case SelectedSpeakerActions.SELECT:
return payload.id;
}
}
Obviously, you'll need to lookup the selected speaker where you use it, using the ID. You might also find it easier to have an object (or Map) from id=>speaker in your speaker store, rather than a plain array.

Does data that will never change also belong into the store?

Some parts of my initial state will never change during the whole lifecycle of my app. Now I wonder if this kind of data also belong into the store?
If yes:
Is there a way to put this data in the initial state when calling createStore(), without having an (empty) corresponding reducer function? Because since the data never changes there's no need for a reducer, but combineReducers() is pushing me to have one, otherwise it throws this error:
Unexpected key "keyName" found in initialState argument passed
to createStore. Expected to find one of the known reducer keys
instead: "otherKey1", "otherKey2". Unexpected keys will be ignored.
Example of what I'm looking for:
var dataThatWillChange = function(state, action) { /* reduce */ };
var myApp = Redux.combineReducers({
dataThatWillChange: dataThatWillChange,
dataThatWillNeverChange: Redux.dummyReducer // <-- something like this?
});
var store = Redux.createStore(myApp, {
dataThatWillChange: [0, 1, 2],
dataThatWillNeverChange: { createdBy: "me" } // <-- no need for a reducer
});
you could just have a reducer that always returns the initial state, there is nothing wrong with that:
var initialState = { createdBy: "me" }
var dataThatWillNeverChange = function(state=initialState, action) {
return state;
};
var store = Redux.createStore(myApp, {
dataThatWillChange: DataThatWillChange,
dataThatWillNeverChange: dataThatWillNeverChange
});
or more compactly:
var initialState = { createdBy: "me" }
var store = Redux.createStore(myApp, {
dataThatWillChange: DataThatWillChange,
dataThatWillNeverChange: () => initialState
});
If your constant data doesn't really belong to the "application state", you could also consider exporting it from some module and importing it whenever you need it, like
export default {
createdBy: "me",
...
}

Resources