Managing multiple states inside hook - reactjs

I've recently started to try to learn React hooks, but for the life of me I can't figure out some things, like multiple state management, I've found a few example but nothing seems to be in a specific pattern. I'm not even sure how you're supposed to define your states if there's more than 2 and you want to change them individually, should it be like this:
const [slides, setSlides] = useState([])
const [currentSlide, setCurrentSlide] = useState(0)
const [tagName, setTagName] = useState([])
Or like this:
const [states, setStates] = useState({
slides: [],
currentSlide: 0,
tagName: []
})
And if both or the second one is viable (honestly I would prefer the second one since its less repetitive in calling useState and closer to the standard state meta) how could one go about changing states in such a example? I couldn't really find anything on this, so what I tried was this:
useEffect(() => {
Object.entries(Slides).forEach(slide => {
setStates(prevState => ({ slides: [...prevState.slides, slide[1]] }))
})
}, [])
And I messed around with it a bunch trying to get some decent results but I couldn't get it to work properly.
Any ideas of what I'm doing wrong? And on which one of these to methods of best practice?
Thanks!

In terms of updating state, first template is much easy and simple, because each stateUpdate function will be responsible for updating single value, that can be obj/array/int/bool/string. Another thing is, to access each value of the state (it will be an object), you need to write states.slides, states.tagName etc.
With first case, state update will be simply:
// only value in case of int/string/bool
updateState({ [key]: value }) or updateState(value);
But in second case, state will be an object with multiple keys, so to update any single value you need to copy the object, then pass the updated key-value. Like this:
updateState({
...obj,
[key]: newValue
})
To update the slides array:
updateState(prevState => ({
...prevState,
slides: newArray
}))
Handling complex state update:
Use useReducer to handle the complex state update, write a separate reducer function with action types and for each action type so the calculation and return the new state.
For Example:
const initialState = { slides: [], tagName: [], currentSlide: 0 };
function reducer(state, action) {
switch (action.type) {
case 'SLIDES':
return { ... };
case 'TAGNAME':
return { ... };
case 'CURRENT_SLIDE':
return { ... }
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
....
}

It is true that the useState hook can become quite verbose when dealing with complex state object.
In this case, you can also consider using the useReducer hook.
I would recommend the reading of this article about useState vs useReducer.

Related

Redux dispatch does not update state array with new payload object

I decided to add Redux to my pet project (surprise, todolist).
Here's add entry function:
const [todoEntry, setTodoEntry] = useState('');
const addNewEntry = (e) => {
e.preventDefault();
// console.log(todoEntry);
dispatch({
type: ADD_TODO,
payload: {
prodName: todoEntry,
done: false,
favorite: false,
edit: false,
id: uuid()
}
})
setTodoEntry('');
todoEntry comes from another component like that:
<input
id='standartInput'
style={{minWidth: '250px'}}
value={todoEntry}
onChange={e => setTodoEntry(e.target.value)}
type='text'
placeholder='Add new entry (max 55 symbols)' />
Also, I use some hooks to manage my state:
const myTodoItems = useSelector((state) => state.todos[0])
const dispatch = useDispatch()
const [data, setData] = useState(myTodoItems);
And, finally, the reducer:
import { todolist } from "./todolist"
import { ADD_TODO } from '../Store/todoactions'
export const todoReducer = (state = [todolist], action) => {
switch (action.type) {
case ADD_TODO: {
const newItem = action.payload
console.log(newItem)
console.log(todolist)
return ([...todolist, newItem])
}
default:
{ return state }
}
}
The issue is:
todolist exists, I can see at browser console
newItem exists too, I also can see at browser console
BUT! When clicking on 'Add' button, state is not updated.
What I'm doing wrong?
Thanks.
const myTodoItems = useSelector((state) => state.todos[0])
You seem to be selecting only the first item, so it's not surprising that you don't see the rest.
const [data, setData] = useState(myTodoItems);
This looks like an antipattern, why do you need a state variable for something that is already tracked by Redux?
You should also use Redux Toolkit, it is not recommended to use Redux directly.
Edit:
Thank you for the codesandbox, now the problem is clear.
You are using both Redux and React state to deal with the same data, for instance you add todos through Redux but complete them with React state.
A very important principle in React/Redux is to have a single source of truth, but in your case you have two sources of truth for the todos: the Redux store and the useState hook. You use the React state for rendering and initialize it with the Redux state, but then you don't update it when a todo is added, so the UI shows outdated information.
It's fine to use sometimes Redux, sometimes useState as long as it is for independent pieces of data, but for the same data, you need to choose.
Remember that everytime you use useState(initialState) you create a new state variable/source of truth, which will become different from the initial state. Sometimes this is exactly what you want, but not here.
So I would suggest to remove the useState and go through Redux for everything you want to change about the todos (edit them, complete them, and so on).
There are some things you can have as React state (for instance the boolean "are we currently editing this specific todo item"), but then it would be much easier to have a simple useState(false) directly in the TodoItem component.

Is mutating "copy" of initial state inside a passed reducer function in "Redux Toolkits" createSlice() a bad practice?

Context
I'm trying to create the state for a multi-level accordion menu, where the top-level items are called subjects, each subject has multiple chapters and each chapter will have multiple articles.
At any given time only a single "Subject" can be in the "selected" state. The same applies to chapters and articles, with the additional restriction being they need to be "Children" of a parent in the "selected" state.
Code
I have a deeply nested object that is to be passed as the initial state to the createSlice() method,it has the following shape,
const initialState = {
currentArticle: null,
currentChapter: null,
currentSubject: null
subjects: [
{
id:"001",
chapters: [
{
id: "001001",
articles: [
{
id: "001001001",
selected: false
},
//....... more articles
],
selected: false
},
//....... more chapters
],
selected: false
},
//....... more subjects
]
}
Following is my createSlice() method,
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialState.subjects.map((subject) => {
if (payload.id === subject.id) {
subject.selected = true;
state.currentSubject = subject.id
}
return subject;
});
state.subjects = newNavigationState;
},
// ...... more reducer functions
},
});
The subjects array is directly used for rendering the UI, Every time a dispatch function is called I conditionally use the initial state and then calculate the next state,(those conditions are not included in the following code snippets for simplicities sake), For now let's consider that I use the initial states "subject" array every single time I need to calculate the next state instead of using the previous state passed to the reducer.
The reason for using the initial state is to not have to manually set the selected state of nested objects to false, in case the parents selected state changes.
Problem
However when I dispatch an action that executes the "setTopic" reducer function I get the following error,
TypeError: Cannot assign to read only property 'selected' of object '#<Object>'
Attempts to solve the issue
Using the spread operator to create a new copy of initialState within the reducer.
const copyInitialState = { ...initialState }
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
Using Object.assign() to create a new Object, within the reducer
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
create 2 copies of the initial state, before invoking createSlice and pass one copy inside the createSlice() invocation as the initial state and use the other copy within the passed reducer function.
const initialStateCopy = Object.assign(initialState);
const initializedInitialState = Object.assign(initialState);
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initializedInitialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateCopy.subjects.map((subject) => {
//............
I.E : I tried this approach with the spread operator as well.
The only solution that works(not a desirable approach )
explicitly declare a completely new constant and initialize it in the exact same way as the initialState object. In this case, this simply means I'm copying the exact same object creation code one after the other so that they are completely two different objects,
const initialState = {//.... deeply nested object}
const initialStateExplicitCopy = {//.... deeply nested object}
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateExplicitCopy.subjects.map((subject) => {
Question
I think this has to do something with Immer and how it treats the initial state Object. I see that even though I do an Object.assign() nested objects are sealed and frozen.
Does this mean I'm attempting to perform something wrong? or something which is considered bad practice? Does this in any way make the reducer impure? If so I don't see why because the initial state never changes, I'm just using the initial state all the time to calculate the next state.
Is there a better approach to handle this while using the redux toolkit?
Yeah, the problem is the attempt to mutate initialState, for a couple different reasons.
It's only safe to write code that "mutates" data if that data has actually been passed through Immer and wrapped in proxies, so that Immer can track the attempted changes. When you reference initialState, that object hasn't been handed to Immer yet, so your code really is trying to mutate initialState.
Fortunately, when you call createSlice({initialState: someInitialStateValue}), createSlice internally "freezes" that value to make sure you don't accidentally mutate it for real later on. That's why you're getting the error - it's telling you you are doing something wrong.
Conceptually, I'm not sure why you're trying to always base the calculations off of initialState. Wouldn't you want to be doing updates based on the current state as the starting point?
If you truly do need to use initialState as the starting point, the best option is to use Immer directly and feed it initialState. Immer's main function is exported from RTK as createNextState, so you can use that to wrap your current logic
import { createNextState } from "#reduxjs/toolkit";
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = createNextState(initialState.subjects, draftSubjects) => {
const subject = draftSubjects.find(subject => subject.id === payload.id);
if (subject) {
subject.selected = true;
state.currentSubject = subject.id
}
}
state.subjects = newNavigationState;
},
}
});

The right way to use the Hook useReducer for a complex state

Trying to catch up on the React Hooks. I'm reading that they recommend the use of the Hook useReducer when dealing with a complex state. But my doubt starts from the following scheme:
Using React + Typescript, suppose I have a state with several fields (I'll give an example with classes):
type Person = {
username: string,
email: string
}
type Pet = {
name: string,
age: number
}
this.state: MyState = {
person: Person,
pet: Pet,
loading: boolean
}
If I wanted to handle this state with a new Hooks-based approach, I could think of several options:
Option 1: using a Hook useState for each field
const [person, setPerson] = useState<Person>(null)
const [pet, setPet] = useState<Pet>(null)
const [loading, setLoading] = useState<boolean>(false)
This method has the disadvantage of low scalability and some of my real states have at least 15 fields, is unmanageable.
Option 2: Using a single setState for the entire object
const [state, setState] = useState<MyState>({
person: null,
pet: null,
loading: false
})
This is the method that seems simplest to me, where I can simply do setState((prev) => {...prev, person: {username: 'New', email: 'example#gmail.com'}}) or adapt it to any field modification. I can even update several fields at once.
Option 3: use a useReducer for each of the complex fields by passing a specific reducer for each one, use useState for the simple ones
const [person, dispatchPerson] = useReducer<Person>(personReducer)
const [pet, dispatchPet] = useReducer<Pet>(petReducer)
const [loading, setLoading] = useState<boolean>(false)
I find this one manageable, but I don't see the point of having to set up a reduce function with a multi-line switch, in addition to the tedious process of setting the dispatching types in Typescript for each reduce function when you could just use setState and be done with it.
Option 4: use one useReducer for the entire state
const [state, dispatch] = useReducer(generalReducer)
The main problem with this is the type of the reducer, think of 15 fields, where all the types and the structure of the information to update them are different. Specifying the types in Typescript does not scale or is unclear. There are several articles about this and none of them solve the problem in a clean way (example 1), or they are extremely simple and don't apply to the problem (example 2).
What would be the best way to handle this type of cases? Where the number of fields in the state is large and can have several levels of depth. Are there good practices or any official examples that represent these cases? The examples with a number field to handle a simple counter that bloggers or official documentation people like so much are not being very helpful.
Any light on the subject would be more than welcome! Thanks in advance and sorry about my English
I think your observations are spot on.
I think you should use Option 1 for simple state (e.g. you have only a few items to keep track of), and Option 2 for complex state (lots of items, or nested items).
Options 1 and 2 are also the most readable and declarative.
Option #2 is talked about here: https://reactjs.org/docs/hooks-faq.html#should-i-use-one-or-many-state-variables
useReducer is more useful when you have multiple types of actions. If you're just updating state (one type of action), then actions are overkill. Also, useReducer is useful if you're performing calculations or transformations based on previous state (not just replacing part of state). If you're familiar with Redux, useReducer is a simplified version of Redux principles.
We recently dealt with similar situation using custom hook because reducer became too unpredictable. Idea was to create our state in custom hook,
then we created typesafe helpers operating on state, and then we exposed state and helpers.
interface State{
count: number;
}
interface ExportType{
state: State;
add: (arg: number)=>void;
subtract: (arg: number)=>void;
}
export default function useAddRemove(): ExportType {
const [state, setState] = useState<State>({
count: 0
})
function add(arg:number){
setState(state=>({...state, count: state.count+arg}))
}
function subtract(arg:number){
setState(state=>({...state, count: state.count-arg}))
}
return {
state,
add,
subtract,
}
}
Please let me know if you have any suggestions.
I would typically go for either option 2 or option 4 for a lot of state. Option 2 is fine if your data is easily updated, isn't nested, and doesn't have interdependence between the fields.
Option 4 is great because you can get a lot of more complicated behavior easily. I.e. updating fetching and error when you set the data for an asynchronous fetch operation. It is also great because you can pass the dispatch function down to child components for them to use to update the state.
Here's an example I put together using redux toolkit to strongly type a reducer that uses combineReducers for use in useReducer.
https://codesandbox.io/s/redux-toolkit-with-react-usereducer-2pk6g?file=/src/App.tsx
const [state, dispatch] = useReducer<Reducer<ReducerType>>(reducer, {
slice1: initialState1,
slice2: initialState2
});
const initialState1: { a: number; b: string } = { a: 0, b: "" };
const slice1 = createSlice({
name: "slice1",
initialState: initialState1,
reducers: {
updateA(state, action: PayloadAction<number>) {
state.a += action.payload;
},
updateB(state, action: PayloadAction<string>) {
state.b = action.payload;
}
}
});
const initialState2: { c: number; d: number } = { c: 0, d: 0 };
const slice2 = createSlice({
name: "slice2",
initialState: initialState2,
reducers: {
updateC(state, action: PayloadAction<number>) {
state.c += action.payload;
},
updateD(state, action: PayloadAction<number>) {
state.d += action.payload;
},
updateCDD(state, action: PayloadAction<number>) {
state.c += action.payload;
state.d += action.payload * 2;
}
}
});
const reducer = combineReducers({
slice1: slice1.reducer,
slice2: slice2.reducer
});
type ReducerType = ReturnType<typeof reducer>;

How to update a specific property of an initial state object using useState

Consider that code where I need to update the state after some data has been fetched, I just need to update the data portion of the initial state (clientData):
const [clientData, setClientData] = useState({
data: {},
product: 'profile'
});
useEffect(() => {
getProducts(1).then(res =>
setClientData({ ...clientData, data: res.data })
);
}, []);
How can I and is it possible to update just the "data" portion of the initial state without having to spread it (...clientData)
This is not possible, seeing that the set method returned from the useState hook replaces the prior state value with what ever you pass it, in it's entirety.
One possiblity would be to separate your state with multiple hooks which would give you a more granular means of updating your component's state:
/* Separate your state across multiple hooks */
const [data, setData] = useState({});
const [product, setProduct] = useState('profile');
useEffect(() => {
getProducts(1).then(res =>
/* Only update data portion of state via setData hook */
setData(res.data)
);
}, []);
Here's a couple of options...
1) Separate the following...
const [clientData, setClientData] = useState({
data: {},
product: 'profile'
});
into...
const [clientData, setClientData] = useState({});
const [clientProduct, setClientProduct] = useState('profile');
and then you can update the portion of the data that you need to. This is probably recommended in the case you've listed above. Or...
2) use a library like 'immer' which allows you to create a new state tree by simply modifying the existing one...
import produce from 'immer';
...
const [clientData, setClientData] = useState({
data: {},
product: 'profile'
});
useEffect(() => {
getProducts(1).then(res =>
setClientData(produce(clientData, draft => {
draft.data = res.data;
}));
);
}, []);
Immer is a fantastic library and you can learn more about it here
This is not possible for useState. It's possible with useReducer though.
As previously mentioned, it is best practice to split this state between multiple hooks, however, it is actually possible to do this with useState() using a function like this:
const setClientDataSelectively = ({data=null, product=null}) => {
setClientData((prevState) => ({
data: data !== null ? data : data,
product: product !== null ? product : product,
}));
}
The parameters are defaulted to null, then using the prevState object and a conditional operator, you can check if something was actually passed in and attribute accordingly.

How can I update few useState just for one render?

I have a little question about re-render with useState.
For example, I have five useState methods:
setFirstState(someValue);
setSecondState(someValueSecond);
setThirdState(someValueThird);
...
And I want for this all just one render with react hooks. Can you explane me or maybe some example?
You can combine it with useEffect with empty deps.
import { useState, useEffect } from 'react';
const MyComp = () => {
const [val, setVal] = useState('initialVal');
useEffect(() => {
setVal('newVal');
}, []);
// --^ - this is an empty dependency list
return <div>Hello</div>;
};
You can combine all those into one state.
setCombinedState({
first: someValue,
second: someValueSecond,
third: someValueThird,
});
:) Hope that will solve your problem.
You would like to use useReducer instead, and initiate it with an object with your (first, second, third...) properties, and then modify the one you want each time.
Declare your initial values
const initialState = {
someValue: 'someValue',
someValueSecond: 'someValueSecond',
someValueThird: 'someValueThird',
}
set it
const [val, setVal] = React.useReducer(initalState);
And modify then whenever you need it.
setVal({someValue: 'anotherValue'});
setVal({someValueSecond: 'anotherValue'});
setVal({someValueThird: 'anotherValue'});
Usually, when you want to change multiple states at the same time, it usually means that these states are couples in some manner.
Example
Take for instance this case: You want to fetch some data from an API, so you would have the following states:
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const [data, setData] = useState(undefined)
Usually when one of these states changes, the others do as well.
This is a clear case where you should use useReducer.
For instance, like this:
const reducer = (state, action) => {
switch (action.type) {
case 'error':
return {error: action.payload.error, loading: false };
case 'fetched':
return {data: action.payload.data, loading: false };
case 'loading':
return {loading: true};
default:
throw new Error();
}
}
And after this, you could make a custom hook to manage these states:
const {loading, error, data, dispatch} = useFetchingData()
To sum up
This is just an example, but I would recommend you to take a look at your case, and see if these states you are talking about are somehow connected, and could be better arranged in this manner.
Reducers need more code that the one I posted here. I just wrote the reducer function. If you want to know more about how they work, I recommend you to read the docs.
If you want to update your question with the actual case you are facing right now, I could help you further.

Resources