How to push to array value of reducer state in redux toolkit? - reactjs

I have a simple reducer function
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { TSnackBarProps } from 'plugins/notification/NotificationContext';
import { MAX_STACK } from 'plugins/notification/NotificationsStack';
interface INotificationState {
notifications: TSnackBarProps[];
}
const initialState: INotificationState = {
notifications: [],
};
const notificationSlice = createSlice({
name: 'notification',
initialState,
reducers: {
addNewNotification(state, action: PayloadAction<TSnackBarProps>) {
const { notifications } = state;
const { payload: notification } = action;
if (notifications.find((n) => n.severity === notification.severity && n.key === notification.key)) {
return;
}
if (notifications.length >= MAX_STACK) {
notifications.splice(0, notifications.length - MAX_STACK);
}
state.notifications.push(notification);
},
},
});
export default notificationSlice.reducer;
But, it throws the error as shown below:
I am just starting to write this reducer and got stuck here. Thanks for your help.
Also, TSnackBarProps is just SnackBarProps type from material-ui with severity property added.

immer's Draft type, which is used by RTK removes the readonly temporarily from all state types so that you can freely modify it. Unfortunately, that goes a little bit too far in this case.
But of course: you know better than TypeScript here. So you could just cast your state variable, which is Draft<INotificationState> at the moment to INotificationState to assign to it, which is a perfectly valid thing to do in a situation like this.

Related

Is it a good practice to use a single reducer to set all states in Redux

I'm relatively new to Redux and still grasping the concepts of this library. I'm trying to understand if is it a good or bad practice to use a single reducer to set the state?
Here is what I mean by that. In most examples and tutorials I see the slice.js file with multiple reducers set in the following fashion:
slice.js:
import { createSlice } from '#reduxjs/toolkit'
const userSlice = createSlice({
name: 'user',
initialState: {
id: null,
name: null,
isPremium: false,
isLoggedIn: false
// ... and possibly a whole bunch of even more properties
},
reducers: {
nameSet: (data, action) {
data.name = action.payload
},
idSet: (data, action) {
data.id = action.payload
}
// ... rest of the reducers that corresponds to the rest of the state the properties
},
});
export const { nameSet, idSet } = userSlice.actions
export default userSlice.reducer
In components and other files, it used like:
import { store } from '#/stores/store'
import { nameSet } from '#/stores/user/slice'
// ...
const userName = 'Jon Doe'
store.dispatch(nameSet(userName))
// ...
So depending on a component or action I want to perform, I have to import a certain reducer, which will result in constant managing of imports and slice.js file in case I want to add, remove or modify reducers.
So I was thinking instead of creating multiple reducers like nameSet, idSet, etc inside of slice.js file. Is it a good idea to create a single reducer and then call it with an argument that is an object which will hold all the data about the state property and value, like so:
slice.js:
import { createSlice } from '#reduxjs/toolkit'
const userSlice = createSlice({
name: 'user',
initialState: {
id: null,
name: null,
isPremium: false,
isLoggedIn: false
// ... and a possible bunch of even more properties
},
reducers: {
set: (data, action) {
const { key, value, options = {} } = action.payload
// options is an optional argument that can be used for condition
data[key] = value
}
}
});
export const { set } = userSlice.actions
export default userSlice.reducer
So in the end I always import a single reducer in any component file and used it like this:
import { store } from '#/stores/store'
import { set } from '#/stores/user/slice'
// ...
const userName = 'Jon Doe'
store.dispatch(set({ key: 'name', value: userName }))
// ...
I know it's a pretty simplistic example and in the real life it's not always the case, but I guess even the single reducer can be created in a way to handle more complex cases.

useSelector only returning initial state

I'm building a simple review app with react and redux toolkit.
Reviews are added via a form in AddReview.js, and I'm wanting to display these reviews in Venue.js.
When I submit a review in AddReview.js, the new review is added to state, as indicated in redux dev tools:
However when I try to pull that state from the store in Venue.js, I only get the initial state (the first two reviews), and not the state I've added via the submit form:
Can anyone suggest what's going wrong here?
Here's how I've set up my store:
store.js
import { configureStore } from "#reduxjs/toolkit";
import reviewReducer from '../features/venues/venueSlice'
export const store = configureStore({
reducer:{
reviews: reviewReducer
}
})
Here's the slice managing venues/reviews:
venueSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.push(action.payload)
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And here's the Venue.js component where I want to render reviews:
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Venue = () => {
const { id } = useParams()
const reviews = useSelector((state) => state.reviews)
console.log(reviews)
return (
<div>
{reviews.map(item => (
<h1>{item.title}</h1>
))}
</div>
)
}
export default Venue;
Form component AddReview.js
import { useState } from "react"
import { useDispatch } from "react-redux"
import { ADD_REVIEW } from "./venueSlice"
import { nanoid } from "#reduxjs/toolkit"
const AddReview = () => {
const [ {title,blurb}, setFormDetails ] = useState({title:'', blurb: ''})
const dispatch = useDispatch()
const handleChange = (e) => {
const { name, value } = e.target
setFormDetails(prevState => ({
...prevState,
[name]: value
}))
}
const handleSubmit = (e) => {
console.log('it got here')
e.preventDefault()
if(title && blurb){
dispatch(ADD_REVIEW({
id: nanoid(),
title,
blurb
}))
// setFormDetails({title: '', blurb: ''})
}
}
return(
<div>
<form onSubmit={handleSubmit}>
<input
type = 'text'
name = 'title'
onChange={handleChange}
/>
<input
type = 'text'
name = 'blurb'
onChange={handleChange}
/>
<button type = "submit">Submit</button>
</form>
</div>
)
}
export default AddReview;
I can notice that you pushing directly to the state, I can suggest to use variable in the state and then modify that variable.
Also I suggest to use concat instead of push. Where push will return the array length, concat will return the new array.
When your code in the reducer will looks like that:
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
reviews: [{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}]
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.reviews = state.reviews.concat(action.payload);
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And then your selector will looks like that:
const reviews = useSelector((state) => state.reviews.reviews)
Your code seems to be fine. I don't see any reason why it shouldn't work.
I run your code on stackblitz react template and its working as expected.
Following is the link to the app:
stackblitz react-redux app
Link to the code:
Project files react-redux
if you are still unable to solve the problem, do create the sandbox version of your app with the issue to help further investigate.
Thanks
Expanding on #electroid answer (the solution he provided should fix your issue and here is why):
Redux toolkit docs mention on Rules of Reducers :
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
and on Reducers and Immutable Updates :
One of the primary rules of Redux is that our reducers are never allowed to mutate the original / current state values!
And as mdn docs specify the push method changes the current array (so it mutates your state). You can read more about mutating the state in the second link link (Reducers and Immutable Updates).
If you really want to keep the state.reviews and avoid state.reviews.reviews you could also do something like this:
ADD_REVIEW: (state,action) => {
state = [...state, action.payload];
}
But I wouldn't recommend something like this in a real app (it is avoided in all the examples you can find online). Some reason for this would be:
It harder to work with, read and track the state when having an overall dynamic state instead of a state structure
It leads to a lot of slices in a real app (creating a slices for an array without grouping the data) which can also become hard to track and maintain.
Usually you need a redux slice in multiple parts of the app (otherwise you can just use state). That data is usually bigger than just an array and not grouping the data properly on reducers can become very very confusing.
But I would definitely advise to use something else (not reviews.reviews). In your case I think something like state.venue.reviews
(so on store.js
...
export const store = configureStore({
reducer:{
venue: reviewReducer // reviewReducer should probably also be renamed to venueSlice or venueReducer
}
})
So an option to avoid state.venue.reviews or state.reviews.reviews would be to export a selector from the venueSlice.js:
export const selectReviews = (state) => state.venue.reviews
and in your Venue.js component you can just use
const reviews = useSelector(selectReviews)
Exporting a selector is actually suggested by the redux toolkit tutorials as well (this link is for typescript but the same applies to javascript). Although this is optional.

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; },
...}

Cannot read properties of undefined (reading 'type') Redux

I have a very simple redux boilerplate code that works fine if I use action?.type and not when I use action.type. My understanding is that the action is never null right? So, any idea why I'm getting the error:
Cannot read properties of undefined (reading 'type')
The way I've created the project is I have an action export const NAME_UPDATED = 'profile/updateName'; and an action creator for this action i.e. export function updateProfile(text) { return { type: NAME_UPDATED, payload: text }}.
My reducer is:
import { NAME_UPDATED } from './actions';
//initial state
const initialState = { name: 'Initial Name' }
//reducers
export function myReducer(state = initialState, action) {
switch (action?.type) {
case NAME_UPDATED: return { ...state, name: action.payload };
default: return state;
}
}
I'm dispatching the actions on a button press:
// Imports
const myDispatch = useDispatch();
const mySelector = useSelector(myReducer);
// Other code
<p>
The Current value of <code>name</code> in the store is: <code>{mySelector.name}</code>.
</p>
<button onClick={() => myDispatch(updateProfile('Updated Name'))}>
Learn React
</button>
// Other code
Here is the code sand box for more info: https://codesandbox.io/embed/redux-basic-example-ftrvs0?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
useSelector allows you to extract data from the Redux store state, using a selector function. https://react-redux.js.org/api/hooks#useselector
In your case:
const mySelector = useSelector((store) => store)

How to execute Redux dispatch?

I have a Next.js app with Redux. Using 3 library:
Redux Toolkit
React Redux
next-redux-wrapper
After first user interaction I would store data in redux, so I call:
useAppDispatch(setInvoiceItems(itemsWithSelection2));
and it raise an error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at Object.throwInvalidHookError (react-dom.development.js?61bb:14906)
at useContext (react.development.js?72d0:1504)
at useReduxContext (useReduxContext.js?9825:21)
This is the whole method inside the function component:
const switchSelection = (key) => {
let itemsWithSelection2;
if (itemsWithSelection) {
itemsWithSelection2 = { ...itemsWithSelection };
} else {
itemsWithSelection2 = Object.fromEntries(
Object.keys(invoiceItemsFiltered)
.filter((key) => invoiceItems[key].defaultValue != undefined)
.map((key) => [key, invoiceItems[key].defaultValue])
);
}
itemsWithSelection2[key] = itemsWithSelection2[key] == 1 ? 0 : 1;
setItemsWithSelection(itemsWithSelection2);
useAppDispatch(setInvoiceItems(itemsWithSelection2));
};
What is wrong in my code?
I store StartPaymentIn type in redux. It has a field invoiceItems, string number pairs.
import { Action, createSlice, PayloadAction } from "#reduxjs/toolkit";
import { StartPaymentIn, InvoiceItemData } from "../../sharedDirectory/Types";
const initialState: StartPaymentIn = {
eventId: "",
hostName: "",
lang: "",
invoiceItems: {},
formFields: {},
};
const StartPaymentInSlice = createSlice({
name: "StartPaymentIn",
initialState,
reducers: {
setInvoiceItems(state, action: PayloadAction<{ InvoiceItemData? }>) {
state.invoiceItems = action.payload;
},
},
});
export const { setInvoiceItems } = StartPaymentInSlice.actions;
export default StartPaymentInSlice.reducer;
export type StartPaymentIn = {
invoiceItems?: InvoiceItemDataOnBuyTicket;
};
export type InvoiceItemDataOnBuyTicket = {
[invoiceItemId: string]: number;
};
const InvoiceItemsToDeliver = (props: ProductProps) => {
let itemsWithSelection2 = useAppSelector((state) => state.invoiceItems);
if (invoiceItems) {
if (!itemsWithSelection2) {
useAppDispatch(setInvoiceItems(invoiceItems2));
}
}
const [itemsWithSelection, setItemsWithSelection] = useState<{
[formFieldId: string]: number;
}>(itemsWithSelection2);
React hook "useAppDispatch" cannot be used inside pure functions. They can only be called inside react component. In your code you are using the useAppDispatch(setInvoiceItems(itemsWithSelection2)); inside a pure function outside of the component.

Resources