React custom mount hook - reactjs

I always forget to add empty dependencies array in my useEffect hook in React to run the effect only once. That's why I decided to use a custom hook with a clear intention in it's name to run only once on mount. But I don't understand why is it running twice?
import React from "react";
import "./styles.css";
function useOnMount(f) {
const isMountedRef = React.useRef(false);
console.log("ref1", isMountedRef.current);
if (!isMountedRef.current) {
f();
isMountedRef.current = true;
console.log("ref2", isMountedRef.current);
}
}
export default function App() {
useOnMount(() => {
console.log("useOnMount");
});
return <div>Hello useOnMount</div>;
}
Here's the output:
ref1 false
useOnMount
ref2 true
ref1 false
useOnMount
ref2 true
I use ref hook to keep mutable flag between renders. But I can't understand why isMountedRef.current is true on the first render and reverts to false on the second render 🤔

You keep forgetting the dependencies array? Why not just remember to write it once in your custom hook?
function useOnce (once) {
return useEffect(once, [])
}
Using it in your App -
function App () {
useOnce(_ => console.log("useOnce"))
return <div>hello</div>
}
Here's a demo -
const { useEffect, useState } = React
function useOnce (once) {
return useEffect(once, [])
}
function App () {
useOnce(_ => alert("Wake up. It's time to make candy!"))
const [candy, setCandy] =
useState(0)
const earn =
<button onClick={_ => setCandy(candy + 1)}>
Make candy
</button>
const spend =
<button onClick={_ => setCandy(candy - 10)}>
Buy Chocolate (10)
</button>
return <div>
<b>Candy Box</b>
<p>Alert will only appear one time after component mounts</p>
<p>
{earn}
{(candy >= 10) && spend}
</p>
<p>You have {candy} candies</p>
</div>
}
ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

This one works for me and doesn't generate react-hooks/exhaustive-deps warnings.
function useOnce (once) {
const [ res ] = useState(once)
return res
}
Usage:
function App () {
useOnce(() => {
console.log("useOnce")
// use any App props or result of other hooks
...
})
return <div>hello</div>
}
It was recommended here for another task but it works well as a componentDidMount replacement. Any parameters may be used inside the function. Just make sure you really don't need to monitor changes of props.

Related

In my React App getting firebase Google login Warning in the console, how can I fix this Warning? [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.
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]);

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).
function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
const [controls, setControls] = useState<any>([]);
const [tip, setTip] = useState<string>("0");
function customTipChanged(percent: string) {
setTip(percent);
}
//Build controls
function buildControls()
{
let controlList: any[] = [];
controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
setControls(controlList);
}
useEffect(()=>{
console.log("TipSelector: useEffect");
buildControls();
return ()=> {
console.log("unmounts");
}
},[])
console.log("TipSelector: Render -> "+tip);
return (
<div className={styles.tipSelector}>
<span className={globalStyles.label}>Select Tip %</span>
<div className={styles.btnContainer}>
{
controls
}
</div>
</div>
);
}
If I move the creation of the input directly into the return() statement the value is updated properly.
I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.
See:
https://codesandbox.io/s/naughty-http-d38w9
e.g.:
import { useState, useEffect } from "react";
import CustomInput from "./Input";
function TipSelector({ selections, onTipChanged }) {
const [controls, setControls] = useState([]);
//Build controls
function buildControls() {
let controlList = [];
controlList.push(<CustomInput />);
controlList.push(<CustomInput />);
setControls(controlList);
}
useEffect(() => {
buildControls();
return () => {
console.log("unmounts");
};
}, []);
return (
<div>
<span>Select Tip %</span>
<div>{controls}</div>
</div>
);
}
export default TipSelector;
import { useState, useEffect } from "react";
function CustomInput() {
const [tip, setTip] = useState("0");
function customTipChanged(percent) {
setTip(percent);
}
return (
<input
value={tip.toString()}
onChange={(event) => {
customTipChanged(event.target.value);
}}
></input>
);
}
export default CustomInput;
You are only calling buildControls once, where the <input ... gets its value only that single time.
Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.
I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

Avoid runnning an effect hook when Context get updated

I have a component MyContainer which has a state variable (defined via useState hook), defines a context provider to which it passes the state variable as value and contains also 2 children, MySetCtxComponent and MyViewCtxComponent.
MySetCtxComponent can change the value stored in the context invoking a set function which is also passed as part of the context, BUT DOES NOT RENDER it.
MyViewCtxComponent, on the contrary, RENDERS the value stored in the context.
MySetCtxComponent defines also an effect via useEffect hook. This effect is, for instance, used to update the value of the context at a fixed interval of time.
So the code of the 3 components is this
MyContainer
export function MyContainer() {
const [myContextValue, setMyContextValue] = useState<string>(null);
const setCtxVal = (newVal: string) => {
setMyContextValue(newVal);
};
return (
<MyContext.Provider
value={{ value: myContextValue, setMyContextValue: setCtxVal }}
>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
);
}
MySetCtxComponent
(plus a global varibale to make the example simpler)
let counter = 0;
export function MySetCtxComponent() {
const myCtx = useContext(MyContext);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter);
counter++;
}, 3000);
return () => clearInterval(intervalID);
}, [myCtx]);
return <button onClick={() => (counter = 0)}>Reset</button>;
}
MyViewCtxComponent
export function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the contex: {myCtx.value}
</div>
);
}
Now my problem is that, in this way, everytime the context is updated the effect of MySetCtxComponent is run again even if this is not at all required since MySetCtxComponent does not need to render when the context is updated. But, if I remove myCtx from the dependency array of the useEffect hook (which prevents the effect hook when the context get updated), then I get an es-lint warning such as React Hook useEffect has a missing dependency: 'myCtx'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
Finally the question: is this a case where it is safe to ignore the warning or do I have a fundamental design error here and maybe should opt to use a store? Consider that the example may look pretty silly, but it is the most stripped down version of a real scenario.
Here a stackblitz to replicate the case
One pattern for solving this is to split the context in two, providing one context for actions and another for accessing the context value. This allows you to fulfill the expected dependency array of the useEffect correctly, while also not running it unnecessarily when only the context value has changed.
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
)
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
setContextState("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
return (
<div>
This is the value of the context: {contextState}
</div>
);
}
ReactDOM.render(
<MyContainer />,
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>
The problem is what you're passing to the useEffect dependency array in MySetCtxComponent. You should only pass the update function as shown below.
However, personally I would destructure out the setter as it's more readable and naturally avoids this issue.
const { useState, createContext, useContext, useEffect, useRef, useCallback } = React;
const MyContext = createContext();
function MyContainer() {
const [myContextValue, setMyContextValue] = useState(null);
// this function is currently unnecessary, but left in because I assume you change the functions default behvaiour in your real code
// also this should be wrapped in a useCallback if used
const setCtxVal = useCallback((newVal: string) => {
setMyContextValue(newVal);
}, [setMyContextValue]);
return (
<MyContext.Provider value={{ value: myContextValue, setMyContextValue: setCtxVal }}>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
)
}
function MySetCtxComponent() {
const myCtx = useContext(MyContext);
// or const { setMyContextValue } = useContext(MyContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter.current);
// or setMyContextValue("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [myCtx.setMyContextValue, /* or setMyContextValue */]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the context: {myCtx.value}
</div>
);
}
ReactDOM.render(
<MyContainer />,
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>

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Why useState in React Hook not update state

When i try example from React Hook, i get a problem about useState.
In code below, when click button, i add event for document and check value of count.
My expect is get count in console.log and view as the same. But actual, i got old value (init value) in console & new value in view . I can not understand why count in view changed and count in callback of event not change.
One more thing, when i use setCount(10); (fix a number). And click button many time (>2), then click outside, i got only 2 log from checkCount. Is it React watch count not change then don't addEventListener in next time.
import React, { useState } from "react";
function Example() {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
console.log("value set is ", count);
document.addEventListener("click", checkCount);
};
const checkCount = () => {
console.log(count);
};
return (
<div>
<p>You clicked {count} times</p>
<p>Click button first then click outside button and see console</p>
<button onClick={() => add()}>Click me</button>
</div>
);
}
export default Example;
If you want to capture events outside of your component using document.addEventListener, you will want to use the useEffect hook to add the event, you can then use the useState to determine if your capturing or not.
Notice in the useEffect I'm passing [capture], this will make it so the useEffect will get called when this changes, a simple check for this capture boolean determines if we add the event or not.
By using useEffect, we also avoid any memory leaks, this also copes with when your unmount the component, it knows to remove the event too.
const {useState, useEffect} = React;
function Test() {
const [capture, setCapture] = useState(false);
const [clickInfo, setClickInfo] = useState("Not yet");
function outsideClick() {
setClickInfo(Date.now().toString());
}
useEffect(() => {
if (capture) {
document.addEventListener("click", outsideClick);
return () => {
document.removeEventListener("click", outsideClick);
}
}
}, [capture]);
return <div>
<p>
Click start capture, then click anywhere, and then click stop capture, and click anywhere.</p>
<p>{capture ? "Capturing" : "Not capturing"}</p>
<p>Clicked outside: {clickInfo}</p>
<button onClick={() => setCapture(true)}>
Start Capture
</button>
<button onClick={() => setCapture(false)}>
Stop Capture
</button>
</div>
}
ReactDOM.render(<React.Fragment>
<Test/>
</React.Fragment>, document.querySelector('#mount'));
p { user-select: none }
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="mount"></div>
#Keith i understand your example but when i apply get some confuse. In origin, i always call function is handleClick and still call it after run handleClickOutside but now i don't know how to apply that with hook.
This is my code that i want insted of Hook
class ClickOutSide extends Component {
constructor(props) {
super(props)
this.wrapperRef = React.createRef();
this.state = {
active: false
}
}
handleClick = () => {
if(!this.state.active) {
document.addEventListener("click", this.handleClickOut);
document.addEventListener("contextmenu", this.handleClickOut);
this.props.clickInside();
} else {
document.removeEventListener("click", this.handleClickOut);
document.removeEventListener("contextmenu", this.handleClickOut);
}
this.setState(prevState => ({
active: !prevState.active,
}));
};
handleClickOut = event => {
const { target } = event;
if (!this.wrapperRef.current.contains(target)) {
this.props.clickOutside();
}
this.handleClick()
}
render(){
return (
<div
onDoubleClick={this.props.onDoubleClick}
onContextMenu={this.handleClick}
onClick={this.handleClick}
ref={this.wrapperRef}
>
{this.props.children}
</div>
)
}
}
export default ClickOutSide

Resources