React Hooks useState+useEffect+event gives stale state - javascript

I'm trying to use an event emitter with React useEffect and useState, but it always gets the initial state instead of the updated state. It works if I call the event handler directly, even with a setTimeout.
If I pass the value to the useEffect() 2nd argument, it makes it work, however this causes a resubscription to the event emitter every time the value changes (which is triggered off of keystrokes).
What am I doing wrong? I've tried useState, useRef, useReducer, and useCallback, and couldn't get any working.
Here's a reproduction:
import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";
let ee = new EventEmitter();
const initialValue = "initial value";
function App(props) {
const [value, setValue] = useState(initialValue);
// Should get the latest value, both after the initial server load, and whenever the Codemirror input changes.
const handleEvent = (msg, data) => {
console.info("Value in event handler: ", value);
// This line is only for demoing the problem. If we wanted to modify the DOM in this event, we would instead call some setState function and rerender in a React-friendly fashion.
document.getElementById("result").innerHTML = value;
};
// Get value from server on component creation (mocked)
useEffect(() => {
setTimeout(() => {
setValue("value from server");
}, 1000);
}, []);
// Subscribe to events on component creation
useEffect(() => {
ee.on("some_event", handleEvent);
return () => {
ee.off(handleEvent);
};
}, []);
return (
<React.Fragment>
<CodeMirror
value={value}
options={{ lineNumbers: true }}
onBeforeChange={(editor, data, newValue) => {
setValue(newValue);
}}
/>
{/* Everything below is only for demoing the problem. In reality the event would come from some other source external to this component. */}
<button
onClick={() => {
ee.emit("some_event");
}}
>
EventEmitter (doesnt work)
</button>
<div id="result" />
</React.Fragment>
);
}
export default App;
Here's a code sandbox with the same in App2:
https://codesandbox.io/s/ww2v80ww4l
App component has 3 different implementations - EventEmitter, pubsub-js, and setTimeout. Only setTimeout works.
Edit
To clarify my goal, I simply want the value in handleEvent to match the Codemirror value in all cases. When any button is clicked, the current codemirror value should be displayed. Instead, the initial value is displayed.

value is stale in the event handler because it gets its value from the closure where it was defined. Unless we re-subscribe a new event handler every time value changes, it will not get the new value.
Solution 1: Make the second argument to the publish effect [value]. This makes the event handler get the correct value, but also causes the effect to run again on every keystroke.
Solution 2: Use a ref to store the latest value in a component instance variable. Then, make an effect which does nothing but update this variable every time value state changes. In the event handler, use the ref, not value.
const [value, setValue] = useState(initialValue);
const refValue = useRef(value);
useEffect(() => {
refValue.current = value;
});
const handleEvent = (msg, data) => {
console.info("Value in event handler: ", refValue.current);
};
https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often
Looks like there are some other solutions on that page which might work too. Much thanks to #Dinesh for the assistance.

Updated Answer.
The issue is not with hooks. Initial state value was closed and passed to EventEmitter and was used again and again.
It's not a good idea to use state values directly in handleEvent. Instead we need to pass them as parameters while emitting the event.
import React, { useState, useEffect } from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";
let ee = new EventEmitter();
const initialValue = "initial value";
function App(props) {
const [value, setValue] = useState(initialValue);
const [isReady, setReady] = useState(false);
// Should get the latest value
function handleEvent(value, msg, data) {
// Do not use state values in this handler
// the params are closed and are executed in the context of EventEmitter
// pass values as parameters instead
console.info("Value in event handler: ", value);
document.getElementById("result").innerHTML = value;
}
// Get value from server on component creation (mocked)
useEffect(() => {
setTimeout(() => {
setValue("value from server");
setReady(true);
}, 1000);
}, []);
// Subscribe to events on component creation
useEffect(
() => {
if (isReady) {
ee.on("some_event", handleEvent);
}
return () => {
if (!ee.off) return;
ee.off(handleEvent);
};
},
[isReady]
);
function handleClick(e) {
ee.emit("some_event", value);
}
return (
<React.Fragment>
<CodeMirror
value={value}
options={{ lineNumbers: true }}
onBeforeChange={(editor, data, newValue) => {
setValue(newValue);
}}
/>
<button onClick={handleClick}>EventEmitter (works now)</button>
<div id="result" />
</React.Fragment>
);
}
export default App;
Here is a working codesandbox

useCallback should have worked here.
import React, { useState, useEffect, useCallback } from "react";
import PubSub from "pubsub-js";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import EventEmitter from "events";
let ee = new EventEmitter();
const initialValue = "initial value";
function App(props) {
const [value, setValue] = useState(initialValue);
// Should get the latest value
const handler = (msg, data) => {
console.info("Value in event handler: ", value);
document.getElementById("result").innerHTML = value;
};
const handleEvent = useCallback(handler, [value]);
// Get value from server on component creation (mocked)
useEffect(() => {
setTimeout(() => {
setValue("value from server");
}, 1000);
}, []);
// Subscribe to events on component creation
useEffect(() => {
PubSub.subscribe("some_event", handleEvent);
return () => {
PubSub.unsubscribe(handleEvent);
};
}, [handleEvent]);
useEffect(() => {
ee.on("some_event", handleEvent);
return () => {
ee.off(handleEvent);
};
}, []);
return (
<React.Fragment>
<CodeMirror
value={value}
options={{ lineNumbers: true }}
onBeforeChange={(editor, data, newValue) => {
setValue(newValue);
}}
/>
<button
onClick={() => {
ee.emit("some_event");
}}
>
EventEmitter (works)
</button>
<button
onClick={() => {
PubSub.publish("some_event");
}}
>
PubSub (doesnt work)
</button>
<button
onClick={() => {
setTimeout(() => handleEvent(), 100);
}}
>
setTimeout (works!)
</button>
<div id="result" />
</React.Fragment>
);
}
export default App;
Check the codesandbox here https://codesandbox.io/s/react-base-forked-i9ro7

Related

Value of variable outside of useEffect hook has old data

What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.
import { useEffect, useReducer } from "react";
let elements: any[] = [];
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
elements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
console.log('elements 1:', elements)
}, [state]);
console.log('elemets 2:', elements)
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.
The strange thing is that the problem doesn't happen if you comment out this if-statement:
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.
Question: What could be the problem, and how to fix it? I want to render the current length of elements.
Live code:
It's happening because of the elements variable is not a state, so it's not reactive.
Create a state for the elements:
const [elements, setElements] = useState<HTMLButtonElement[]>([])
And use this state to handle the elements.
import { useEffect, useReducer, useState } from "react";
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
const [elements, setElements] = useState<HTMLButtonElement[]>([])
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
let newElements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
setElements(newElements)
console.log("elements 1:", elements?.length);
}, [state]);
console.log("elemets 2:", elements?.length);
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements?.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
Your useEffect() runs after your component has rendendered. So the sequence is:
You type something into input, that triggers handleInputChange
handleInputChange then updates your state using updateState()
The state update causes a rerender, so App is called App()
console.log('elemets 2:', elements.length) runs and logs elements as 0 as it's still empty
App returns the new JSX
Your useEffect() callback runs, updating elements
Notice how we're only updating the elements after you've rerendered and App has been called.
The state of your React app should be used to describe your UI in React. Since elements isn't React state, it has a chance of becoming out of sync with the UI (as you've seen), whereas using state doesn't have this issue as state updates always trigger a UI update. Consider making elements part of your state. If it needs to be accessible throughout your entire App, you can pass it down as props to children components, or use context to make it accessible throughout all your components.
With that being said, I would make the following updates:
Add elements to your state
Remove your useEffect() with the dependency of [state]. If we were to update the elements state within this effect, then that would trigger another rerender directly after the one we just did for the state update. This isn't efficient, and instead, we can tie the update directly to your event handler. See You Might Not Need an Effect for more details and dealing with other types of scenarios:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useEffect, useReducer} = React;
const App = () => {
const initialState = {keyEvent: {}, value: "Initial state", elements: []};
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates}),
initialState
);
function searchDOM(value) {
const selectors = "button";
const pattern = new RegExp(value === "" ? "^$" : value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
return nodeWithText?.textContent?.match(pattern);
}
return false;
}
);
}
function handleInputChange(event) {
updateState({
value: event.target.value,
elements: searchDOM(event.target.value)
});
}
function isCommand(event) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event) {
if (isCommand(event)) {
updateState({
keyEvent: event
});
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
console.log("elements:", state.elements.length);
return (
<div>
<input id="input" type="text" onChange={handleInputChange} value={state.value} />
<div id="count">{state.elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
ReactDOM.createRoot(document.body).render(<App />);
</script>
useEffect triggered after react completed its render phase & flush the new changes to the DOM.
In your case you have two useEffects. The first one register your event lister which will then update your component state when input field change. This triggers a state update.( because of the setState )
So React will start render the component again & finish the cycle. And now you have 2nd useEffect which has state in dependency array. Since the state was updated & the new changes are committed to the DOM, react will execute 2nd useEffect logic.
Since your 2nd useEffect just assign some values to a normal variable React will not go re render your component again.
Based on your requirement you don't need a 2nd useEffect. You can use a useMemo,
let elements = useMemo(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
})
}, [state])
Note: You don't need to assign your elements into another state. It just create another unwanted re render in cycle. Since you are just doing a calculation to find out the element array you can do it with the useMemo

UseEffect runs on first render without being called using Context [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
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>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("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>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

Debouncing and Timeout in React

I have a here a input field that on every type, it dispatches a redux action.
I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?
useTimeout
import { useCallback, useEffect, useRef } from "react";
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}, [delay]);
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
return { reset, clear };
}
useDebounce
import { useEffect } from "react";
import useTimeout from "./useTimeout";
export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay);
useEffect(reset, [...dependencies, reset]);
useEffect(clear, []);
}
Form component
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const { handleChangeProductName = () => {} } = props;
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
useDebounce(() => handleChangeProductName(e.target.value), 1000, [
e.target.value,
]);
}}
/>
);
}
I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.
Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example usage:
export default function ProductInputs({ handleChangeProductName }) {
const debouncedHandler = useCallback(
debounce(handleChangeProductName, 200),
[handleChangeProductName]
);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandler(e.target.value);
}}
/>
);
}
If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.
Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...
In my opinion, useDebounce should return a "debounced" version of callback
Here is my take on useDebounce:
export default function useDebounce(callback, delay) {
const [debounceReady, setDebounceReady] = useState(true);
const debouncedCallback = useCallback((...args) => {
if (debounceReady) {
callback(...args);
setDebounceReady(false);
}
}, [debounceReady, callback]);
useEffect(() => {
if (debounceReady) {
return undefined;
}
const interval = setTimeout(() => setDebounceReady(true), delay);
return () => clearTimeout(interval);
}, [debounceReady, delay]);
return debouncedCallback;
}
Usage will look something like:
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const handleChangeProductName = useCallback((value) => {
if (props.handleChangeProductName) {
props.handleChangeProductName(value);
} else {
// do something else...
};
}, [props.handleChangeProductName]);
const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandleChangeProductName(e.target.value);
}}
/>
);
}
Debouncing onChange itself has caveats. Say, it must be uncontrolled component, since debouncing onChange on controlled component would cause annoying lags on typing.
Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.
With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:
const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);
useEffect(() => {
if (isValueSettled) {
props.onChange(text);
}
}, [text, isValueSettled]);
...
<input value={value} onChange={({ target: { value } }) => setText(value)}
And useIsSetlled itself will debounce:
function useIsSettled(value, delay = 500) {
const [isSettled, setIsSettled] = useState(true);
const isFirstRun = useRef(true);
const prevValueRef = useRef(value);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
setIsSettled(false);
prevValueRef.current = value;
const timerId = setTimeout(() => {
setIsSettled(true);
}, delay);
return () => { clearTimeout(timerId); }
}, [delay, value]);
if (isFirstRun.current) {
return true;
}
return isSettled && prevValueRef.current === value;
}
where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).
And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.

How do I automatically trigger a button click after 5 seconds with React?

I have an 'Accept' button which I would like to be automatically clicked after 5 seconds. I'm using React with Next.js. The button code is:
<button name="accept" className="alertButtonPrimary" onClick={()=>acceptCall()}>Accept</button>
If I can't do this, I would like to understand why, so I can improve my React and Next.js skills.
I'm guessing you want this activated 5 seconds after render, in that case, put a setTimeout inside of the useEffect hook, like so. this will call whatever is in the hook after the render is complete.
Although this isn't technically activating the button click event.
useEffect(() => {
setTimeout(() => {
acceptCall()
}, timeout);
}, [])
in that case you should use a ref like so,
const App = () => {
const ref = useRef(null);
const myfunc = () => {
console.log("I was activated 5 seconds later");
};
useEffect(() => {
setTimeout(() => {
ref.current.click();
}, 5000); //miliseconds
}, []);
return (
<button ref={ref} onClick={myfunc}>
TEST
</button>
);
};
Hopefully, this is what you are looking for.
https://codesandbox.io/s/use-ref-forked-bl7i0?file=/src/index.js
You could create a ref for the <button> and set a timeout inside of an effect hook to call the button click event after 5 seconds.
You could throw in a state hook to limit the prompt.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const buttonRef = useRef("accept-button");
const [accepted, setAccepted] = useState(false);
const acceptCall = (e) => {
alert("Accepted");
};
const fireEvent = (el, eventName) => {
const event = new Event(eventName, { bubbles: true });
el.dispatchEvent(event);
};
useEffect(() => {
if (!accepted) {
setTimeout(() => {
if (buttonRef.current instanceof Element) {
setAccepted(true);
fireEvent(buttonRef.current, "click");
}
}, 5000);
}
}, [accepted]);
return (
<div className="App">
<button
name="accept"
className="alertButtonPrimary"
ref={buttonRef}
onClick={acceptCall}
>
Accept
</button>
</div>
);
};
export default App;

React: How to skip useEffect on first render [duplicate]

This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const onChange = ({target}) => {
console.log("onChange")
setValue(target.value)
}
return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
initialValue: PropTypes.any
}
const DummyForm = ({dummy, onChange}) => {
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [foo, bar])
return (
<div>
<input type="text" {...fooBinding} />
<div>{foo}</div>
<input type="text" {...barBinding} />
<div>{bar}</div>
</div>
)
}
function App() {
return (
<div className="App">
<header className="App-header">
<DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
</header>
</div>
);
}
However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?
Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.
onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
Line 118: React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
r # backend.js:1
printWarnings # webpackHotDevClient.js:120
handleWarnings # webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage # webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent # eventtarget.js:56
(anonymous) # main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage # main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit # emitter.js:53
WebSocketTransport.ws.onmessage # websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
// do things after first render
}
});
As for the warning you were getting:
React Hook useEffect has a missing dependency: 'onChange'
The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.
If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:
export const useComponentDidMount = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};
In your component you can use it like:
const isComponentMounted = useComponentDidMount();
useEffect(() => {
if(isComponentMounted) {
// Do something
}
}, [someValue])
In your case it will be:
const DummyForm = ({dummy, onChange}) => {
const isComponentMounted = useComponentDidMount();
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
if(isComponentMounted) {
console.log("onChange callback")
onChange({foo, bar})
}
}, [foo, bar])
return (
// code
)
}
Let me know if it helps.
I create a simple hook for this
https://stackblitz.com/edit/react-skip-first-render?file=index.js
It is based on paruchuri-p
const useSkipFirstRender = (fn, args) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('running')
return fn();
}
}, args)
useEffect(() => {
isMounted.current = true
}, [])
}
The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.
Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.
It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [count])
I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?
const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
return (
<div>
<input type="text" value={valueOne} onChange={updateOne} />
<div>{valueOne}</div>
<input type="text" value={valueTwo} onChange={updateTwo} />
<div>{valueTwo}</div>
</div>
)
}
function App() {
const [inputOne, setInputOne] = useState("");
const [inputTwo, setInputTwo] = useState("");
return (
<div className="App">
<header className="App-header">
<DummyForm
valueOne={inputOne}
updateOne={(e) => {
setInputOne(e.target.value);
}}
valueTwo={inputTwo}
updateTwo={(e) => {
setInputTwo(e.target.value);
}}
/>
</header>
</div>
);
}
Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.

Categories