How to nest Redux Toolkit reducers for a single property - javascript

I'm migrating a codebase from vanilla Redux to Redux Toolkit. I'm trying to find a good way to nest reducers created with createReducer just for a single property.
Let's say I have a setup like the following contrived example with a user reducer and a friends reducer nested under it. The user can change their name, which only affects itself, and also add and remove their friends, which affects itself and its friends array property that is managed by the friends reducer.
const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";
const initialState = {
username: "",
email: "",
lastActivity: 0,
friends: [],
};
const user = (state = initialState, action) => {
switch (action.type) {
case CHANGE_NAME: {
const { newName, time } = action.payload;
return {
...state,
name: newName,
lastActivity: time,
};
}
case ADD_FRIEND:
case REMOVE_ALL_FRIENDS: {
const { time } = action.payload;
return {
...state,
friends: friends(state.friends, action),
lastActivity: time,
};
}
default: {
return {
...state,
friends: friends(state.friends, action),
};
}
}
};
const friends = (state = initialState.friends, action) => {
switch (action.type) {
case ADD_FRIEND: {
const { newFriend } = action.payload;
return [...state, newFriend];
}
case REMOVE_ALL_FRIENDS: {
return [];
}
default: {
return state;
}
}
};
To note:
friends is necessarily correlated with user, so I had decided to nest it within its state slice.
user manually calls the friends reducer to calculate the friends slice of state with possibly overlapping action types, and only for that one friends property.
I am now trying to refactor this with Redux Toolkit createReducers. My first attempt was the following:
import { createReducer } from "#reduxjs/toolkit";
const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";
const initialState = {
username: "",
email: "",
lastActivity: 0,
friends: [],
};
const user = createReducer(initialState, (builder) => {
builder
.addCase(CHANGE_NAME, (state, action) => {
const { newName, time } = action.payload;
state.name = newName;
state.lastActivity = time;
})
.addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
state.friends = friends(state.friends, action);
};
});
const friends = createReducer(initialState, (builder) => {
builder
.addCase(ADD_FRIEND, (state, action) => {
const { newFriend } = action.payload;
state.push(newFriend);
})
.addCase(REMOVE_ALL_FRIENDS, () => []);
});
To note:
The last matcher for the user reducer is the main focus here.
The friends reducer now has two ways of modifying the state: "modifying" the state by pushing to it with .push, and returning a new empty state by directly returning [].
In my intuition this would work as it appears to be the same logic. However, this only works for the ADD_FRIEND action, and does nothing or emits an error about simultaneously modifying state and returning a new state for the REMOVE_ALL_FRIENDS action type.
This seems to be because the state being modified turns it to an ImmerJS Proxy object in the user reducer, but when it is passed to the friends reducer and it returns a state object directly instead of modifying it causing RTK to throw an error as it says you must only modify or return state, but not both. In the handler for ADD_FRIEND this is not an issue as it always modifies the state, the same as all the handlers in user.
As a hacky workaround I have manually checked whether the friends reducer returns a Proxy or a new state directly, and if it returns a new state then it sets it in the user reducer, but I am sure there is a better way:
import { createReducer, current } from "#reduxjs/toolkit";
const user = createReducer(initialState, (builder) => {
builder
.addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
const result = friends(state.friends, action);
let output;
// If state has been returned directly this will error and we can set the state manually,
// Else this will not error because a Proxy has been returned, and thus the state has been
// set already by the sub-reducer.
try {
output = current(result);
} catch (error) {
output = result;
}
if (output) {
state.progress = output;
}
};
});
My question is then how can I fix this so that I don't have to manually check the return type and can easily nest RTK reducers within each other, whether it be by restructuring my reducers or fixing the code logic?
Ideally I would still like to keep the friends reducer nested under the user reducer as that is how a lot of "vanilla" Redux code structures their state logic with many different reducers handling many different pieces of state, instead of them all being nested at the root-level with a single combineReducers call, but if there is a better and cleaner solution given I am fine with that too.
Thanks for any help, and sorry for the long post - just wanted to be as detailed as possible as other solutions online didn't seem to address this exact problem.

The issue was that my original user reducer code was reducer was returning a new state object by spreading the state and setting the friends property in that object spread. This produced an error from ImmerJS as it was returning a new value from the user reducer and was also modifying it in the friends reducer at the same time.
My posted code worked (with some modifications thanks to Linda), but to fix my original code (and I had not posted the version that produced errors - apologies) I had to change the following:
.addMatcher(
(action) =>
action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
(state, action) => ({
...state,
lastActivity: action.payload.time,
friends: friends(state.friends, action)
})
)
to:
.addMatcher(
(action) =>
action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
state.friends = friends(state.friends, action);
}
)
Thanks for the help, everyone.

In this particular case it's easy to handle the friends property in the user reducer: state.friends.push(newFriend) or state.friends = []. But there shouldn't be any issue with keeping it separate.
I did notice a few issues when trying to run your code:
Using the initialState for the whole user as the initial state of friends, instead of initialState.friends or []
Unmatched parentheses in addMatcher around the action.type check
Assigning to state.name instead of state.username
After fixing those I was not able to reproduce your issue. I am able to add and remove friends successfully.

This could actually be a bug in Redux Toolkit. Could you please file an issue with a reproduction CodeSandbox over at out github issue tracker?

Related

Setting Initial State to response from API with REDUX

I'm having trouble getting my head around setting the api response to the initial state object.
I've tried using the useAsyncThunk as stated in the documentation, however the project I'm working on only lets me use redux-thunk.
The goal is to download the usernames from https://jsonplaceholder.typicode.com/users and use that to create an autofill input field that suggests usernames.
I don't understand why you want to initialise your usernames already as initialState. That's not how the redux pattern works. Normally, you initialise your store with reasonable empty states:
const initialState = {
loading: false,
usernames: [],
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOADING_USERNAMES:
return {
...state,
loading: true,
};
case USERNAMES_LOADED:
return {
...state,
loading: false,
usernames: action.payload.usernames,
};
default:
return state;
}
}
export default reducer;
And then you set up your thunks and plain actions:
export const LOADING_USERNAMES = '[User] loading usernames';
export const USERNAMES_LOADED = '[User] usernames loaded';
export const loadingUsernames = () => ({
type: LOADING_USERNAMES,
});
export const usernamesLoaded = (usernames) => ({
type: USERNAMES_LOADED,
payload: {
usernames,
}
});
const getUsernameList = () => (dispatch) => {
dispatch(loadingUsernames()); // for the loading state
return fetch('https://jsonplaceholder.typicode.com/users')
.then(response => dispatch(usernamesLoaded(response.json())))
And then in your useEffect hook wherever you want to initialize this list, you call the thunk:
dispatch(getUsernameList());
You can decide not to show anything as long as loading is true and no list is present, but in my opinion a good UI lets the user know that something is happening. Maybe I didn't understand your question properly, but this at least would be my approach.
You state, that you want this for an autofill, so maybe don't show the form until your data is ready, or alternatively show a spinner.

Store.Dispatch() Resetting Redux Store

I dispatch(action()) to trigger an action from outside my react component. It is working correctly in that it is triggering my action and updating the new item in my store. The problem is that it seems to be completely resetting everything else in my store, which to me at least makes it more of a problem than its worth.
Worth noting: I am using next.js.
Here is a basic idea of my flow:
I have a utils folder with a service where I am dispatching this action from:
import store from './store';
store.dispatch(myAction());
I have my actions
export const myAction = () => ({
type: HELP_ME,
payload: true,
});
const initialState = {
reducerVar: fase,
otherExistingVar: {},
otherExistingVarTwo: null,
otherExistingThree:null,
otherExistingVarFour: false,
};
const myReducer = (state = initialState, action) => {
switch (action.type) {
case HELP_ME: {
const reducerVar = action.payload;
}
default: {
return state;
}
}
};
export default myReducer;
I am not sure if I am misusing store.dispatch() because I dont see why anyone would use this technique if it completely wipes out the existing data in the store. Or is there a better way to trigger this simple action from outside my component.
Basically I want to dispatch this action without completely wiping out my store just like I would dispatch the action if I were in a component.
Thank You!
you should return the state with value in reducer like this.
const myReducer = (state = initialState, action) => {
switch (action.type) {
case HELP_ME: {
return {...state, reducerVar : action.payload}
}
default: {
return state;
}
}
};
What you are trying to do is fine. But your reducer must return the whole state with your change done by the action like below.
const myReducer = (state = initialState, action) => {
switch (action.type) {
case HELP_ME:
const reducerVar = action.payload;
return {...state, reducerVar }
default:
return state;
}
};

adding an object element to an immutable array(javascript) [duplicate]

How do I add elements in my array arr[] of redux state in reducer?
I am doing this-
import {ADD_ITEM} from '../Actions/UserActions'
const initialUserState = {
arr:[]
}
export default function userState(state = initialUserState, action)
{
console.log(arr);
switch (action.type)
{
case ADD_ITEM:
return {
...state,
arr: state.arr.push([action.newItem])
}
default:
return state
}
}
Two different options to add item to an array without mutation
case ADD_ITEM :
return {
...state,
arr: [...state.arr, action.newItem]
}
OR
case ADD_ITEM :
return {
...state,
arr: state.arr.concat(action.newItem)
}
push does not return the array, but the length of it (docs), so what you are doing is replacing the array with its length, losing the only reference to it that you had. Try this:
import {ADD_ITEM} from '../Actions/UserActions'
const initialUserState = {
arr:[]
}
export default function userState(state = initialUserState, action){
console.log(arr);
switch (action.type){
case ADD_ITEM :
return {
...state,
arr:[...state.arr, action.newItem]
}
default:return state
}
}
If you need to insert into a specific position in the array, you can do this:
case ADD_ITEM :
return {
...state,
arr: [
...state.arr.slice(0, action.pos),
action.newItem,
...state.arr.slice(action.pos),
],
}
Since this question gets a lot of exposure:
If you are looking for the answer to this question, there is a good chance that you are following a very outdated Redux tutorial.
The official recommendation (since 2019) is to use the official Redux Toolkit to write modern Redux code.
Among other things, that will eliminate string action constants and generate action creators for you.
It will also employ methods that allow you to just write mutating logic in your Reducers created by createReducer or createSlice, so there is no need to write immutable code in Reducers in modern Redux in the first place.
Please follow the official Redux tutorials instead of third-party tutorials to always get the most up-to-date information on good Redux practices and will also show you how to use Redux Toolkit in different common scenarios.
For comparison, in modern Redux this would look like
const userSlice = createSlice({
name: "user",
initialState: {
arr:[]
},
reducers: {
// no ACTION_TYPES, this will internally create a type "user/addItem" that you will never use by hand. You will only see it in the devTools
addItem(state, action) {
// you can use mutable logic in createSlice reducers
state.arr.push(action.payload)
}
}
})
// autogenerated action creators
export const { addItem } = slice.actions;
// and export the final reducer
export default slice.reducer;
If you want to combine two arrays, one after another then you can use
//initial state
const initialState = {
array: [],
}
...
case ADD_ARRAY :
return {
...state,
array: [...state.array, ...action.newArr],
}
//if array = [1,2,3,4]
//and newArr = [5,6,7]
//then updated array will be -> [1,2,3,4,5,6,7]
...
This Spread operator (...) iterates array element and store inside the array [ ] or spreading element in the array, what you can simply do using "for loop" or with any other loop.
I have a sample
import * as types from '../../helpers/ActionTypes';
var initialState = {
changedValues: {}
};
const quickEdit = (state = initialState, action) => {
switch (action.type) {
case types.PRODUCT_QUICKEDIT:
{
const item = action.item;
const changedValues = {
...state.changedValues,
[item.id]: item,
};
return {
...state,
loading: true,
changedValues: changedValues,
};
}
default:
{
return state;
}
}
};
export default quickEdit;
The easiest solution to nested arrays is concat():
case ADD_ITEM:
state.array = state.array.concat(action.paylod)
return state
concat() spits out an updated array without mutating the state. Simply set the array to the output of concat() and return the state.
This worked for me
//Form side
const handleSubmit = (e) => {
e.preventDefault();
let Userdata = { ...userdata, id: uuidv4() };
dispatch(setData(Userdata));
};
//Reducer side
const initialState = {
data: [],
};
export const dataReducer = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.SET_DATA:
return { ...state, data: [...state.data, action.payload] };
default:
return state;
}
};

REDUX: store variables accessed via this.props are outdated but store.getState() works

Well hello there!
I'm having some issues - that I never had before - by accessing store variables through mapStateToProps. Namely, they never change and always have their default value I setup in the store BEFORE changing them in any way. If I call them by store.getState().reducer.x my code works!
Here's my store:
export const initialState = {
isKeyManagementWindowOpen: false
};
const rootReducer = combineReducers({
some: someReducer,
settings: settingsComponentReducer
)};
const store = createStore(rootReducer, compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : variable => variable));
export default store;
settingsComponentActions.js
export const TOGGLE_KEY_MANAGEMENT_WINDOW = 'TOGGLE_KEY_MANAGEMENT_WINDOW';
export const toggleKeyManagementWindow = isKeyManagementWindowOpen => {
return { type: TOGGLE_KEY_MANAGEMENT_WINDOW, isKeyManagementWindowOpen};
}
settingsComponentReducer.js
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return Object.assign({}, state, {
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default: return state;
}
};
One thing that may be causing issues is that I am calling this.props in my websocket's subscribe method.
Key.js
connectToWebsocket = ip => {
const stompClient = Stomp.client(`url/receivekey`);
stompClient.heartbeat.outgoing = 0;
stompClient.heartbeat.incoming = 0;
stompClient.debug = () => null;
stompClient.connect({ name: ip }, frame => this.stompSuccessCallBack(frame, stompClient), err => this.stompFailureCallBack(err, ip));
}
stompSuccessCallBack = (frame, stompClient) => {
stompClient.subscribe(KEY_READER_NODE, keyData => {
if (!this.props.isKeyManagementWindowOpen) {
this.loginWithKey(keyData.body);
} else {
this.addToKeyList(keyData.body);
}
});
}
Even though I set isKeyManagementWindowOpen beforehand to true it still resolves to false. If I swap !this.props.isKeyManagementWindowOpen with !store.getState().settings.isKeyManagementWindowOpen the code works and it goes into this.addToKeyList(keyData.body).
So, if I swap those but LEAVE every store call in this.addToKeyList as this.props. then those are all default valued too, which doesn't make sense. It only works if I swap every this.props. line with store.getState()....
const mapStateToProps = state => ({
...
...
isKeyManagementWindowOpen: state.settings.isKeyManagementWindowOpen,
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Key));
As of now, my code works but I'd like to call the props as this.props... and not via store.getState().... Any idea why this could happen?
Thanks!
Seems like you're using deep state
Object assign only makes shallow copies of objects. So let's try to eliminate the easiest possible cause.
export const settingsComponentReducer = (state = initialState, action) => {
const newState = JSON.parse(JSON.stringify(state));
Then use newState instead of state below.
This will make a deep copy of your state and will always be a new object forcing your app to see it as a new prop and re-render correctly.
Why not use something like this, as you shouldn't directly mutate the overall state of the app, only update it if an action is triggered but spread the original state in prior to updating.
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return {
...state,
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default:
return state;
}
};
The problem is that React cannot have updated any value in this.props by the time the next line of code has executed.
This is not actually a Redux-specific problem. In any React component, triggering a state change on a line will still result in the same props and state values on the next line, because the current function is still executing and React has not re-rendered yet.

Create initialState on Store with Redux

I am using repo from https://github.com/caspg/simple-data-table-map to implement into my existing app. However, in my existing app it contains various reducers with its own particular initialState. In the repo, the initialState is in the main.js
const initialState = {
regionData: statesData,
emptyRegions: [],
sortState: { key: 'regionName', direction: 'ASC' }
};
const store = createStore(rootReducer, initialState);
I separate out the reducers in the repo ( without using the index.js to combine 3 of them ) so I can add them into my existing code.
const reducers = {
User: require('../reducers/User.js'),
Alert: require('../reducers/Alert.js'),
Auth: require('../reducers/Auth.js'),
Header: require('../reducers/Header.js'),
PasswordPolicy: require('../reducers/PasswordPolicy.js'),
AuditTrail: require('../reducers/AuditTrail.js'),
StoragePolicy: require('../reducers/StoragePolicy.js'),
DragAndDrop: require('../reducers/DragAndDrop.js'),
UserProfile: require('../reducers/UserProfile.js'),
emptyRegions: require('../reducers/map/emptyRegions.js'), //here is it
regionData: require('../reducers/map/regionData.js'), //here is it
sortState: require('../reducers/map/sortState.js'), //here is it
Storage: require('./storage/Storage.js'),
router: routerStateReducer
};
module.exports = combineReducers(reducers);
I have a file to combine all the reducers and each of their initialstate(which in the same file with the particular reducer)
stores/index.js
module.exports = function(initialState) {
const store = redux.createStore(reducers, initialState,
compose(reduxReactRouter({ createHistory }), window.devToolsExtension ? window.devToolsExtension() : f => f)
)
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextReducer = require('../reducers')
store.replaceReducer(nextReducer)
})
}
return store
}
I tried to put the initiateState into sortState.js but it is not working.
The data do not show up. It must be something that from the author's repo
const store = createStore(rootReducer, initialState);
that set the initialState into the application state.
Please enlighten me. Thanks
If you are using combineReducers(…), each Reducer needs to return its initial state on the first run.
const DEFAULT_STATE = { … }
const emptyRegionsReducer = (state = DEFAULT_STATE, action) => {
switch (action.type) {
…
default:
return state;
}
}
Have look at line 60 in the redux repo. For each reducer in the object:
const initialState = reducer(undefined, { type: ActionTypes.INIT })
what is triggered right away when combineReducers() is called.
There are two ways to initialize the state in Redux - the docs explain the details very well.
You can set the global initialState by passing it as an optional second argument to the createStore function, like this:
const initialState = { /*..initial state props..*/ }
const store = createStore(rootReducer, initialState);
OR, you can set the initialState for an individual reducer in the following way (from official docs):
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
}
Passing state = 0 means that state is given the value 0 within that reducer when it is first initialized. After the counter reducer is initialized, the state value becomes whatever counter is in the Redux store. One important thing to note is that you must include the line default: return state; for this method to work.
In your case, it looks like you are using both methods. If you are happy with the initialState that you set within your reducers, then you should remove the initialState argument from createStore:
const store = createStore(rootReducer);

Categories

Resources