How to migrate app state from React Context to Redux Tool Kit? - reactjs

What I'm trying to achieve:
I have a NextJS + Shopify storefront API application. Initially I set up a Context api for the state management but it's not that efficient because it re-renders everything what's wrapped in it. Thus, I'm moving all state to the Redux Toolkit.
Redux logic is pretty complex and I don't know all the pitfalls yet. But so far I encounter couple problems. For example in my old Context API structure I have couple functions that take a couple arguments:
const removeFromCheckout = async (checkoutId, lineItemIdsToRemove) => {
client.checkout.removeLineItems(checkoutId, lineItemIdsToRemove).then((checkout) => {
setCheckout(checkout);
localStorage.setItem('checkout', checkoutId);
});
}
const updateLineItem = async (item, quantity) => {
const checkoutId = checkout.id;
const lineItemsToUpdate = [
{id: item.id, quantity: parseInt(quantity, 10)}
];
client.checkout.updateLineItems(checkoutId, lineItemsToUpdate).then((checkout) => {
setCheckout(checkout);
});
}
One argument (checkoutId) from the state and another one (lineItemIdsToRemove) extracted through the map() method.
Inside actual component in JSX it looks and evokes like this:
<motion.button
className="underline cursor-pointer font-extralight"
onClick={() => {removeFromCheckout(checkout.id, item.id)}}
>
How can I declare this type of functions inside createSlice({ }) ?
Because the only type of arguments reducers inside createSlice can take are (state, action).
And also is it possible to have several useSelector() calls inside one file?
I have two different 'Slice' files imported to the component:
const {toggle} = useSelector((state) => state.toggle);
const {checkout} = useSelector((state) => state.checkout);
and only the {checkout} gives me this error:
TypeError: Cannot destructure property 'checkout' of 'Object(...)(...)' as it is undefined.
Thank you for you're attention, hope someone can shad the light on this one.

You can use the prepare notation for that:
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (id: number, text: string) => {
return { payload: { id, text } }
},
},
},
})
dispatch(todosSlice.actions.addTodo(5, "test"))
But 99% of the cases you would probably stay with the one-parameter notation and just pass an object as payload, like
dispatch(todosSlice.actions.addTodo({ id: 5, text: "test"}))
as that just works out of the box without the prepare notation and makes your code more readable anyways.

Related

Is it safe to update redux state outside the slice (in a reusable function)?

Is it safe to update redux state outside the slice? So basically to have something like this in your code:
export const dataSlice = createSlice({
name: "data",
initialState: initState,
reducers: {
...
updateData(state, {payload}: PayloadAction<{ id: number; newData: TData }>) => {
setData(state, payload)
}
...
}
extraReducers: (builder) => {
...
.addMatcher(api.endpoints.getSomeData.matchFulfilled, (state, {meta, payload}) => {
// some formatting of the data from the request
...
setData(state, {id, newData})
}
...
}
})
// end of dataSlice
const setData = (
state: WritableDraft<WritableDraft<IFiltersSlice>>,
{ id, newData }: { id: number; newData: TData }
) => {
const idIndex = state.dataArr.findIndex((data) => data.id === id)
if (idIndex !== -1) {
if (newData.length === 2 && typeof newData[0] === "number" && typeof newData[1] === "number")
state.dataArr[idIndex].value = newData
state.dataArr[filterIndex].bounds = newData
}
}
It looks a little sketchy to me, but I tried it, and it seems to work.
Another way to do it would be to return the state from setData and, in the slice, just return the result once more where the function is called. But, since redux toolkit uses Immer, I think this should also be fine?
Basically I found this in code, and I was wondering if it is bad (and if yes, why).
Also should we keep an eye out for this in the future?
I couldn't find anything related to this online (or in the docs). Maybe I just missed it?
Anyway any opinions are welcomed in the comments, and any good answer it up is deeply appreciated.
Thank you very much!
It does seem sketchy to me. I use the reducers array in Redux Toolkit’s createSlice for this kind of thing.
https://redux-toolkit.js.org/api/createSlice

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.

How to create reusable redux-module?

For example, my app contains the two list: colors & my favorite colors. How to create the re-usable filter-module for this two lists?
The problem is the actions in redux commiting into global scope, so filter-reducer for colors and filter-reducer for favorite colors reacting to the same actions.
I try something like high-order functions that receive the module-name and returned new function (reducer) where classic switch contain module + action.type.
But how make scoped actions or scoped selectors? Best-practise?
Maybe Redux-Toolkit can solve this problem?
High-order reducer
High-order action
High-order selector
Well described here
Name your function like createFilter and add a parameter like domain or scope
after this in switch check it e.g
export const createFilters = (domain, initState) => {
return (state = initState, { type, payload }) => {
switch (type) {
case: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`:
...
and for actions create something like this
const createFilterActions = (domain: string) => {
return {
addFilter: (keys: string[]) => {
return {
type: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`,
payload: keys
}
},
updateFilter: (key: string, state: any) => {
return {
type: `${domain}/${FILTER_ACTIONS.FILTER_UPDATED}`,
payload: { key, state }
}
},
}
}

Comment on the validity of this redux simplify

I have some coding experience with React & Redux one and half year ago. And then I have been using Vue & Vuex in this year. I like the simplicity of the Vuex structure. So when I return to React & Redux. I want to try some way to make Redux code structure become more simplify.
I have try to define the State and Action using a single object(like Vuex). Then use reduxGenerator function to generate 'reducer' and 'action creator'.
But I didn't think deeply about this. I always worried about this method will cause some problems.
I know that Redux splits the 'state', 'reducer', 'action type' and 'action creator' to make the structure clearer. The team can work together to write different part of the entire state. But more often in my team. Everyone is assigned a different module. One developer needs to face all 'state', 'reducer', 'action type' and 'action creator' code in multiple place. And the Developer need to manually write some 'mechanized' code. But why developers still maintain this initial Redux code structure all the time. Is my method causing some serious problems?
Example code:
https://codesandbox.io/s/redux-simplify-v07n9
Redux Object
const lists = {
naming: "lists",
initState: [
{
id: idGenerator(),
name: "List 1"
},
{
id: idGenerator(),
name: "List 2"
}
],
actions: {
addList(state) {
return [
...state,
{
id: idGenerator(),
name: "New List"
}
];
},
// ....
}
}
export default reduxGenerator(lists);
reduxGenerator
export default ({ naming, initState, actions }) => {
const redux = {};
redux.actionCreator = Object.fromEntries(
Object.entries(actions).map(action => [
action[0],
args => ({
type: `${naming}/${action[0]}`,
args
})
])
);
redux.reducer = (state = initState, action) => {
console.log(`%c action: ${action.type}`, "color: #00BEBE");
const actionType = /.+\/.+/.test(action.type)
? action.type.split("/")[1]
: action.type;
if (Object.keys(actions).includes(actionType)) {
return actions[actionType](state, action.args);
}
console.error(`${action.type} not found!`);
return state;
};
return redux;
}
So I hope to discuss the rationality of this. To help me jump out of my personal limitations.
This may not be a general question in stackoverflow. Please forgive me.

Passing props from Container to mapDispatchToProps

I'm trying to access a Container's props (that were passed in from Redux state) from within mapDispatchToProps (mDTP). My container is responsible for dispatching an action to fetch data, but the action that it dispatches needs access to a piece of redux state. That state will be used as a field in the HTTP header.
The redux state that I am trying to access is languages. I want to then pass it in as a header to mDTP
Here is my container:
const ExperienceListContainer = ({ list, loading, failed, readList, languages }) => {
useEffect(() => {
readList()
}, [])
const apiRequestParams = {
endpoint: 'GetToursByCity',
count: 10,
step: 10,
cityid: 'e2e12bfe-15ae-4382-87f4-88f2b8afd27a',
startdate: '2018-12-20 8:30:00',
enddate: '2018-12-20 22:15:00',
sort_by: 'rating',
guests: 10,
min_price: 1,
max_price: 100.50,
time_of_day: 1,
sort_by: 'rating',
categories: '',
languages: languages
}
return <ExperienceList {...{ list, loading, failed, languages }} />
}
ExperienceListContainer.propTypes = {
list: T.arrayOf(T.object).isRequired,
limit: T.number,
loading: T.bool,
failed: T.bool,
readList: T.func.isRequired,
filter: T.shape({
interests: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
languages: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
}),
}
const mapStateToProps = state => {
return (
({
list: state.resource.experiences,
languages: state.filter.languages
})
)
}
const mapDispatchToProps = (dispatch) => ({
readList: () => {
return (
dispatch(resourceListReadRequest('experiences', { apiRequestParams }))
)
}
})
export default connect(mapStateToProps, mapDispatchToProps)(ExperienceListContainer)
The problem with this current code is that mDTP cannot access apiRequestParams. It is undefined as far as it is concerned.
Based on my research, the answers that pop up are to use mergeProps (as an arg passed into connect()) and ownProps (a param included with mapDispatchToProps), but neither of these seem to be working, at least in the way I'm implementing them. Is there a better way to accomplish what I'm trying to do?
You have to pass them to readList as a parameter to access them like this:
readList(apiParams)
And use them like this:
readList: apiRequestParams => {
return (
dispatch(resourceListReadRequest('experiences', {
apiRequestParams }))
)
This is because apiRequestParams is local to the container itself and cannot be access outside of the component.
You would also have to declare it above the useEffect and pass it as parameter to the useEffect so that it gets evaluates, if the language changes for example and keep the reference with useRef or declare it within the useEffect so that you don't cause an infinity loop.
Hope that helps. Happy coding.

Resources