Advise about the reduction of the amount of React useState hooks - reactjs

I am trying to build my very own first project that is relatively large, at least for my level of experience anyway.
I am heavily relying on useContext in combination with useStates hooks to handle the logic between my different components, as time goes, it's really starting to get difficult to keep track of all these different states changes and simple onClick events I have to change the logic of a large number of states.
Hoping for a little bit of personal advice that could steer me in the right direction.
as somehow what I do, doesn't feel normal, or this is the reality of React?
Surely there are more clever ways to reduce the amount of state logic management?
Here are a few snippets of code I am using
const onClick = (note: INote) => {
SetAddNote(false);
SetNote(note);
onSelected(note)
SetReadOnly(true);
SetEditor(note.data.value);
SetInputValue(note.data.name);
SetCategory(note.data.category);
};
const { note, noteDispatch, SetNoteDispatch } = useContext(NoteContext);
const { categories } = useContext(CategoriesContext);
const [ editMode, setEditMode ] = useState(false);
const [ module, setModule ] = useState<{}>(modulesReadOnly)
const [inputValue, setInputValue] = useState<string>('');
const [category, setCategory] = useState('');
const [color, setColor] = useState('');
import React, { createContext, useState } from 'react';
type EditorContextType = {
editor: string;
SetEditor: React.Dispatch<React.SetStateAction<string>>;
readOnly: boolean;
SetReadOnly: React.Dispatch<React.SetStateAction<boolean>>;
inputValue: string;
SetInputValue: React.Dispatch<React.SetStateAction<string>>;
category: string;
SetCategory: React.Dispatch<React.SetStateAction<string>>;
};
type EditorContextProviderProps = {
children: React.ReactNode;
};
export const EditorContext = createContext({} as EditorContextType);
export const EditorContextProvider = ({
children,
}: EditorContextProviderProps) => {
const [editor, SetEditor] = useState('');
const [readOnly, SetReadOnly] = useState(false)
const [inputValue, SetInputValue] = useState('');
const [category, SetCategory] = useState('');
return (
<EditorContext.Provider value={{ editor, SetEditor, readOnly, SetReadOnly, inputValue, SetInputValue, category, SetCategory }}>
{children}
</EditorContext.Provider>
);
};
Sure I could shave a few states and merge them into one, but seems like that would get even more complex than it is.
I am reading about the useReducer hook however it's difficult to grasp the entire idea behind it yet and not quite sure if it's really going to help me in this case.
It feels to me I am setting myself for a failure given I continue working in this fashion, but I don't see any better options to implement

I have working on big project too, and as you say in your question, Reducer will help you to fix your issue, but surly you need to be careful how you will build and manage your state, so the idea how you manage a state, so before I put my answer, I will write some important note:
Make sure to reduce nested context as you can, only build context and use context when theres a needed for that, this will optomize your work
For handling or merge state, you can use object, arrays and normal variable, but keep in your mind, try to prevent nested level of objects, to keep state update on state change.
Use reducer to handling update on state will give you a nice ability
We can do some tricks to improve performance like set condition in reducer which its check old state and new state.
Keep in your mind, really its easy to use it, but the first time its hard to learn...
Now lets start from a real project example:
// create VirtualClass context
export const JitsiContext = React.createContext();
// General Action
const SET_IS_SHARED_SCREEN = 'SET_IS_SHARED_SCREEN';
const SET_ERROR_TRACK_FOR_DEVICE = 'SET_ERROR_TRACK_FOR_DEVICE';
const UPDATE_PARTICIPANTS_INFO = 'UPDATE_PARTICIPANTS_INFO';
const UPDATE_LOCAL_PARTICIPANTS_INFO = 'UPDATE_LOCAL_PARTICIPANTS_INFO';
// Initial VirtualClass Data
const initialState = {
errorTrackForDevice: 0,
participantsInfo: [],
remoteParticipantsInfo: [],
localParticipantInfo: {}
};
// Global Reducer for handling state
const Reducer = (jitsiState = initialState, action) => {
switch (action.type) {
case UPDATE_PARTICIPANTS_INFO:// Update particpants info and remote list
if (arraysAreEqual(action.payload, jitsiState.remoteParticipantsInfo)) {
return jitsiState;
}
return {
...jitsiState,
participantsInfo: [jitsiState.localParticipantInfo, ...action.payload],
remoteParticipantsInfo: action.payload,
};
case UPDATE_LOCAL_PARTICIPANTS_INFO:// Update particpants info and local one
if (JSON.stringify(action.payload) === JSON.stringify(jitsiState.localParticipantInfo)) {
return jitsiState;
}
return {
...jitsiState,
localParticipantInfo: action.payload,
participantsInfo: [action.payload, ...jitsiState.remoteParticipantsInfo],
};
case SET_IS_SHARED_SCREEN:
if (action.payload === jitsiState.isSharedScreen) {
return jitsiState;
}
return {
...jitsiState,
isSharedScreen: action.payload,
};
default:
throw new Error(`action: ${action.type} not supported in VirtualClass Context`);
}
};
const JitsiProvider = ({children}) => {
const [jitsiState, dispatch] = useReducer(Reducer, initialState);
// Update shared screen flag
const setIsSharedScreen = useCallback((flag) => {
dispatch({type: SET_IS_SHARED_SCREEN, payload: flag})
}, []);
// Update list of erros
const setErrorTrackForDevice = useCallback((value) => {
dispatch({type: SET_ERROR_TRACK_FOR_DEVICE, payload: value})
}, []);
// Local Participant info
const updateLocalParticipantsInfo = useCallback((value) => {
dispatch({type: UPDATE_LOCAL_PARTICIPANTS_INFO, payload: value})
}, []);
const updateParticipantsInfo = useCallback(async (room, currentUserId = null) => {
if (!room.current) {
return;
}
// get current paricipants in room
let payloads = await room.current.getParticipants();
// ... some logic
let finalResult = payloads.filter(n => n).sort((a, b) => (b.startedAt - a.startedAt));
dispatch({type: UPDATE_PARTICIPANTS_INFO, payload: finalResult})
}, []);
const contextValue = useMemo(() => {
return {
jitsiState,
setIsSharedScreen,
setErrorTrackForDevice,
updateParticipantsInfo,
updateLocalParticipantsInfo,
};
}, [jitsiState]);
return (
<JitsiContext.Provider
value={contextValue}
>
{children}
</JitsiContext.Provider>
);
};
export default JitsiProvider;
This example allow you to update state and you have more than one case, all state value share by jitsiState, so you can get any data you want, and about function, you can use dispatch direct! but in our experience we build a callback method and send it via provider too, this give us upillty to control code and logic in one place, and make process very easy, so when click in every place just we call needed method...
You will see also conditions and useMemo...these to prevent render un-needed trigger, like change the key in memory not the real value and so on...
Finally after we use it, we control now all the state between all component too easy, and we didn't have nested context except the wrapper context.
Note: surly you can skip or change this code base on your logic or needed concepts.
Note 2: this code is catted and do some changes to make it easy to read or understand...
Note 3: You can ignore all functions pass in provider and use dispatch direct, but in my project I send a function like this example.

Related

Combine useMachine with useContext

I'm working on a UI project which handles state updates through a shared context, very similar as described here
const {appState, dispatch} = useContext(AppContext);
I'm not toying around with state machines through xstate for some of the components.
const SomeComponent = () => {
const {appState, dispatch} = useContext(AppContext);
const [state,send,service] = useMachine(MyComponentStateMachine);
}
Now, ideally I would like my state machine to dispatch certain events when entering a state. What's the best way for the state machine to get a hold of my AppContext, though?
At the moment, I'm handling this event dispatching on the component itself, observing the state of the state machine and dispatching an event as needed when it enters a certain state:
const SomeComponent = () => {
const {appState, dispatch} = useContext(AppContext);
const [state,send,service] = useMachine(MyComponentStateMachine);
useEffect(() => service.subscribe(state => {
if(state.value == "Some relevant state of MyComponentStateMachine")
dispatch({type: SomeEvent, arg: 12345});
}).unsubscribe, [service]);
}
This works well, but it strikes me as bad design. I'd think it would be cleaner to dispatch this event from the state machine directly, rather than from the component.
Is there any good way for the state machine to get a hold of AppContext?
Would it be sensible to simply create a factory method for the state machine which takes dispatch as an argument, and holds on to it?
I believe there's nothing wrong to call your dispatch function there. Due that you are using context, you wouldn't be able to call the dispatch inside the machine, unless you pass the function as a parameter. You can try that, not sure it that would work.
(You can use actions to trigger side effects on events or state changes)
In that case it would be something like this:
<pre>
//Component
const SomeComponent = () => {
const { appState, dispatch } = useContext(AppContext);
const [ state, send ] = useMachine(MyComponentStateMachine);
useEffect(() => {
if(state.matches("stateName")) {
const data = { type: SomeEvent, arg: 12345 };
send("EVENT_NAME", { callback: dispatch, data })
}
}, [state]);
}
//Machine
const MyComponentStateMachine = Machine({
...
states: {
stateName: {
on: {
EVENT_NAME: "secondState",
},
},
secondState: {
entry: ["actionName"]
}
},
{
actions: {
actionName: (ctx, e) => {
e.callback(e.data);
},
}
}
});
</pre>
*** Also look how I compare the state, that is a cleaner way to read the state value. Also you don't need to subscribe to the service if you are already using the useMachine hook, the hook will trigger a component rerender if the state changes.

Make a second API call and return data

I need some help figuring out an issue.
I'm working on a web app and basically I make an API call which is an array of data to get someone's details and return them on the page, but for one param (editedBy) I get the person' id. So, I was asked to make another API call from where I can get that person's name.
import React from 'react';
import {getData} from '.api/calls';
import { connect } from "react-redux";
import {getEditedBy} from './api/calls';
const PersonDetails = ({userId}) => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [editedBy, setEditedBy] = useState('')
const setDetails = user => {
setFirstName(user.firstName);
setLastName(user.lastName);
}
useEffect(() => {
getData(userId).then(res => {
setDetails(res.data)
});
getEditedBy(userId).then(res => {
setEditedBy(res.fullName)
});
}, [userId])
return (
<div>
<p>First Name: {firstName}</p>
<p>LastName: {lastName}</p>
<p>Edited By: {editedBy}</p>
</div>
)
}
export default connect(
state => ({
userId: state.user.userId
})
)(PersonDetails);
How can I return the person's full name and keep that value in place, instead of the id? Any help is appreciated.
This should cover your case:
useEffect(() => {
async function getUserDetailsAsync() {
const userData = (await getData(userId)).data;
const editedBy = (await getEditedBy(userData.editedBy)).data;
setDetails(userData);
setEditedBy(editedBy.fullName);
}
getUserDetailsAsync();
}, [userId]);
If you're not a fan of the await syntax for some reason you can go with the .then chaining but in any case you should make the second API call dependent on the first one.
useEffect(() => {
getData(userId).then(res => {
const userData = res.data;
getEditedBy(userData.editedBy).then(res => {
const editedBy = res.data;
setDetails(userData);
setEditedBy(editedBy.fullName);
});
});
}, [userId]);
In your current solution, you're firing 2 requests simultaneously which will cause completely nondeterministic result for you. First of all, you're querying for the fullName not of the person who edited the post, but for the same person you just queried(if I understood your context correctly). Second, your state will be updated at different times due to the independent requests. Third - one of the requests may fail while the other succeed, thus leaving your component with inconsistent state. Furthermore, you might have problems with trying to update an already unmounted component and you should have this in mind.

how to prevent re-render if fetch from many sources in react hooks?

I'm using react hooks in React Native.
My problem is that the function of useState which to initialize state makes re-render.
So if I set state like below
const [A, setA] = useState(false);
const [B, setB] = useState(false);
const [C, setA] = useState(false);
// ...
const testFunc = () => {
setA(true);
setB(true);
setC(true);
}
EDITED
I think examples were wrong.
Here's another example.
const useFetch(coords) {
const [example, setExample] = useState([])
const [checker, setChecker] = useState(false);
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(){
setExample(res.data());
setChecker(true);
}
}
useEffect(() => {
fetchData();
}, [coords])
return example;
}
const useLocation = () => {
...
return coords;
}
const App = () => {
const coords = useLocation();
const example = useFetch(coords); // example is undefined.
const [data, setData] = useState(example); // data is undefined.
}
It causes many re-render as many as I use the set function.
Is this natural thing?
If I don't want to make this re-render, can't use the set function multiple times?
You can not do it in straightforward way. I will suggest you the two solutions for it.
Solution 1: Combine states in one object.
const [value, setValue] = useState({A: false, B: false, C: false});
// ...
const testFunc = () => {
setValue({A: true, B: true, C: true});
}
Solution 2: Another solution is useReducer.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{A: false, B: false, C: false}
);
// ...
const testFunc = () => {
setState({A: true, B: true, C: true});
}
Here I have implemented your another example: https://stackblitz.com/edit/react-usestate-wcjshg
Hope this will help for you!
React does not batch state updates if they are triggered outside React-based event. That means, if you want your state updates to be batched you need to wrap it on an event handle such as onClick.
If your local component state is non-trival and/or using an event handler is not an option, I'd recommend you to use useReducer as you can batch your state updates within that.
This appears to be normal React behavior. It works the exact same way if you were to call setState() in a class component multiple times.
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().
I think there's plans long-term to always batch events, but not sure on the details
Sources:
https://github.com/facebook/react/issues/14259#issuecomment-439632622
https://github.com/facebook/react/issues/14259#issuecomment-468937068
As stated in the other answers, React does not batch state updates if they are triggered outside React-based events (in then for example), one of the solutions is to merge your state in one object and call setState one time. But if you like to keep your state separated, the solution is to use ReactDOM.unstable_batchedUpdates like this :
const fetchData = () => {
axios.fetch(`url+${coords.latitue}+${coords.longitude}`).then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setExample(res.data());
setChecker(true);
});
});
}
Recommended by Dan Abramov here

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.

Managing multiple states inside hook

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.

Resources