EDIT: below the line is the initial problem where I asked if my whole architecture was fine. I later edited the topic (and title) to go straight to what was my issue in order to help the community. Therefore, you might find what you want by jumping directly to the answer while skipping the main post
I am new to react and I am encountering issues back to back. I suspect that something is wrong in my pattern and would really love to have it criticized by someone a bit stronger.
Component A:
It renders (among others) a Component B
Component A has a useState hook on an array of C_obj it renders, called C_obj_arr
const [C_obj_arr, ASetter] = useState<C_obj[]>()
It provides this C_obj_arr and ASetter to Component B as properties to allow the data to go back up.
Component B
It renders each C_obj of the C_obj_arr in a list of Component C.
It has a useReducer hook that controls the states of C_obj_arr
const [C_obj_array, dispatch] = useReducer(reducer, props.C_obj_array);
It has a useEffect hook such that if C_obj_arr changes, data goes back up to Compoennt A
useEffect(() => {
ASetter(C_obj_array);
}, [C_obj_array, ASetter]);
Question: Is it fine so far to use the prop like this to init the reducer?
it also uses a useCallback hook to wrap a function that will allow getting the data back from Component C
const fn_callback = useCallback(
(c: C_obj) =>
dispatch({
kind: Kind.AN_ACTION,
payload: { wells_plan: c },
}),
[]
);
Component C
It has another useReducer that controls the states of C_obj
const [C_obj, dispatch] = useReducer(reducer, props.C_obj);
To send the information back up to Component B, it uses the function fn_callback, created in B thanks to a useEffect hook with dep on C_obj
useEffect(() => {
props.fn_callback(C_obj);
}, [C_obj, props.fn_callback]);
I hope it is not a total brain schmuck to read, I am very new to all of that so I understand I can be doing something totally broken by design.
Many thanks for help
EDIT: as requested, here is a block of code to synthetize
const A = (): JSX.Element => {
const [C_obj_arr, ASetter] = useState<C_obj[]>();
return (
<>
<B>C_obj_arr=C_obj_arr ASetter=ASetter</B>
</>
);
};
const B = (C_obj_arr_init: C_obj[], ASetter: () => void): JSX.Element => {
const [C_obj_array, dispatch] = useReducer(reducer, C_obj_arr_init);
useEffect(() => {
ASetter(C_obj_array);
}, [C_obj_array, ASetter]);
const fn_callback = useCallback(
(c_obj: C_obj) =>
dispatch({
kind: Kind.UPDATE_OBJ,
payload: { wells_plan: c_obj },
}),
[]
);
return C_obj_array.map(C_obj => (
<C C_obj={C_obj} fn_callback={fn_callback}></C>
));
};
const C = (C_obj_init, fn_callback): JSX.Element => {
const [C_obj, dispatch] = useReducer(reducer, C_obj_init);
useEffect(() => {
fn_callback(C_obj);
}, [C_obj, fn_callback]);
return <div>{C.toString()}</div>;
};
I assume, that you mean
import { useState, useEffect, useReducer, useCallback } from "react"
type SomeObj = {
name: string
key: string
}
const A = (): JSX.Element => {
const [items, setitems] = useState<SomeObj[]>([
{
key: "a",
name: "hello"
}
])
return (
<>
<B items={items} setitems={setitems} />
</>
)
}
const B = ({ items, setitems }: { items: SomeObj[]; setitems: (x: SomeObj[]) => void }): JSX.Element => {
const [items2, dispatch] = useReducer(
(
x: SomeObj[],
a: {
kind: string
payload: {}
}
) => {
return x
},
items
)
useEffect(() => {
setitems(items2)
}, [items2, setitems])
const fn_callback = useCallback(
(item: SomeObj) =>
dispatch({
kind: "update",
payload: { wells_plan: item },
}),
[]
)
return (
<div>
{items2.map((item) => (
<C itemInit={item} fn_callback={fn_callback} key={item.key}></C>
))}
</div>
)
}
const C = ({ itemInit, fn_callback }: { itemInit: SomeObj; fn_callback: (c_obj: SomeObj) => void }): JSX.Element => {
const [item, dispatch] = useReducer((x: SomeObj) => x, itemInit)
useEffect(() => {
fn_callback(item)
}, [item, fn_callback])
return <div>{item.name}</div>
}
function App() {
return (
<main>
<A></A>
</main>
)
}
export default App
And, I think, it's basically the reason for using global/atom state managers to react like: redux, recoil, jotai, etc.
In your scenario, you have a couple of prop passing layers through components, and only if I understand correctly, you try to take up changes from deeply nested component.
Using global state, you can have only one array and source of truth, and only one handler in each child component. That probably will remove all unnecessary hooks
The missing bolt for this thing to work was to handle the rerendering of component C. The problem was that useReducer initial state is applied only once, then not when the component is rendered. Therefore, there is a need to force the update of the obj_c when re-rendering it. To do that, I added an extra dispatch action in the Component C body.
useEffect(() => {
dispatch({
kind: Kind.UPDATE,
payload: {
obj_C: C_obj_init,
},
});
}, [C_obj_init]);
which associated case in the reducer is
switch (action.kind) {
case Kind.UPDATE:
return action.payload.obj_C;
An important thing point here is to not return a recreated obj_C, this wouldlead to an infinite loop to this other hook seen earlier:
useEffect(() => {
fn_callback(C_obj);
}, [C_obj, fn_callback]);
Related
I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
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.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.
When designing a generic component, I adopted the principle of SOC where the generic component will not know the implementation details of the user and allow the user to listen for callbacks to handle their own state.
For example:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
const handleToggle = React.useCallback(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
setIsActive(updatedValue);
}, [isActive, onToggle]);
return (
<div onClick={handleToggle}>Sort</div>
);
}
Parent.tsx
const Parent: React.FC<Props> = ({
}) => {
const [parentObject, setParentObject] = React.useState({});
const handleToggle = React.useCallback((value: boolean) => {
let updatedValue = value;
// do some computation to updatedValue if needed
setParentObject((parent) => { ...parent, calculated: updatedValue });
}, []);
return (
<SortButton onToggle={handleToggle} />
);
}
The above implementation allows the generic component (ie. Toggle) to handle their own state and allows parents to implement their own implementation using callbacks. However, I have been getting the following error:
Warning: Cannot update a component (`Parent`) while rendering a different component (`Toggle`). To locate the bad setState() call inside `Toggle`
My understanding from the error is that, within the callback onClick, it triggers state mutation of the parent (in this case, another component) which should not be allowed. The solution I figured was to move the callback within an useEffect like this:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
React.useEffect(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
}, [isActive, onToggle]);
const handleToggle = React.useCallback(() => {
setIsActive((value) => !value);
}, []);
return (
<div onClick={handleToggle}>Sort</div>
);
}
The new implementation works but I have a few questions which I would hope to get some guidance on.
The example works and is easy to refactor because it is a simple state of isActive. What if the value we need is more complicated (for example, mouse position, etc) and does not have a state to store the value and is only available from onMouseMove? Do we create a state to store the `mouse position and follow the pattern?
Is the existing implementation an anti-pattern to any of the React concepts in the first place?
Is there any other possible implementation to solve the issue?
This is a somewhat biased opinion, but I'm a big proponent of Lifting State Up and "Dumb" Components/Controlled Components.
I would design it so that the SortButton does not have any internal state. It would get all of the information that it needs from props. The Parent would be responsible for passing down the correct value of isActive/value, which it will update when the child SortButton calls its onToggle prop.
We can include the event in the onToggle callback just in case the parent wants to use it.
SortButton.tsx
import * as React from "react";
interface Props {
isActive: boolean;
onToggle: (value: boolean, e: React.MouseEvent<HTMLDivElement>) => void;
}
const Toggle: React.FC<Props> = ({ isActive, onToggle }) => {
return (
<div
className={isActive ? "sort-active" : "sort-inactive"}
onClick={(e) => onToggle(!isActive, e)}
>
{isActive ? "Unsort" : "Sort"}
</div>
);
};
export default Toggle;
Parent.tsx
import * as React from "react";
import SortButton from "./SortButton";
interface Props {
list: number[];
}
const Parent: React.FC<Props> = ({ list }) => {
// parent stores the state of the sort
const [isSorted, setIsSorted] = React.useState(false);
// derived data is better as a memo than as state.
const sortedList = React.useMemo(
// either sort the list or don't.
() => (isSorted ? [...list].sort() : list),
// depends on the list prop and the isSorted state.
[list, isSorted]
);
return (
<div>
<SortButton
isActive={isSorted}
// you could use a more complicated callback, but it's not needed here.
onToggle={setIsSorted}
/>
<ul>
{sortedList.map((n) => (
<li>{n.toFixed(3)}</li>
))}
</ul>
</div>
);
};
export default Parent;
Code Sandbox Demo
I'm using a debounce library (tried different ones but currently the one from lodash) in a react component in order to avoid executing code too often while scrolling in the browser.
The problem is that I have multiple instances of the react component and it seems that the debounce function is accidentally shared between those instances. Consequently the function code with '... some code here' is only executed in one instance and not in all instances of the react component. The debounce functionality works great if I have only one instance of my component rendered.
useEffect(() => {
document.querySelector(props.scrollSelector!)?.addEventListener('scroll', e => {
setViewport(props, state, e.target as HTMLDivElement, ref)
}, true)
}, [state.obj])
const setViewport = debounce((p: Props, s: State, rowHeaderObj: any, scrollContainer: HTMLDivElement, ref: any) => {
// ... some code here
}, 20)
Is there some way to change the code so the debounce function works for each instance separately? Please consider that the react component instances have unique keys assigned so that should not be the issue.
One approach could be to create a new debounced function each time you register the event listener instead of reusing the same function, in which case the event handler would be debounced independently within each instance of your component.
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = (props) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
const srollableElement = document.querySelector(props.scrollSelector!)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(props, state, e.target as HTMLDivElement, ref)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, props, ref])
return <></>
}
As a side note, be careful with this usage of useEffect, as (I think) the props parameter that's passed to your component will change each time the parent component re-renders, causing useEffect to potentially re-run very often. One fix for this is making sure the dependencies array passed to useEffect only contains primitive or stable values. Feel free to read this section of the React docs for a discussion of this topic. Taking this into consideration, you might want to re-write the above example as follows (depending on the shape of the Props type):
interface Props {
scrollSelector?: string
b: string
c: number
}
const _setViewport = () => (
p: Props,
s: State,
rowHeaderObj: any,
scrollContainer: HTMLDivElement,
ref: any
) => {
// ... some code here
}
const MyComponent: React.FC<Props> = ({ scrollSelector, b, c }) => {
const [state, setState] = useState<State>()
const ref = useRef<any>()
useEffect(() => {
if (!scrollSelector) {
return
}
const srollableElement = document.querySelector(scrollSelector)
if (!srollableElement) {
return
}
const setViewport = debounce(_setViewport, 20)
const scrollHandler = (e: Event) =>
setViewport(
{ scrollSelector, b, c },
state,
e.target as HTMLDivElement,
ref
)
srollableElement.addEventListener('scroll', scrollHandler, true)
return () => {
srollableElement.removeEventListener('scroll', scrollHandler, true)
}
}, [state, scrollSelector, b, c, ref])
return <></>
}
Using react hooks with TypeScript and here is a minimal representation of what I am trying to do: Have a list of buttons on screen and when the user clicks on a button, I want to change the text of the button to "Button clicked" and then only re-render the button which was clicked.
I am using useCallback for wrapping the button click event to avoid the click handler getting re-created on every render.
This code works the way I want: If I use useState and maintain my state in an array, then I can use the Functional update in useState and get the exact behaviour I want:
import * as React from 'react';
import { IHelloWorldProps } from './IHelloWorldProps';
import { useEffect, useCallback, useState } from 'react';
import { PrimaryButton } from 'office-ui-fabric-react';
interface IMyButtonProps {
title: string;
id: string;
onClick: (clickedDeviceId: string) => (event: any) => void;
}
const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => {
console.log(`Button rendered for ${props.title}`);
return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />;
});
interface IDevice {
Name: string;
Id: string;
}
const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => {
//If I use an array for state instead of object and then use useState with Functional update, I get the result I want.
const initialState: IDevice[] = [];
const [deviceState, setDeviceState] = useState<IDevice[]>(initialState);
useEffect(() => {
//Simulate network call to load data.
setTimeout(() => {
setDeviceState([{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }]);
}, 500);
}, []);
const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => {
setDeviceState(prevState => prevState.map((device: IDevice) => {
if (device.Id === clickedDeviceId) {
device.Name = `${device.Name} clicked`;
}
return device;
}));
}), []);
return (
<React.Fragment>
{deviceState.map((device: IDevice) => {
return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />;
})}
</React.Fragment>
);
};
export default HelloWorld;
Here is the desired result:
But here is my problem: In my production app, the state is maintained in an object and we are using the useReducer hook to simulate a class component style setState where we only need to pass in the changed properties. So we don't have to keep replacing the entire state for every action.
When trying to do the same thing as before with useReducer, the state is always stale as the cached version of useCallback is from the first load when the device list was empty.
import * as React from 'react';
import { IHelloWorldProps } from './IHelloWorldProps';
import { useEffect, useCallback, useReducer, useState } from 'react';
import { PrimaryButton } from 'office-ui-fabric-react';
interface IMyButtonProps {
title: string;
id: string;
onClick: (clickedDeviceId: string) => (event: any) => void;
}
const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => {
console.log(`Button rendered for ${props.title}`);
return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />;
});
interface IDevice {
Name: string;
Id: string;
}
interface IDeviceState {
devices: IDevice[];
}
const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => {
const initialState: IDeviceState = { devices: [] };
//Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state.
const [deviceState, setDeviceState] = useReducer((previousState: IDeviceState, updatedProperties: Partial<IDeviceState>) => ({ ...previousState, ...updatedProperties }), initialState);
useEffect(() => {
//Simulate network call to load data.
setTimeout(() => {
setDeviceState({ devices: [{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }] });
}, 500);
}, []);
//Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time.
//If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device.
const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => {
//Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
const updatedDeviceList = deviceState.devices.map((device: IDevice) => {
if (device.Id === clickedDeviceId) {
device.Name = `${device.Name} clicked`;
}
return device;
});
setDeviceState({ devices: updatedDeviceList });
//Cannot add the deviceState.devices dependency here because we are updating deviceState.devices inside the function. This would mean useCallback would be useless.
}), []);
return (
<React.Fragment>
{deviceState.devices.map((device: IDevice) => {
return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />;
})}
</React.Fragment>
);
};
export default HelloWorld;
This is how it looks:
So my question boils down to this: When using useState inside useCallback, we can use the functional update pattern and capture the current state (instead of from when useCallback was cached)
This is possible without specifying dependencies to useCallback.
How can we do the same thing when using useReducer? Is there a way to get the current state inside useCallback when using useReducer and without specifying dependencies to useCallback?
You can dispatch a function that will be called by the reducer and gets the current state passed to it. Something like this:
//Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state.
const [deviceState, dispatch] = useReducer(
(previousState, action) => action(previousState),
initialState
);
//Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time.
//If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device.
const _deviceClicked = useCallback(
(clickedDeviceId) => (event) => {
//Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
dispatch((deviceState) => ({
...deviceState,
devices: deviceState.devices.map((device) => {
if (device.Id === clickedDeviceId) {
device.Name = `${device.Name} clicked`;
}
return device;
}),
}));
//no dependencies here
},
[]
);
Below is a working example:
const { useCallback, useReducer } = React;
const App = () => {
const [deviceState, dispatch] = useReducer(
(previousState, action) => action(previousState),
{ count: 0, other: 88 }
);
const click = useCallback(
(increase) => () => {
//Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
dispatch((deviceState) => ({
...deviceState,
count: deviceState.count + increase,
}));
//no dependencies here
},
[]
);
return (
<div>
<button onClick={click(1)}>+1</button>
<button onClick={click(2)}>+2</button>
<button onClick={click(3)}>+3</button>
<pre>{JSON.stringify(deviceState)}</pre>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is not how you would normally use useReducer and don't se a reason why you would not just use useState instead in this instance.
I'm trying React hooks for the first time and all seemed good until I realised that when I get data and update two different state variables (data and loading flag), my component (a data table) is rendered twice, even though both calls to the state updater are happening in the same function. Here is my api function which is returning both variables to my component.
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
In a normal class component you'd make a single call to update the state which can be a complex object but the "hooks way" seems to be to split the state into smaller units, a side effect of which seems to be multiple re-renders when they are updated separately. Any ideas how to mitigate this?
You could combine the loading state and data state into one state object and then you could do one setState call and there will only be one render.
Note: Unlike the setState in class components, the setState returned from useState doesn't merge objects with existing state, it replaces the object entirely. If you want to do a merge, you would need to read the previous state and merge it with the new values yourself. Refer to the docs.
I wouldn't worry too much about calling renders excessively until you have determined you have a performance problem. Rendering (in the React context) and committing the virtual DOM updates to the real DOM are different matters. The rendering here is referring to generating virtual DOMs, and not about updating the browser DOM. React may batch the setState calls and update the browser DOM with the final new state.
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Alternative - write your own state merger hook
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
This also has another solution using useReducer! first we define our new setState.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
after that we can simply use it like the good old classes this.setState, only without the this!
setState({loading: false, data: test.data.results})
As you may noticed in our new setState (just like as what we previously had with this.setState), we don't need to update all the states together! for example I can change one of our states like this (and it doesn't alter other states!):
setState({loading: false})
Awesome, Ha?!
So let's put all the pieces together:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript Support.
Thanks to P. Galbraith who replied this solution,
Those using typescript can use this:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
where MyState is the type of your state object.
e.g. In our case it'll be like this:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Previous State Support.
In comments user2420374 asked for a way to have access to the prevState inside our setState, so here's a way to achieve this goal:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction checks whether the passed argument is a function (which means you're trying to access the prevState) or a plain object. You can find this implementation of isFunction by Alex Grande here.
Notice. For those who want to use this answer a lot, I decided to turn it into a library. You can find it here:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Batching update in react-hooks https://github.com/facebook/react/issues/14259
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 an async call.
This will do:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
To replicate this.setState merge behavior from class components,
React docs recommend to use the functional form of useState with object spread - no need for useReducer:
setState(prevState => {
return {...prevState, loading, data};
});
The two states are now consolidated into one, which will save you a render cycle.
There is another advantage with one state object: loading and data are dependent states. Invalid state changes get more apparent, when state is put together:
setState({ loading: true, data }); // ups... loading, but we already set data
You can even better ensure consistent states by 1.) making the status - loading, success, error, etc. - explicit in your state and 2.) using useReducer to encapsulate state logic in a reducer:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
PS: Make sure to prefix custom Hooks with use (useData instead of getData). Also passed callback to useEffect cannot be async.
If you are using third-party hooks and can't merge the state into one object or use useReducer, then the solution is to use :
ReactDOM.unstable_batchedUpdates(() => { ... })
Recommended by Dan Abramov here
See this example
A little addition to answer https://stackoverflow.com/a/53575023/121143
Cool! For those who are planning to use this hook, it could be written in a bit robust way to work with function as argument, such as this:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
Update: optimized version, state won't be modified when incoming partial state was not changed.
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
In addition to Yangshun Tay's answer you'll better to memoize setMergedState function, so it will return the same reference each render instead of new function. This can be crucial if TypeScript linter forces you to pass setMergedState as a dependency in useCallback or useEffect in parent component.
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};
You can also use useEffect to detect a state change, and update other state values accordingly