I'm looking to have child components update a state value through the useContext hook when they are unmounted from the DOM, but am unable to make sense of the order in which their clean up functions (the functions returned from a useEffect hook) are called.
What I'm trying to achieve in the example code is to have the switches all individually able to modify the count of switches in an "on" state so that it's accurate, but also when they are removed from the DOM by the "Remove all switches" button they do the same. It seems like what is happening is that those updates when they are removed from the DOM are not happening in the order I'd expect, and the final state of that counter is not accurate.
If you add several of the switches, turn several of them "on", and then remove all of them, you'll see that the counter in the parent component is not set correctly. I've added a console.log call in the returned function from the useEffect hook, what's especially confusing here to me is that it looks like the counter is being set correctly, but somehow not in the correct order?
(Here is the example code running on StackBlitz.)
import React, { useState, createContext, useContext, useEffect } from 'react';
import './style.css';
const SwitchContext = createContext();
const ChildComponent = () => {
const [switchOn, setSwitchOn] = useState(false);
const { numberOfSwitchesOn, setNumberOfSwitchesOn } =
useContext(SwitchContext);
useEffect(() => {
return () => {
if (switchOn) {
console.log(
`Setting number of switches on to: ${numberOfSwitchesOn - 1}`
);
setNumberOfSwitchesOn(numberOfSwitchesOn - 1);
}
};
}, [switchOn]);
const toggleSwitch = () => {
if (switchOn) {
// We are turning off this switch...
setNumberOfSwitchesOn(numberOfSwitchesOn - 1);
} else {
// We are turning it on...
setNumberOfSwitchesOn(numberOfSwitchesOn + 1);
}
setSwitchOn(!switchOn);
};
return (
<button onClick={() => toggleSwitch()}>{switchOn ? 'on' : 'off'}</button>
);
};
export default function App() {
const [numberOfSwitches, setNumberOfSwitches] = useState(1);
const [numberOfSwitchesOn, setNumberOfSwitchesOn] = useState(0);
let switches = [];
for (let i = 0; i < numberOfSwitches; i++) {
switches.push(<ChildComponent key={`switch-${i}`} />);
}
return (
<SwitchContext.Provider
value={{
numberOfSwitchesOn,
setNumberOfSwitchesOn,
}}
>
<div>
<button onClick={() => setNumberOfSwitches(numberOfSwitches + 1)}>
Add switch
</button>
<button onClick={() => setNumberOfSwitches(0)}>
Remove all switches
</button>
<p>Number of switches on: {numberOfSwitchesOn}</p>
{switches}
</div>
</SwitchContext.Provider>
);
}
This is because they are all updating the same value simultaneously which react does not like. If you want to make sure each ChildComponent is working with the actual latest value you could use functional updates as described in the react docs
useEffect(() => {
return () => {
if (switchOn) {
console.log(
`Setting number of switches on to: ${numberOfSwitchesOn - 1}`
);
setNumberOfSwitchesOn((prev) => prev - 1);
}
};
}, [switchOn]);
Have a look at Your stackblitz sample edited
Related
Assume I have 3 queries x, y and z. I want to update the query x only when it is in view. Is there any way to identify it ?
I tried to maintain the view query key as a global state, but it seems not working fine. Is there any way to identify without maintaining viewing query key as global state.
Is there any possible way to get list of viewing queries ???
First of all, you need to hold the state of the component if it's in the viewport or not. The IntersectionObserver API allows you to detect when an element enters or leaves the viewport, which you can use to update the state of your component.
Then you can use that state as a key in your useQuery. Also you can use it in enabled option if you want to prevent refetch when item is not on the viewport.
import React, { useState, useEffect, useRef } from 'react';
import { useQuery } from 'react-query';
function Test() {
const [isInViewport, setIsInViewport] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInViewport(true);
} else {
setIsInViewport(false);
}
});
});
observer.observe(ref.current);
return () => {
observer.unobserve(ref.current);
};
}, []);
useQuery(
[isInViewport],
() => (
/// your query
),
{
enabled: isInViewport,
},
);
return (
<div ref={ref}>
<div>{isInViewport ? 'In viewport' : 'Not in viewport'}</div>
</div>
);
}
export default Test;
, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)
I am trying on React.js but what I would like does not work. Here is my code :
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
setCount(count + 1);
return (
<div>
<p>value of count : {count} fois</p>
</div>
);
}
export default App;
I got :
Too many re-renders. React limits the number of renders to prevent an infinite loop.
You can see the error there : https://codesandbox.io/s/nostalgic-engelbart-tzbxv?file=/src/App.js:0-244
Thank you for your help !
You're creating an infinite loop. Every time state updates, the component is re-rendered. The component is rendering, then it's calling setCount the state updates, then the component renders, and on and on.
You can use useEffect to update the state after it's rendered the first time.
useEffect(() => {
setCount(count + 1)
},[])
I'm not sure what the point of this would be though, since you can just pass the initial value into useState
You are calling setCount in the body of the component, which is essentially the render function if thinking in a class based component. Therefore it is causing an infinite loop of setting a new value, rerender, setting a new value, rerender, and so on.
You should only be calling setCount from some sort of event, e.g. a button click or an effect, e.g. using useEffect
Whenever you change the state then rerender is triggered.
In your case, when you are calling setCount on the count and the count state is changing and it is triggering rerender and this cycle is continued. That's why this error.
Try this example, where the setCount is called when only once and the value is being changed as it supposed to be.
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []);
return (
<div>
<p>value of count : {count} fois</p>
</div>
);
}
export default App;
Here is another example where count is increased every time the button is pressed:
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []);
return (
<div>
<p>value of count : {count} fois</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Increase
</button>
</div>
);
}
export default App;
Output:
Codesandbox Example
The reason you are getting the error is that calling setCount(count + 1); will increase the state count forever. Try putting it on a condition, for example in this codesandbox/code below, the state count is increased by 1 every time you click the button. You could also increase state if (count < 10), inside of a setInterval or under a wide variety of conditions. Just not infinitely! ;)
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount(count + 1);
};
return (
<div>
<p>value of count : {count} fois</p>
<button onClick={increaseCount}>start count</button>
</div>
);
}
im trying to call a function called deleteTask inside the Context Provider, from a component that consumes the context using the useContext hook, which deletes a certain item from an array in the state of the context provider, but when i do it, the state of the provider doesnt change at all, i try to follow the problem and the function excecutes but it seems like if it was excecuting in the scope of a copied Provider? Also tried a function to add a task and im having the same issue. I also added a function to set the active task, and i dont know why that one did work, while the others dont. I dont really know whats happening, here is the code, pleeeeease help me:
tasks-context.jsx
import React, { useState } from 'react';
import { useEffect } from 'react';
const dummyTasks = [{
task: {
text: 'hello',
},
key: 0,
isActive: false
},
{
task: {
text: 'hello 2',
},
key: 1,
isActive: false
}];
export const TasksContext = React.createContext({ });
export const TasksProvider = ( props ) => {
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
//NOT WORKING
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
//THIS ONE WORKS (??)
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
ficha.isActive = false;
}
return task;
});
setTask( newTaskArray );
};
return ( <TasksContext.Provider
value={{ tasks,
activeTask,
addTask,
deleteTask,
handleSelectTask}}>
{props.children}
</TasksContext.Provider>
);
};
the "main"
Main.jsx
import React from 'react';
import './assets/styles/gestion-style.css';
import './assets/styles/icons.css';
import { TasksProvider } from '../../Context/tasks-context';
import TaskContainer from './components/taskContainer.jsx';
function Main( props ) {
return (
<TasksProvider>
<TaskContainer />
</TasksProvider>
);
}
the task container maps the array of tasks:
TaskContainer.jsx
import React, { useContext, useEffect } from 'react';
import TaskTab from './TaskTab';
import { TasksContext } from '../../Context/tasks-context';
function TaskContainer( props ) {
const { tasks } = useContext( TasksContext );
return (
<div className="boxes" style={{ maxWidth: '100%', overflow: 'hidden' }}>
{tasks? tasks.map( taskTab=>
( <TaskTab task={taskTab.task} isActive={taskTab.isActive} key={taskTab.key} taskTabKey={taskTab.key} /> ))
:
null
}
</div>
);
}
export default TaskContainer;
And the task component from which i call the context function to delete:
TaskTab.jsx
import React, { useContext } from 'react';
import { TasksContext } from '../../Context/tasks-context';
function TaskTab( props ) {
let { task, isActive, taskTabKey } = props;
const { handleSelectTask, deleteTask } = useContext( TasksContext );
const selectTask = ()=>{
handleSelectTask( task, taskTabKey );
};
const handleDelete = () =>{
deleteTask( taskTabKey );
};
return (
<div onClick={ selectTask }>
<article className={`${task.type} ${isActive ? 'active' : null}`}>
<p className="user">{task.text}</p>
<button onClick={handleDelete}>
<i className="icon-close"></i>
</button>
</article>
</div>
);
}
export default TaskTab;
Thanks for the great question!
What is happening here is understandably confusing, and it took me a while to realize it myself.
TL;DR: handleSelectTask in the Provider is being called every time a button is clicked for deleteTask because of event propagation. handleSelectTask isn't using the state that has been modified by deleteTask, even though it's running after it, because it has closure to the initial tasks array.
Quick Solution 1
Stop the event from propagating from the delete button click to the TaskTab div click, which is probably the desired behavior.
// in TaskTab.jsx
const handleDelete = (event) => {
event.stopPropagation(); // stops event from "bubbling" up the tree
deleteTask(taskTabKey);
}
In the DOM (and emulated by React as well), events "bubble" up the tree, so that parent nodes can handle events coming from their child nodes. In the example, the <button onClick={handleDelete}> is a child of the <div onClick={selectTask}>, which means that when the click event is fired from the button, it will first call the handleDelete function like we want, but it will also call the selectTask function from the parent div afterwards, which is probably unintended. You can read more about event propagation on MDN.
Quick Solution 2
Write the state updates to use the intermediary state value at the time they are called.
// in tasks-context.jsx
const deleteTask = ( taskToDeleteKey ) => {
setActiveTask(null);
// use the function version of setting state to read the current value whenever it is run
setTasks((stateTasks) => stateTasks.filter(task => task.key !== taskToDeleteKey));
}
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// updated to use the callback version of the state update
setTasks((stateTasks) => stateTasks.map( task => {
// set the correct one to active
}));
};
Using the callback version of the setTasks state update, it will actually read the value at the time the update is being applied (including and especially in the middle of an update!), which, since the handleSelectTask is called after, means that it actually sees the array that has already been modified by the deleteTask that ran first! You can read more about this callback variant of setting state in the React docs (hooks) (setState). Note that this "fix" will mean that your component will still call handleSelectTask even though the task has been deleted. It won't have any ill-effects, just be aware.
Let's walk through what's happening in a bit more detail:
First, the tasks variable is created from useState. This same variable is used throughout the component, which is totally fine and normal.
// created here
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
// referenced here, no big deal
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// tasks is referenced here, too, awesome
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
task.isActive = false;
}
return task;
});
setTasks( newTaskArray );
};
Where the trouble comes in, is that if both of the functions are trying to update the same state value in the same render cycle, they will both be referencing the original value of the tasks array, even if the other function has attempted to update the state value! In your case, because the handleSelectTask is running after deleteTask, this means that handleSelectTask will update state using the array that hasn't been modified! When it runs, it will still see two items in the array, since the tasks variable won't change until the update is actually committed and everything rerenders. This makes it look like the delete portion isn't functioning, when really its effect is just being discarded since handleSelectTask isn't aware that the delete happened before it.
Lucas, this is not an issue with Context or Provider.
The problem that you are facing is actually a mechanism known as event bubbling where the current handler executes followed by parent handlers.
More info on event bubbling could be found here. https://javascript.info/bubbling-and-capturing.
In your case first, the handleDelete function gets called followed by handleSelect function.
Solution: event.stopPropagation();
Change your handleDelete and handleSelect function to this
const selectTask = () => {
console.log("handle select called");
handleSelectTask(task, taskTabKey);
};
const handleDelete = event => {
console.log("handle delete called");
event.stopPropagation();
deleteTask(taskTabKey);
};
Now check your console and you will find only the handle delete called will print and this would solve your problem hopefully.
If it still doesn't work then do let me know. I will create a codesandbox version for you.
Happy Coding.
Considering below hooks example
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Basically we use this.forceUpdate() method to force the component to re-render immediately in React class components like below example
class Test extends Component{
constructor(props){
super(props);
this.state = {
count:0,
count2: 100
}
this.setCount = this.setCount.bind(this);//how can I do this with hooks in functional component
}
setCount(){
let count = this.state.count;
count = count+1;
let count2 = this.state.count2;
count2 = count2+1;
this.setState({count});
this.forceUpdate();
//before below setState the component will re-render immediately when this.forceUpdate() is called
this.setState({count2: count
}
render(){
return (<div>
<span>Count: {this.state.count}></span>.
<button onClick={this.setCount}></button>
</div>
}
}
But my query is How can I force above functional component to re-render immediately with hooks?
This is possible with useState or useReducer, since useState uses useReducer internally:
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
forceUpdate isn't intended to be used under normal circumstances, only in testing or other outstanding cases. This situation may be addressed in a more conventional way.
setCount is an example of improperly used forceUpdate, setState is asynchronous for performance reasons and shouldn't be forced to be synchronous just because state updates weren't performed correctly. If a state relies on previously set state, this should be done with updater function,
If you need to set the state based on the previous state, read about the updater argument below.
<...>
Both state and props received by the updater function are guaranteed
to be up-to-date. The output of the updater is shallowly merged with
state.
setCount may not be an illustrative example because its purpose is unclear but this is the case for updater function:
setCount(){
this.setState(({count}) => ({ count: count + 1 }));
this.setState(({count2}) => ({ count2: count + 1 }));
this.setState(({count}) => ({ count2: count + 1 }));
}
This is translated 1:1 to hooks, with the exception that functions that are used as callbacks should better be memoized:
const [state, setState] = useState({ count: 0, count2: 100 });
const setCount = useCallback(() => {
setState(({count}) => ({ count: count + 1 }));
setState(({count2}) => ({ count2: count + 1 }));
setState(({count}) => ({ count2: count + 1 }));
}, []);
React Hooks FAQ official solution for forceUpdate:
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
// usage
<button onClick={forceUpdate}>Force update</button>
Working example
const App = () => {
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
return (
<div>
<button onClick={forceUpdate}>Force update</button>
<p>Forced update {_} times</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>
<script>var useReducer = React.useReducer</script>
<div id="root"></div>
Generally, you can use any state handling approach you want to trigger an update.
With TypeScript
codesandbox example
useState
const forceUpdate: () => void = React.useState({})[1].bind(null, {}) // see NOTE below
useReducer (recommended)
const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void
as custom hook
Just wrap whatever approach you prefer like this
function useForceUpdate(): () => void {
return React.useReducer(() => ({}), {})[1] as () => void // <- paste here
}
How this works?
"To trigger an update" means to tell React engine that some value has changed and that it should rerender your component.
[, setState] from useState() requires a parameter. We get rid of it by binding a fresh object {}.
() => ({}) in useReducer is a dummy reducer that returns a fresh object each time an action is dispatched.
{} (fresh object) is required so that it triggers an update by changing a reference in the state.
PS: useState just wraps useReducer internally, so use reducer to reduce complexity. source
NOTE: Referential instability
Using .bind with useState causes a change in function reference between renders.
It is possible to wrap it inside useCallback as already explained in this answer here, but then it wouldn't be a sexy one-linerâ„¢. The Reducer version already keeps reference equality (stability) between renders. This is important if you want to pass the forceUpdate function in props to another component.
plain JS
const forceUpdate = React.useState({})[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
As the others have mentioned, useState works - here is how mobx-react-lite implements updates - you could do something similar.
Define a new hook, useForceUpdate -
import { useState, useCallback } from 'react'
export function useForceUpdate() {
const [, setTick] = useState(0);
const update = useCallback(() => {
setTick(tick => tick + 1);
}, [])
return update;
}
and use it in a component -
const forceUpdate = useForceUpdate();
if (...) {
forceUpdate(); // force re-render
}
See https://github.com/mobxjs/mobx-react-lite/blob/master/src/utils.ts and https://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver.ts
Alternative to #MinhKha's answer:
It can be much cleaner with useReducer:
const [, forceUpdate] = useReducer(x => x + 1, 0);
Usage:
forceUpdate() - cleaner without params
You can simply define the useState like that:
const [, forceUpdate] = React.useState(0);
And usage: forceUpdate(n => !n)
Hope this help !
You should preferably only have your component depend on state and props and it will work as expected, but if you really need a function to force the component to re-render, you could use the useState hook and call the function when needed.
Example
const { useState, useEffect } = React;
function Foo() {
const [, forceUpdate] = useState();
useEffect(() => {
setTimeout(forceUpdate, 2000);
}, []);
return <div>{Date.now()}</div>;
}
ReactDOM.render(<Foo />, document.getElementById("root"));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Simple code
const forceUpdate = React.useReducer(bool => !bool)[1];
Use:
forceUpdate();
Potential option is to force update only on specific component using key. Updating the key trigger a rendering of the component (which failed to update before)
For example:
const [tableKey, setTableKey] = useState(1);
...
useEffect(() => {
...
setTableKey(tableKey + 1);
}, [tableData]);
...
<DataTable
key={tableKey}
data={tableData}/>
You can (ab)use normal hooks to force a rerender by taking advantage of the fact that React doesn't print booleans in JSX code
// create a hook
const [forceRerender, setForceRerender] = React.useState(true);
// ...put this line where you want to force a rerender
setForceRerender(!forceRerender);
// ...make sure that {forceRerender} is "visible" in your js code
// ({forceRerender} will not actually be visible since booleans are
// not printed, but updating its value will nonetheless force a
// rerender)
return (
<div>{forceRerender}</div>
)
One line solution:
const useForceUpdate = () => useState()[1];
useState returns a pair of values: the current state and a function that updates it - state and setter, here we are using only the setter in order to force re-render.
react-tidy has a custom hook just for doing that called useRefresh:
import React from 'react'
import {useRefresh} from 'react-tidy'
function App() {
const refresh = useRefresh()
return (
<p>
The time is {new Date()} <button onClick={refresh}>Refresh</button>
</p>
)
}
Learn more about this hook
Disclaimer I am the writer of this library.
My variation of forceUpdate is not via a counter but rather via an object:
// Emulates `forceUpdate()`
const [unusedState, setUnusedState] = useState()
const forceUpdate = useCallback(() => setUnusedState({}), [])
Because {} !== {} every time.
Solution in one single line:
const [,forceRender] = useReducer((s) => s+1, 0)
You can learn about useReducer here.
https://reactjs.org/docs/hooks-reference.html#usereducer
This will render depending components 3 times (arrays with equal elements aren't equal):
const [msg, setMsg] = useState([""])
setMsg(["test"])
setMsg(["test"])
setMsg(["test"])
const useForceRender = () => {
const [, forceRender] = useReducer(x => !x, true)
return forceRender
}
Usage
function Component () {
const forceRender = useForceRender()
useEffect(() => {
// ...
forceRender()
}, [])
For regular React Class based components, refer to React Docs for the forceUpdate api at this URL. The docs mention that:
Normally you should try to avoid all uses of forceUpdate() and only
read from this.props and this.state in render()
However, it is also mentioned in the docs that:
If your render() method depends on some other data, you can tell React
that the component needs re-rendering by calling forceUpdate().
So, although use cases for using forceUpdate might be rare, and I have not used it ever, however I have seen it used by other developers in some legacy corporate projects that I have worked on.
So, for the equivalent functionality for Functional Components, refer to the React Docs for HOOKS at this URL. Per the above URL, one can use the "useReducer" hook to provide a forceUpdate functionality for Functional Components.
A working code sample that does not use state or props is provided below, which is also available on CodeSandbox at this URL
import React, { useReducer, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
// Use the useRef hook to store a mutable value inside a functional component for the counter
let countref = useRef(0);
const [, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
countref.current++;
console.log("Count = ", countref.current);
forceUpdate(); // If you comment this out, the date and count in the screen will not be updated
}
return (
<div className="App">
<h1> {new Date().toLocaleString()} </h1>
<h2>You clicked {countref.current} times</h2>
<button
onClick={() => {
handleClick();
}}
>
ClickToUpdateDateAndCount
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
NOTE: An alternate approach using the useState hook (instead of useReducer) is also available at this URL.
There are many ways to force re-render in Hook.
For me simple way with useState() and tip of reference object values.
const [, forceRender] = useState({});
// Anywhre
forceRender({});
Codesandbox Example
A bit late to the party but I notice that most (all) of the answers have missed the part where you can pass a callback to forceUpdate lifecycle method.
As per the react source code, this callback has the same behavior as the one in the setState method - it is executed after the update.
Hence, the most correct implementation would be like this:
/**
* Increments the state which causes a rerender and executes a callback
* #param {function} callback - callback to execute after state update
* #returns {function}
*/
export const useForceUpdate = (callback) => {
const [state, updater] = useReducer((x) => x + 1, 0);
useEffect(() => {
callback && callback();
}, [state]);
return useCallback(() => {
updater();
}, []);
};
I was working with an array and spotted this issue. However, instead of explicit forceUpdate I found another approach - to deconstruct an array and set a new value for it using this code:
setRoutes(arr => [...arr, newRoute]); // add new elements to the array
setRouteErrors(routeErrs => [...routeErrs]); // the array elements were changed
I found it very interesting that setting even a copy of an array will not trigger the hook. I assume React does the shallow comparison