Updating the same React state concurrently in different locations [duplicate] - javascript

This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 5 months ago.
I'm working on building a file upload portal in React that allows for multiple concurrent uploads, using an amalgamation of some preexisting code and new code that I'm writing myself. The code base is pretty complex, so I will explain what's happening at a high level, illustrate the problem, and then provide a toy example to simulate what's happening in CodeSandbox.
The top level component has a files state variable from useState, which is an object that contains sub objects with information regarding each file that the user is currently uploading, that is being mapped across in the JSX and returning UI elements for each. Think:
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
},
2: {
id: 2,
name: "File 2",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const progressElements = Object.values(files).map((file) => (
<Progress key={file.id} value={file.progress} />
))
When an upload is initiated, existing code dictates that a callback is provided from the top level that receives an updated progress value for a given upload, and then sets that into state in files for the corresponding upload. This works perfectly fine when there is only one active upload, but as soon as a second file is added, the fact that the same files state object is being concurrently updated in multiple places at once bugs out the UI and causes the rendered JSX to be inaccurate. What is the correct way to handle concurrent updates to the same state at once?
Below is a super simplified (and hastily written, my apologies) sandbox as a toy example of what's going on. It's obviously not an exact replica of what's happening in the actual code, but it gets the general idea across. You can see that, with one upload going, the UI updates fine. But when additional uploads are added, any updates to the first overwrite the existence of the new upload in state and thus break the UI.
https://codesandbox.io/s/elastic-wozniak-yvbfo6?file=/src/App.js:133-297
const { useState, useEffect } = React;
const App = () => {
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const [uploading, setUploading] = useState(false);
const updateProgress = (uploadId, progress) => {
setFiles({
...files,
[uploadId]: {
...files[uploadId],
progress
}
});
};
const filesArray = Object.values(files);
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles({
...files,
[newId]: {
id: newId,
name: 'File ' + newId,
progress: 0
}
});
};
return (
<div className="App">
{filesArray.map((file) => (
<UploadStatus
key={file.id}
uploading={uploading}
setUploading={setUploading}
file={file}
updateProgress={updateProgress}
/>
))}
<button
onClick={
uploading ? () => setUploading(false) : () => setUploading(true)
}
>
{uploading ? "Cancel Upload" : "Start Upload"}
</button>
{uploading && <button onClick={addNewFile}>Add New File</button>}
</div>
);
}
const UploadStatus = ({
file,
updateProgress,
uploading,
setUploading
}) => {
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgress(file.id, calls * 10);
}, 1000);
if (calls === 10) {
clearInterval(interval);
setUploading(false);
}
return () => clearInterval(interval);
}, [uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<body>
<div id="root"></div>
</body>
Any help or thoughts would be greatly appreciated. Thanks!

Based on what is inside your codesandbox - the issue is just with closures and missing items in dependency array of useEffect. I do understand why you want to have only the things you described in depsArray of useEffect - but if you dont provide all the things there - you will get an issue with closure and you will be calling the old function or refering old variable, what is happening in your codesandbox. Consider wrapping everything that is passed to the hooks or child components with useMemo, useCallback and other memoizing functions react provide and include everything that is needed in depsArray. There is a workaround with useRef hook to hold a reference to the specific function you want to call + useEffect with the only purpose to update this useRef variable.
So rough fix 1:
import { useEffect, useRef } from "react";
export default function UploadStatus({
file,
updateProgress,
uploading,
setUploading
}) {
const updateProgressRef = useRef(updateProgress);
const setUploadingRef = useRef(setUploading);
useEffect(() => {
updateProgressRef.current = updateProgress;
}, [updateProgress]);
useEffect(() => {
setUploadingRef.current = setUploading;
}, [setUploading]);
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgressRef.current(file.id, calls * 10);
if (calls === 10) {
clearInterval(interval);
setUploadingRef.current(false);
}
}, 1000);
return () => clearInterval(interval);
}, [file.id, uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
You will have to fix some logic here due to when any of the Uploadings is done - all the rest uploading will "stop" due to uploading and setUploading parameters.
What can simplify the fixing process is to modify addNewFile and updateProgress so they will not rely on closure captured files. setXXX functions from useState can recieve either the new value as a parameter, either a callback currentValue => newValue. So you can use callback one:
So rough fix 2:
const updateProgress = (uploadId, progress) => {
setFiles((files) => ({
...files,
[uploadId]: {
...files[uploadId],
progress
}
}));
};
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles((files) => ({
...files,
[newId]: {
id: newId,
name: `File ${newId}`,
progress: 0
}
}));
};
This fix will actually work better but still, wirings of states and components and depsArray should be fixed more precisiolly.
Hope that helps you to get the basic idea of what is going on and in which direction to dig with your issues.

Related

Why is useFetcher causing an re-render infinite loop?

I have an input. On every change to the input, I want to call an API.
Here's a simplified version of the code:
// updated by input
const [urlText, setUrlText] = useState("");
const fetcher = useFetcher();
useEffect(() => {
if (urlText === "" || !fetcher) return;
fetcher.load(`/api/preview?url=${urlText}`);
}, [urlText]);
The issue is, when I put urlText inside of the dependencies array, there is an infinite rendering loop, and React claims the issue is I might be updating state inside of the useEffect. However, as far as I can tell, I'm not updating any state inside of the hook, so I'm not sure why an infinite re-render is happening.
Any thoughts?
The fuller version of the code is:
Note: The bug still happens without the debounce, or the useMemo, all of that stuff is roughly irrelevant.
export default function () {
const { code, higlightedCode } = useLoaderData<API>();
const [urlText, setUrlText] = useState("");
const url = useMemo(() => getURL(prefixWithHttps(urlText)), [urlText]);
const debouncedUrl = useDebounce(url, 250);
const fetcher = useFetcher();
useEffect(() => {
if (url === null || !fetcher) return;
fetcher.load(`/api/preview?url=${encodeURIComponent(url.toString())}`);
}, [debouncedUrl]);
return (
<input
type="text"
placeholder="Paste URL"
className={clsx(
"w-full rounded-sm bg-gray-800 text-white text-center placeholder:text-white"
//"placeholder:text-left text-left"
)}
value={urlText}
onChange={(e) => setUrlText(e.target.value)}
></input>
);
}
The problem you're having is that fetcher is updated throughout the fetch process. This is causing your effect to re-run, and since you are calling load again, it is repeating the cycle.
You should be checking fetcher.state to see when to fetch.
useEffect(() => {
// check to see if you haven't fetched yet
// and we haven't received the data
if (fetcher.state === 'idle' && !fetcher.data) {
fetcher.load(url)
}
}, [url, fetcher.state, fetcher.data])
https://remix.run/docs/en/v1/api/remix#usefetcher
You might by setting state in useFetcher hook, please check code of load method from useFetcher.
Update: I'm silly. useDebounce returns an array.

Getting a warning while using firebase login system [duplicate]

I am getting this warning in react:
index.js:1 Warning: Cannot update a component (`ConnectFunction`)
while rendering a different component (`Register`). To locate the
bad setState() call inside `Register`
I went to the locations indicated in the stack trace and removed all setstates but the warning still persists. Is it possible this could occur from redux dispatch?
my code:
register.js
class Register extends Component {
render() {
if( this.props.registerStatus === SUCCESS) {
// Reset register status to allow return to register page
this.props.dispatch( resetRegisterStatus()) # THIS IS THE LINE THAT CAUSES THE ERROR ACCORDING TO THE STACK TRACE
return <Redirect push to = {HOME}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
function mapStateToProps( state ) {
return {
registerStatus: state.userReducer.registerStatus
}
}
export default connect ( mapStateToProps ) ( Register );
function which triggers the warning in my registerForm component called by register.js
handleSubmit = async () => {
if( this.isValidForm() ) {
const details = {
"username": this.state.username,
"password": this.state.password,
"email": this.state.email,
"clearance": this.state.clearance
}
await this.props.dispatch( register(details) )
if( this.props.registerStatus !== SUCCESS && this.mounted ) {
this.setState( {errorMsg: this.props.registerError})
this.handleShowError()
}
}
else {
if( this.mounted ) {
this.setState( {errorMsg: "Error - registration credentials are invalid!"} )
this.handleShowError()
}
}
}
Stacktrace:
This warning was introduced since React V16.3.0.
If you are using functional components you could wrap the setState call into useEffect.
Code that does not work:
const HomePage = (props) => {
props.setAuthenticated(true);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
Now you can change it to:
const HomePage = (props) => {
// trigger on component mount
useEffect(() => {
props.setAuthenticated(true);
}, []);
const handleChange = (e) => {
props.setSearchTerm(e.target.value.toLowerCase());
};
return (
<div key={props.restInfo.storeId} className="container-fluid">
<ProductList searchResults={props.searchResults} />
</div>
);
};
I just had this issue and it took me a bit of digging around before I realised what I'd done wrong – I just wasn't paying attention to how I was writing my functional component.
I was doing this:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
};
return (<div>{component stuff here..}</div>);
};
I had just forgotten to use useEffect before dispatching my redux call of getDateMatches()
So it should have been:
const LiveMatches = (props: LiveMatchesProps) => {
const {
dateMatches,
draftingConfig,
sportId,
getDateMatches,
} = props;
useEffect(() => {
if (!dateMatches) {
const date = new Date();
getDateMatches({ sportId, date });
}
}, [dateMatches, getDateMatches, sportId]);
return (<div>{component stuff here..}</div>);
};
please read the error message thoroughly, mine was pointing to SignIn Component that had a bad setState. which when i examined, I had an onpress that was not an Arrow function.
it was like this:
onPress={navigation.navigate("Home", { screen: "HomeScreen" })}
I changed it to this:
onPress={() => navigation.navigate("Home", { screen: "HomeScreen" }) }
My error message was:
Warning: Cannot update a component
(ForwardRef(BaseNavigationContainer)) while rendering a different
component (SignIn). To locate the bad setState() call inside
SignIn, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render
in SignIn (at SignInScreen.tsx:20)
I fixed this issue by removing the dispatch from the register components render method to the componentwillunmount method. This is because I wanted this logic to occur right before redirecting to the login page. In general it's best practice to put all your logic outside the render method so my code was just poorly written before. Hope this helps anyone else in future :)
My refactored register component:
class Register extends Component {
componentWillUnmount() {
// Reset register status to allow return to register page
if ( this.props.registerStatus !== "" ) this.props.dispatch( resetRegisterStatus() )
}
render() {
if( this.props.registerStatus === SUCCESS ) {
return <Redirect push to = {LOGIN}/>
}
return (
<div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
<RegistrationForm/>
</div>
);
}
}
I think that this is important.
It's from this post that #Red-Baron pointed out:
#machineghost : I think you're misunderstanding what the message is warning about.
There's nothing wrong with passing callbacks to children that update state in parents. That's always been fine.
The problem is when one component queues an update in another component, while the first component is rendering.
In other words, don't do this:
function SomeChildComponent(props) {
props.updateSomething();
return <div />
}
But this is fine:
function SomeChildComponent(props) {
// or make a callback click handler and call it in there
return <button onClick={props.updateSomething}>Click Me</button>
}
And, as Dan has pointed out various times, queuing an update in the same component while rendering is fine too:
function SomeChildComponent(props) {
const [number, setNumber] = useState(0);
if(props.someValue > 10 && number < 5) {
// queue an update while rendering, equivalent to getDerivedStateFromProps
setNumber(42);
}
return <div>{number}</div>
}
If useEffect cannot be used in your case or if the error is NOT because of Redux
I used setTimeout to redirect one of the two useState variables to the callback queue.
I have one parent and one child component with useState variable in each of them. The solution is to wrap useState variable using setTimeout:
setTimeout(() => SetFilterData(data), 0);
Example below
Parent Component
import ExpenseFilter from '../ExpensesFilter'
function ExpensesView(props) {
const [filterData, SetFilterData] = useState('')
const GetFilterData = (data) => {
// SetFilterData(data);
//*****WRAP useState VARIABLE INSIDE setTimeout WITH 0 TIME AS BELOW.*****
setTimeout(() => SetFilterData(data), 0);
}
const filteredArray = props.expense.filter(expenseFiltered =>
expenseFiltered.dateSpent.getFullYear().toString() === filterData);
return (
<Window>
<div>
<ExpenseFilter FilterYear = {GetFilterData}></ExpenseFilter>
Child Component
const ExpensesFilter = (props) => {
const [filterYear, SetFilterYear] = useState('2022')
const FilterYearListener = (event) => {
event.preventDefault()
SetFilterYear(event.target.value)
}
props.FilterYear(filterYear)
return (
Using React and Material UI (MUI)
I changed my code from:
<IconButton onClick={setOpenDeleteDialog(false)}>
<Close />
</IconButton>
To:
<IconButton onClick={() => setOpenDeleteDialog(false)}>
<Close />
</IconButton>
Simple fix
If you use React Navigation and you are using the setParams or setOptions you must put these inside method componentDidMount() of class components or in useEffects() hook of functional components.
Minimal reproducing example
I was a bit confused as to what exactly triggers the problem, having a minimal immediately runnable example helped me grasp it a little better:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.14.7/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function NotMain(props) {
props.setN(1)
return <div>NotMain</div>
}
function Main(props) {
const [n, setN] = React.useState(0)
return <>
<NotMain setN={setN} />
<div>Main {n}</div>
</>
}
ReactDOM.render(
<Main/>,
document.getElementById('root')
);
</script>
</body>
</html>
fails with error:
react-dom.development.js:61 Warning: Cannot update a component (`Main`) while rendering a different component (`NotMain`). To locate the bad setState() call inside `NotMain`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
followed by a stack trace:
at NotMain (<anonymous>:16:9)
at Main (<anonymous>:21:31)
Presumably 16:9 would be the exact line where props.setN(1) is being called from, but the line numbers are a bit messed up because of the Babel JSX translation.
The solution like many other answers said is to do instead:
function NotMain(props) {
React.useEffect(() => { props.setN(1) }, [])
return <div>NotMain</div>
}
Intuitively, I think that the general idea of why this error happens is that:
You are not supposed to updat state from render methods, otherwise it could lead to different results depending on internal the ordering of how React renders things.
and when using functional components, the way to do that is to use hooks. In our case, useEffect will run after rendering is done, so we are fine doing that from there.
When using classes this becomes slightly more clear and had been asked for example at:
Calling setState in render is not avoidable
Calling setState() in React from render method
When using functional components however, things are conceptually a bit more mixed, as the component function is both the render, and the code that sets up the callbacks.
I was facing same issue, The fix worked for me was if u are doing
setParams/setOptions
outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm
TL;DR;
For my case, what I did to fix the warning was to change from useState to useRef
react_devtools_backend.js:2574 Warning: Cannot update a component (`Index`) while rendering a different component (`Router.Consumer`). To locate the bad setState() call inside `Router.Consumer`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
at Route (http://localhost:3000/main.bundle.js:126692:29)
at Index (http://localhost:3000/main.bundle.js:144246:25)
at Switch (http://localhost:3000/main.bundle.js:126894:29)
at Suspense
at App
at AuthProvider (http://localhost:3000/main.bundle.js:144525:23)
at ErrorBoundary (http://localhost:3000/main.bundle.js:21030:87)
at Router (http://localhost:3000/main.bundle.js:126327:30)
at BrowserRouter (http://localhost:3000/main.bundle.js:125948:35)
at QueryClientProvider (http://localhost:3000/main.bundle.js:124450:21)
The full code for the context of what I did (changed from the lines with // OLD: to the line above them). However this doesn't matter, just try changing from useState to useRef!!
import { HOME_PATH, LOGIN_PATH } from '#/constants';
import { NotFoundComponent } from '#/routes';
import React from 'react';
import { Redirect, Route, RouteProps } from 'react-router-dom';
import { useAccess } from '#/access';
import { useAuthContext } from '#/contexts/AuthContext';
import { AccessLevel } from '#/models';
type Props = RouteProps & {
component: Exclude<RouteProps['component'], undefined>;
requireAccess: AccessLevel | undefined;
};
export const Index: React.FC<Props> = (props) => {
const { component: Component, requireAccess, ...rest } = props;
const { isLoading, isAuth } = useAuthContext();
const access = useAccess();
const mounted = React.useRef(false);
// OLD: const [mounted, setMounted] = React.useState(false);
return (
<Route
{...rest}
render={(props) => {
// If in indentifying authentication state as the page initially loads, render a blank page
if (!mounted.current && isLoading) return null;
// OLD: if (!mounted && isLoading) return null;
// 1. Check Authentication is one step
if (!isAuth && window.location.pathname !== LOGIN_PATH)
return <Redirect to={LOGIN_PATH} />;
if (isAuth && window.location.pathname === LOGIN_PATH)
return <Redirect to={HOME_PATH} />;
// 2. Authorization is another
if (requireAccess && !access[requireAccess])
return <NotFoundComponent />;
mounted.current = true;
// OLD: setMounted(true);
return <Component {...props} />;
}}
/>
);
};
export default Index;
My example.
Code with that error:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({values, dirtyFields }: any) => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
Working Code:
<Form
initialValues={{ ...kgFormValues, dataflow: dataflows.length > 0 ? dataflows[0].df_tpl_key : "" }}
onSubmit={() => {}}
render={({ values, dirtyFields }: any) => {
useEffect(() => {
const kgFormValuesUpdated = {
proj_key: projectKey,
name: values.name,
description: values.description,
public: values.public,
dataflow: values.dataflow,
flavours: flavoursSelected,
skipOCR: values.skipOCR
};
if (!_.isEqual(kgFormValues, kgFormValuesUpdated)) {
setNewKgFormValues(kgFormValuesUpdated);
}
}, [values]);
return (
I had the same problem. I was setting some state that was storing a function like so:
// my state definition
const [onConfirm, setOnConfirm] = useState<() => void>();
// then I used this piece of code to update the state
function show(onConfirm: () => void) {
setOnConfirm(onConfirm);
}
The problem was from setOnConfirm. In React, setState can take the new value OR a function that returns the new value. In this case React wanted to get the new state from calling onConfirm which is not correct.
changing to this resolved my issue:
setOnConfirm(() => onConfirm);
I was able to solve this after coming across a similar question in GitHub which led me to this comment showing how to pinpoint the exact line within your file causing the error. I wasn't aware that the stack trace was there. Hopefully this helps someone!
See below for my fix. I simply converted the function to use callback.
Old code
function TopMenuItems() {
const dispatch = useDispatch();
function mountProjectListToReduxStore(projects) {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}
};
New code
function TopMenuItems() {
const dispatch = useDispatch();
const mountProjectListToReduxStore = useCallback((projects) => {
const projectDropdown = projects.map((project) => ({
id: project.id,
name: project.name,
organizationId: project.organizationId,
createdOn: project.createdOn,
lastModifiedOn: project.lastModifiedOn,
isComplete: project.isComplete,
}));
projectDropdown.sort((a, b) => a.name.localeCompare(b.name));
dispatch(loadProjectsList(projectDropdown));
dispatch(setCurrentOrganizationId(projectDropdown[0].organizationId));
}, [dispatch]);
};
My case was using setState callback, instead of setState + useEffect
BAD ❌
const closePopover = useCallback(
() =>
setOpen((prevOpen) => {
prevOpen && onOpenChange(false);
return false;
}),
[onOpenChange]
);
GOOD ✅
const closePopover = useCallback(() => setOpen(false), []);
useEffect(() => onOpenChange(isOpen), [isOpen, onOpenChange]);
I got this when I was foolishly invoking a function that called dispatch instead of passing a reference to it for onClick on a button.
const quantityChangeHandler = (direction) => {
dispatch(cartActions.changeItemQuantity({title, quantityChange: direction}));
}
...
<button onClick={() => quantityChangeHandler(-1)}>-</button>
<button onClick={() => quantityChangeHandler(1)}>+</button>
Initially, I was directly calling without the fat arrow wrapper.
Cannot update a component while rendering a different component warning
I have the same problem but when I dispatch an action inside a component rendered. You should dispatch the action inside useEffect hook to fix that problem
//dispatch action to inform user that 'Marked days already have hours!'
React.useEffect(() => {
if (btn_class == 'redButton') {
dispatch({ type: ActionType.ADD_NOTIFICATION, payload: 'Marked days already have hours!' });
} else {
dispatch({ type: ActionType.ADD_NOTIFICATION, payload: '' });
}
}, [btn_class, dispatch]);
also use union type for btn-class variable
*`
type ButtonState = 'btnAddDay' | 'redButton' | 'btnAddDayBlue' | 'btnAddDayGreen';
`*
Using some of the answers above, i got rid of the error with the following:
from
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
this code was on my component top-level
to
const SelectSorting = () => {
const dispatch = useAppDispatch();
const {value, onChange} = useSelect();
useEffect(() => {
if (value === "newest") {
dispatch(sortArticlesNewest());
} else {
dispatch(sortArticlesOldest());
}
}, [dispatch, value]);

Returning a React Element within a Functional Component

I am fairly new to React and trying to edit a former coworker's code (hence the non-traditional React style).
I need to return a series of buttons (here "Hello" and "Goodbye") when a single button ("Dropdown") is clicked. In order to make this more dynamic down the line, I'd like to return those buttons from a function (dropdownDetails) rather than [show,setShow] variables, etc.
When I run this code, the console shows that dropdownDetails is triggered, but "Hello" and "Goodbye" don't appear in the UI. What am I doing wrong?
Thanks in advance!
///// In base.js:
const e = React.createElement;
///// in page_body.js:
function pageBody(props) {
const dropdownDetailsWrap = (props) => (e) => {
console.log("Wrap triggered")
dropdownDetails()
}
function dropdownDetails() {
console.log("dropdownDetails useEffect triggered")
return(
e("button",{type:"button"},"Hello"),
e("button",{type:"button"},"Goodbye")
);
};
const pageBody = () => (e) => {
return(
e("button", {type:"button",onClick:dropdownDetailsWrap(),className:'btn'},"Dropdown")
)}
}
ReactDOM.render(e(pageBody), document.querySelector('#page_body'));
Note: I know there are more elegant ways to make a dropdown, particularly using packages. Just trying to understand the basics with pure React for now.
Probably you can do like this provided you use the useState hook of react.
Wihtout using state, I doubt we can do this.
I am pasting the code below, check it out on codesandbox to explore more.
Here is the link Link
Code :
import React, { useState } from "react";
const e = React.createElement;
export default function App() {
return (
<div className="App">
<PageBody />
</div>
);
}
function PageBody(props) {
var buttonsToShow;
const [showButtons, setshowButtons] = useState(false);
const [buttons, setButtons] = useState([]);
const dropdownDetailsWrap = (props) => {
console.log("Wrap triggered");
buttonsToShow = dropdownDetails();
setButtons(buttonsToShow);
setshowButtons(true);
};
function dropdownDetails() {
console.log("dropdownDetails useEffect triggered");
return [
e("button", { type: "button", key: 1 }, "Hello"),
e("button", { type: "button", key: 2 }, "Goodbye")
];
}
var dropbutton = () => {
return e(
"button",
{ type: "button", onClick: dropdownDetailsWrap, className: "btn" },
"Dropdown"
);
};
return (
<div>
{dropbutton()} <br /> {showButtons && buttons}
</div>
);
}
Now,I know this is not the best version but still you can clean this code and use the logic. Also by using state your code does not become static, moreover you get to customize more things,

How am I using React hooks wrong here?

I'm building a simple Phonebook to go along with FullstackOpen course from University of Helsinki. My logs from the below code show me that my 'filterBy' state is always a render behind and I'm also getting errors from my contacts.filter that "contact.name.toUpperCase" is not a function. So i guess I have 2 questions.
Can I even do what they're trying to have me do without using class components? They haven't introduced them in the course yet, so I'm assuming yes. But I thought I heard in a tutorial once that class components have to be used when you need state to be in sync (e.g. using an input to filter an array of objects displayed on screen). But I can't get the filterBy state to actually be in sync with what's in the filter input.
I know my line starting "const contactsToShow . . ." is probably a less than optimal way to do it, if functional at all. How can this best be done?
Code:
import React, { useState } from 'react'
import Contact from './components/Contact'
const App = () => {
const [contacts, setContacts] = useState([
{ name:'Guy Fieri', number: '020-4837473'},
{ name:'Gordon Ramsay', number: '75749483832'},
{ name:'Mr. Tasty', number: '43-4982839'},
{ name:'Dude man', number: '11-33-448382'},
])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const [showAll, setShowAll] = useState(true)
const [filterBy, setFilterBy] = useState('');
const contactsToShow = showAll ? contacts : contacts.filter(contact => contact.name.toUpperCase().search(filterBy) !== -1)
const rows = () => contactsToShow.map(contact =>
<Contact
key={contact.name}
name={contact.name}
number={contact.number}
/>
)
const handleContactNameChange = (event) => {
console.log(event.target.value)
setNewName(event.target.value)
}
const handleContactNumberChange = (event) => {
console.log(event.target.value)
setNewNumber(event.target.value)
console.log(newNumber);
}
const handleFiltering = (event) => {
console.log(event.target.value)
setFilterBy(event.target.value)
setShowAll(false)
console.log(filterBy);
}
const addContact = (event) => {
event.preventDefault()
//
if(newName === '') return true
if(newNumber === '') return true
let dup = false
contacts.forEach(contact => {
if (contact.name === newName) dup = true
})
if (!dup) {
const contactObject = {
name: newName,
number: newNumber
}
setContacts(contacts.concat(contactObject))
setNewName('')
setNewNumber('')
}
}
return (
<div>
<h1>Phonebook</h1>
<form onSubmit={addContact}>
<div>
Filter:<input value={filterBy} onChange={handleFiltering}/>
</div>
Name: <input
value={newName}
onChange={handleContactNameChange}
/><br/>
Number: <input
value={newNumber}
onChange={handleContactNumberChange}
/><br/>
<button type="submit">save</button>
</form>
<ul>
{rows()}
</ul>
</div>
)
}
export default App
What can I try? I'm very new to React like this.
There are some changes needed in your code. I don't see why you are even getting this error contact.name.toUpperCase because there is nothing wrong with that.
You are converting the contact to upperCase but not filterBy.
Now, to answer your two questions:
You are using hooks that were introduced with react 16. This brought state to functional components. Your code will work fine with hooks or class component.
Whenever you make a change in any state variable (contact, newName, newNumber, showAll, filterBy), your component re-renders. This will call the rows() function and display the result that you want. So you can just push contactsToShow inside the function.
There is nothing breaking with your code. I don't know what Contact component was, so I changed it to div and added a console.log to see what's happening. It runs fine. Check it out:
https://codesandbox.io/embed/vibrant-dawn-o1r1y
You can let me know if you have more doubts, I'm here to help. :)

React useEffect Hook when only one of the effect's deps changes, but not the others

I have a functional component using Hooks:
function Component(props) {
const [ items, setItems ] = useState([]);
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
The component fetches some items on mount and saves them to state.
The component receives an itemId prop (from React Router).
Whenever the props.itemId changes, I want this to trigger an effect, in this case logging it to console.
The problem is that, since the effect is also dependent on items, the effect will also run whenever items changes, for instance when the items are re-fetched by pressing the button.
This can be fixed by storing the previous props.itemId in a separate state variable and comparing the two, but this seems like a hack and adds boilerplate. Using Component classes this is solved by comparing current and previous props in componentDidUpdate, but this is not possible using functional components, which is a requirement for using Hooks.
What is the best way to trigger an effect dependent on multiple parameters, only when one of the parameters change?
PS. Hooks are kind of a new thing, and I think we all are trying our best to figure out how to properly work with them, so if my way of thinking about this seems wrong or awkward to you, please point it out.
The React Team says that the best way to get prev values is to use useRef: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function Component(props) {
const [ items, setItems ] = useState([]);
const prevItemIdRef = useRef();
useEffect(() => {
prevItemIdRef.current = props.itemId;
});
const prevItemId = prevItemIdRef.current;
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, []);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(() => {
if(prevItemId !== props.itemId) {
console.log('diff itemId');
}
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ])
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
I think that this could help in your case.
Note: if you don't need the previous value, another approach is to write one useEffect more for props.itemId
React.useEffect(() => {
console.log('track changes for itemId');
}, [props.itemId]);
An easy way out is to write a custom hook to help us with that
// Desired hook
function useCompare (val) {
const prevVal = usePrevious(val)
return prevVal !== val
}
// Helper hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
and then use it in useEffect
function Component(props) {
const hasItemIdChanged = useCompare(props.itemId);
useEffect(() => {
console.log('item id changed');
}, [hasItemIdChanged])
return <></>
}
⚠️ NOTE: This answer is currently incorrect and could lead to unexpected bugs / side-effects. The useCallback variable would need to be a dependency of the useEffect hook, therefore leading to the same problem as OP was facing.
I will address it asap
Recently ran into this on a project, and our solution was to move the contents of the useEffect to a callback (memoized in this case) - and adjust the dependencies of both. With your provided code it looks something like this:
function Component(props) {
const [ items, setItems ] = useState([]);
const onItemIdChange = useCallback(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [items, props.itemId]);
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffect(onItemIdChange, [ props.itemId ]);
// Clicking the button should NOT log anything to console
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
So the useEffect just has the ID prop as its dependency, and the callback both the items and the ID.
In fact you could remove the ID dependency from the callback and pass it as a parameter to the onItemIdChange callback:
const onItemIdChange = useCallback((id) => {
if (items) {
const item = items.find(item => item.id === id);
console.log("Item changed to " item.name);
}
}, [items]);
useEffect(() => {
onItemIdChange(props.itemId)
}, [ props.itemId ])
I am a react hooks beginner so this might not be right but I ended up defining a custom hook for this sort of scenario:
const useEffectWhen = (effect, deps, whenDeps) => {
const whenRef = useRef(whenDeps || []);
const initial = whenRef.current === whenDeps;
const whenDepsChanged = initial || !whenRef.current.every((w, i) => w === whenDeps[i]);
whenRef.current = whenDeps;
const nullDeps = deps.map(() => null);
return useEffect(
whenDepsChanged ? effect : () => {},
whenDepsChanged ? deps : nullDeps
);
}
It watches a second array of dependencies (which can be fewer than the useEffect dependencies) for changes & produces the original useEffect if any of these change.
Here's how you could use (and reuse) it in your example instead of useEffect:
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffectWhen(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items, props.itemId ], [props.itemId])
Here's a simplified example of it in action, useEffectWhen will only show up in the console when the id changes, as opposed to useEffect which logs when items or id changes.
This will work without any eslint warnings, but that's mostly because it confuses the eslint rule for exhaustive-deps! You can include useEffectWhen in the eslint rule if you want to make sure you have the deps you need. You'll need this in your package.json:
"eslintConfig": {
"extends": "react-app",
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useEffectWhen"
}
]
}
},
and optionally this in your .env file for react-scripts to pick it up:
EXTEND_ESLINT=true
2022 answer
I know that this is an old question, but it's still worth an answer. Effects have received a lot of attention this year, and it's now clearer what you should / shouldn't do with effects than it was back then.
The new React docs, which are still in beta, cover the topic in length.
So, before answering the 'how', you must first answer the 'why', because there aren't actually many genuine use cases for running an effect only when certain dependencies change.
You might not need an effect
'You might not need an effect' is actually the title of the page that demonstrates that you might be using an effect for the wrong reasons. And the example you gave us is actually discussed in 'adjusting some state when a prop changes'
Your code can be simply rewritten without the second effect:
function Component(props) {
const [ items, setItems ] = useState([]);
// In a callback Hook to prevent unnecessary re-renders
const handleFetchItems = useCallback(() => {
fetchItemsFromApi().then(setItems);
}, []);
// Fetch items on mount
useEffect(() => {
handleFetchItems();
}, [handleFetchItems]);
// Find the item that matches in the list
const item = items.find(item => item.id === props.itemId);
return (
<Button onClick={handleFetchItems}>Fetch items</Button>
);
}
Indeed, you don't need an effect just to identify the item from the list. React already re-renders when items changes, so item can be re-computed in the component directly.
Note: if you really want to log to the console when the item changes, you can still add this code to your component:
useEffect(() => {
console.log("Item changed to ", item?.name);
}, [item])
Note how item is the only dependency of the effect, so it only runs when item changes. But this is not a good use case for an effect again.
Instead, if you want to do something in response to an event that occurs in your application, you should do this 👇
Separating Events from Effects
And yes, 'Separating Events from Effects' is also the title of a page in the new React docs! In the example you gave us, you want to write to the console whenever item changes. But I'm not naive and I know you might want to do more than this in a practice.
If you want to do something when something else changes, then you can use the new useEvent hook to create an event handler for it (or at least, use its polyfill since it hasn't been released yet). The event in question here is the itemId prop changing.
You can then change your second effect to:
// Handle a change of item ID
const onItemIdChange = useEvent((itemId) => {
const item = items?.find(item => item.id === itemId);
console.log("Item changed to " item.name);
});
// Trigger onItemIdChange when item ID changes
useEffect(() => {
onItemIdChange(props.itemId);
}, [props.itemId]);
The advantage of this method is that useEvent returns a stable function, so you don't need to add it as a dependency to your effect (it never changes). Also, the function you pass to useEvent has no dependency array: it can access all the properties, states or variables inside your component and it is always fresh (no stale values).
I still think that based on the example you gave us, you don't really need an effect. But for other genuine cases, at least you know how to extract events from your effects now and thus you can avoid hacking the dependency array.
Based on the previous answers here and inspired by react-use's useCustomCompareEffect implementation, I went on writing the useGranularEffect hook to solve a similar issue:
// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useGranularEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items ], [ props.itemId ])
implemented as (TypeScript):
export const useGranularEffect = (
effect: EffectCallback,
primaryDeps: DependencyList,
secondaryDeps: DependencyList
) => {
const ref = useRef<DependencyList>();
if (!ref.current || !primaryDeps.every((w, i) => Object.is(w, ref.current[i]))) {
ref.current = [...primaryDeps, ...secondaryDeps];
}
// eslint-disable-next-line react-hooks/exhaustive-deps
return useEffect(effect, ref.current);
};
Try it on codesandbox
The signature of useGranularEffect is the same as useEffect, except that the list of dependencies has been split into two:
primary dependencies: the effect only runs when these dependencies change
secondary dependencies: all the other dependencies used in the effect
In my opinion, it makes the case of running the effect only when certain dependencies change easier to read.
Notes:
Unfortunately, there is not linting rule to help you ensure that the two arrays of dependencies are exhaustive, so it is your responsibility to make sure you're not missing any
It is safe to ignore the linting warning inside the implementation of useGranularEffect because effect is not an actual dependency (it's the effect function itself) and ref.current contains the list of all dependencies (primary + secondary, which the linter cannot guess)
I'm using Object.is to compare dependencies so that it's consistent with the behaviour of useEffect, but feel free to use your own compare function or, better, to add a comparer as argument
UPDATE: useGranularEffect has now be published into the granular-hooks package. So just:
npm install granular-hooks
then
import { useGranularEffect } from 'granular-hooks'
I just tried this myself and it seems to me that you don't need to put things in the useEffect dependency list in order to have their updated versions. Meaning you can just solely put in props.itemId and still use items within the effect.
I created a snippet here to attempt to prove/illustrate this. Let me know if something is wrong.
const Child = React.memo(props => {
const [items, setItems] = React.useState([]);
const fetchItems = () => {
setTimeout(() => {
setItems((old) => {
const newItems = [];
for (let i = 0; i < old.length + 1; i++) {
newItems.push(i);
}
return newItems;
})
}, 1000);
}
React.useEffect(() => {
console.log('OLD (logs on both buttons) id:', props.id, 'items:', items.length);
}, [props.id, items]);
React.useEffect(() => {
console.log('NEW (logs on only the red button) id:', props.id, 'items:', items.length);
}, [props.id]);
return (
<div
onClick={fetchItems}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'orange',
textAlign: "center"
}}
>
Click me to add a new item!
</div>
);
});
const Example = () => {
const [id, setId] = React.useState(0);
const updateId = React.useCallback(() => {
setId(old => old + 1);
}, []);
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Child
id={id}
/>
<div
onClick={updateId}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'red',
textAlign: "center"
}}
>Click me to update the id</div>
</div>
);
};
ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
From the example provided, your effect does not depend on items and itemId, but one of the items from the collection.
Yes, you need items and itemId to get that item, but it does not mean you have to specify them in the dependency array.
To make sure it is executed only when the target item changes, you should pass that item to dependency array using the same lookup logic.
useEffect(() => {
if (items) {
const item = items.find(item => item.id === props.itemId);
console.log("Item changed to " item.name);
}
}, [ items.find(item => item.id === props.itemId) ])

Categories