Updating an immutable variable in a child component - javascript

I have a global variable plyViewed in App.js that is set outside of my App component.
let plyViewed = 0;
function App() {
It monitors which move the board game is on. Below the board are some navigation buttons. If you click < I do plyViewed--, if you click I do plyViewed++. You get the picture.
This all worked fine, until I refactored!
I took the navigation buttons, who’s JSX code was all inside the App() function and put it in <MoveButtons /> in MoveButtons.js. So, I thought I could pass down plyViewed as a prop and then update the value in my code in the MoveButton child component. Then I find that props are immutable! Now I am stuck.
My code below gives an example of how I am using that plyViewed code. When someone clicks the navigation buttons, it fires an event that triggers the code to update plyViewed, although now it doesn’t work anymore because it is a prop. The rest of the game data is stored in an object called gameDetails.
I am passing down the plyViewed like this:
<MoveButtons
plyViewed={plyViewed}
// etc.
/>
A shortened version of my MoveList component is below.
plyViewed is used in multiple areas throughout my app, like gameDetails. I’ve considered putting it in the gameDetails object, but then I still have the issue of gameDetails being immutable if passed down as a prop. Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
Am I thinking about this all wrong?
export default function MoveButtons(props) {
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
}

you are trying to update the props that are passed down from the higher-level component in your component tree, which is not possible.
You have the option to create a state using React's useState hook and passing down both the value and the dispatcher, but this is not recommended because you would be drilling props down the tree.
You can also pass the onClick events (or parts of them), up to your App component, which is an improvement to the first method but not the best practice in your case.
What you should really be doing is managing your global state using either, React's own Context API, or Redux. I think this could help you out.

While we're missing the full picture, it sounds like plyViewed should be a state and the asynchronous behaviour shouldn't prevent any computation if done properly with React.
It's easy to overlook the fact that the new state value is synchronously computed by ourselves when setting the state. We can just use that same local value to compute anything else and the async behaviour isn't affecting us at all.
onClick={() => {
if (props.plyViewed > 0) {
// New local value computed by ourselves synchronously.
const updatedPlyViewed = props.plyViewed - 1;
// Set the state with the new value to reflect changes on the app.
props.setPlyViewed(updatedPlyViewed);
// Use the up-to-date local value to compute anything else
props.board.current.setPosition(props.fenHistory[updatedPlyViewed]);
props.setFen(props.fenHistory[updatedPlyViewed]);
props.setSelectedIndex(updatedPlyViewed);
}
}}
This is a really simple pattern that should help solve the most basic issues with new state values.
Simple computations
Quick computations can be done in the render phase. The latest state values will always be available at this point. It's unnecessary to sync multiple state values if it can easily be computed from a single value, like the plyViewed here.
const [plyViewed, setPlyViewed] = useState(0);
// No special state or function needed to get the position value.
const position = fenHistory[plyViewed];
Here's an interactive example of how a simple state can be used to compute a lot of different derived information within the render phase.
// Get a hook function
const { useState } = React;
// This component only cares about displaying buttons, the actual logic
// is kept outside, in a parent component.
const MoveButtons = ({ onBack, onNext }) => (
<div>
<button type="button" onClick={onBack}>
Back
</button>
<button type="button" onClick={onNext}>
Next
</button>
</div>
);
const App = () => {
const [fenHistory, setFenHistory] = useState(["a", "b"]);
const [plyViewed, setPlyViewed] = useState(0);
const position = fenHistory[plyViewed];
const onBack = () => setPlyViewed((curr) => Math.max(curr - 1, 0));
const onNext = () =>
setPlyViewed((curr) => Math.min(curr + 1, fenHistory.length - 1));
return (
<div>
<p>Ply viewed: {plyViewed}</p>
<p>Fen position: {position}</p>
<p>Fen history: {fenHistory.join(", ")}</p>
<button
type="button"
onClick={() =>
setFenHistory((history) => [...history, `new-${history.length}`])
}
>
Add to fen history
</button>
<MoveButtons onBack={onBack} onNext={onNext} />
</div>
);
};
// Render it
ReactDOM.render(<App />, document.getElementById("app"));
button {
margin-right: 5px;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Expensive computations
If the computation takes a considerable amount of time to complete, and that doing it each render cycle is noticeably slowing down the rendering, there are some optimizations we could do.
useMemo will only recompute the memoized value when one of the
dependencies has changed. This optimization helps to avoid expensive
calculations on every render.
const [plyViewed, setPlyViewed] = useState(0);
const position = useMemo(() => /* costly computation here */, [plyViewed]);
Complex computations
If the computation has a lot of dependencies, we could use useReducer to manage a state object.
Note that the following example isn't justifying the use of useReducer and it's only used as an example of the implementation.
const initialState = {
plyViewed: 0,
fenHistory: ["a", "b"],
positionValue: "a",
};
function reducer(state, action) {
const { plyViewed, fenHistory } = state;
switch (action.type) {
case "back":
if (fenHistory.length <= 0) return state;
const newIndex = plyViewed - 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "next":
if (fenHistory.length - 1 > plyViewed) return state;
const newIndex = plyViewed + 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "add":
return {
...state,
fenHistory: [...fenHistory, action.value],
};
default:
throw new Error();
}
}
const App = () => {
const [{ plyViewed, fenHistory, positionValue }, dispatch] = useReducer(
reducer,
initialState
);
const onBack = () => dispatch({ type: "back" });
const onNext = () => dispatch({ type: "next" });
const onAdd = () => dispatch({ type: "add", value: 'anything' });
// ...
Async computations
If we need to get the result, for example, from a distant server, then we could use useEffect which will run once when the value changes.
const App = () => {
const [plyViewed, setPlyViewed] = useState(0);
const [position, setPosition] = useState(null);
useEffect(() => {
fetchPosition(plyViewed).then((newPosition) => setPosition(newPosition));
}, [plyViewed]);
There are a couple pitfalls with useEffect and asynchronously setting the state.
Prevent setting state on an unmounted component.
plyViewed may have changed again since the first fetch was triggered but before it actually succeeded, resulting in a race-condition

Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
I think this is incorrect, you just need to start using useEffect as well:
function App() {
const [plyViewed, setPlyViewed] = useState(0);
<MoveButtons
plyViewed={plyViewed}
setPlyViewed={setPlyViewed}
/>
}
export default function MoveButtons(props) {
useEffect(() => {
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}, [props.plyViewed, props.fenHistory]);
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);

Related

Stop checkbox values being reset after page load

Hello I am working on a pop up window where the user can filter a table of data.
The filter is selected using checkboxes.
My issue:
On page load there is a useEffect that changes every checkbox to false. This is based on the props coming in from the API.
I'd like on page load (and when the filter opens) that the checkbox state is stored based on what the user has selected previously in their session
code:
Filter component*
[...]
import FilterSection from "../FilterSection";
const Filter = ({
open,
handleClose,
setFilterOptions,
[..]
roomNumbers,
}) => {
const [roomValue, setRoomValue] = React.useState();
const [roomListProp, setRoomListProp] = React.useState(); // e.g. [["roomone", false], ["roomtwo", true]];
const sendRoomFilterData = (checkedRoomsFilterData) => {
setRoomValue(checkedRoomsFilterData);
};
const setCheckboxListPropRoom = (data) => {
setRoomListProp(data);
};
// extract, convert to an object and pass back down? or set local storage and get
// local storage and pass back down so that we can get it later?
const convertToLocalStorageFilterObject = (roomData) => { // []
if (roomData !== undefined) {
const checkedRooms = roomData.reduce((a, curval) => ({ ...a, [curval[0]]: curval[1] }), {});
localStorage.setItem("preserved", JSON.stringify(checkedRooms)); // sets in local storage but values get wiped on page load.
}
};
React.useEffect(() => {
const preservedFilterState = convertToLocalStorageFilterObject(roomListProp);
}, [roomListProp]);
const applyFilters = () => {
setFilterOptions([roomValue]);
handleClose();
};
const classes = CurrentBookingStyle();
return (
<Dialog
fullWidth
maxWidth="sm"
open={open}
onClose={() => handleClose(false)}
>
<DialogTitle>Filter By:</DialogTitle>
<DialogContent className={classes.margin}>
<FilterSection
filterName="Room number:"
filterData={roomNumbers}
setFilterOptions={sendRoomFilterData}
setCheckboxListProp={setCheckboxListPropRoom}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={applyFilters}>
Apply Filters
</Button>
</DialogActions>
</Dialog>
);
};
Filter Section used in Filter
import {
TableCell,
Typography,
FormControlLabel,
Checkbox,
FormGroup,
} from "#material-ui/core";
const FilterSection = ({
filterData, filterName, setFilterOptions, setCheckboxListProp
}) => {
const [checkboxValue, setCheckboxValue] = React.useState({});
const [checkboxFilterList, setCheckboxFilterList] = React.useState([]);
const handleCheckboxChange = (event) => {
setCheckboxValue({
...checkboxValue,
[event.target.name]: event.target.checked, // room1: true
});
};
const = () => filterData // ["room1" "room2"]; comes from API
.filter((val) => !Object.keys(checkboxValue).includes(val))
.reduce((acc, currval) => ({
...acc, [currval]: false, // converts array to object and sets values to false
}), checkboxValue);
React.useEffect(() => {
const transformedCheckboxListItems = Object.entries(convertToObject());
setCheckboxFilterList(transformedCheckboxListItems);
setFilterOptions(transformedCheckboxListItems.filter(([, val]) => val).map(([key]) => key));
setCheckboxListProp(transformedCheckboxListItems);
}, [checkboxValue]);
return (
<>
<Typography style={{ fontWeight: "bold" }}>{filterName}</Typography>
<FormGroup row>
{checkboxFilterList.map(([key, val]) => (
<TableCell style={{ border: 0 }}>
<FormControlLabel
control={(
<Checkbox
checked={val}
onChange={handleCheckboxChange}
name={key}
color="primary"
/>
)}
label={key}
/>
</TableCell>
))}
</FormGroup>
</>
);
};
What i have tried:
I have created a reusable component called "FilterSection" which takes takes data from the API "filterData" and transforms it from an array to an object to set the initial state for the filter checkboxes.
On page load of the filter I would like the checkboxes to be true or false depending on what the user has selected, however this does not work as the convertToObject function in my FilterSection component converts everything to false again on page load. I want to be able to change this but not sure how? - with a conditional?
I have tried to do this by sending up the state for the selected checkboxes to the Filter component then setting the local storage, then the next step would be to get the local storage data and somehow use this to set the state before / after page load. Unsure how to go about this.
Thanks in advance
I am not sure if I understand it correctly, but let me have a go:
I have no idea what convertToObject does, but I assume it extracts the saved filters from localStorage and ... updates the filter value that has just been changed?
Each time the FilterSection renders for the first time, checkboxValue state is being initialised and an useEffect runs setCheckboxListProp, which clears the options, right?
If this is your problem, try running setCheckboxListProp directly in the handleCheckboxChange callback rather than in an useEffect. This will ensure it runs ONLY after the value is changed by manual action and not when the checkboxValue state is initialised.
I solved my problem by moving this line:
const [checkboxValue, setCheckboxValue] = React.useState({});
outside of the component it was in because every time the component re-rendered it ran the function (convertToObject() which reset each checkbox to false
by moving the state for the checkboxes up three layers to the parent component, the state never got refreshed when the or component pop up closed. Now the checkbox data persists which is the result I wanted.
:D

How to render the next component in React?

I have three components First, Second and Third that need to render one after the other.
My App looks like this at the moment:
function App() {
return (
<First/>
)
}
So ideally, there's a form inside First that on submission (onSubmit probably) triggers rendering the Second component, essentially getting replaced in the DOM. The Second after some logic triggers rendering the Third component and also passes a value down to it. I'm not sure how to go on about it.
I tried using the useState hook to set a boolean state to render one of the first two components but I would need to render First, then somehow from within it change the set state in the parent which then checks the boolean and renders the second. Not sure how to do that. Something like below?
function App() {
const { isReady, setIsReady } = useState(false);
return (
isReady
? <First/> //inside this I need the state to change on form submit and propagate back up to the parent which checks the state value and renders the second?
: <Second/>
);
}
I'm mostly sure this isn't the right way to do it.
Also need to figure out how to pass the value onto another component at the time of rendering it and getting replaced in the DOM. So how does one render multiple components one after the other on interaction inside each? A button click for example?
Would greatly appreciate some guidance for this.
then somehow from within it change the set state in the parent which then checks the boolean and renders the second.
You're actually on the right track.
In React, when you're talking about UI changes, you have to manage some state.
So we got that part out of the way.
Now, what we can do in this case is manage said state in the parent component and pass functions to the children components as props in-order to allow them to control the relevant UI changes.
Example:
function App() {
const { state, setState } = useState({
isFirstVisible: true,
isSecondVisible: false,
isThirdVisible: false,
});
const onToggleSecondComponent = (status) => {
setState(prevState => ({
...prevState,
isSecondVisible: status
}))
}
const onToggleThirdComponent = (status) => {
setState(prevState => ({
...prevState,
isThirdVisible: status
}))
}
return (
{state.isFirstVisible && <First onToggleSecondComponent={onToggleSecondComponent} /> }
{state.isSecondVisible && <Second onToggleThirdComponent={onToggleThirdComponent} /> }
{state.isThirdVisible && <Third/> }
);
}
Then you can use the props in the child components.
Example usage:
function First({ onToggleSecondComponent }) {
return (
<form onSubmit={onToggleSecondComponent}
...
</form
)
}
Note that there are other ways to pass these arguments.
For example, you can have one function in the parent comp that handles them all, or you can just pass setState to the children and have them do the logic.
Either way, that's a solid way of achieving your desired outcome.
Seen as your saying there are stages, rather than having a state for each stage, just have a state for the current stage, you can then just increment the stage state to move onto the next form.
Below is a simple example, I've also used a useRef to handle parent / child state, basically just pass the state to the children and the children can update the state. On the final submit I'm just JSON.stringify the state for debug..
const FormContext = React.createContext();
const useForm = () => React.useContext(FormContext);
function FormStage1({state}) {
const [name, setName] = React.useState('');
state.name = name;
return <div>
Stage1:<br/>
name: <input value={name} onChange={e => setName(e.target.value)}/>
</div>
}
function FormStage2({state}) {
const [address, setAddress] = React.useState('');
state.address = address;
return <div>
Stage2:<br/>
address: <input value={address} onChange={e => setAddress(e.target.value)}/>
</div>
}
function FormStage3({state}) {
const [hobbies, setHobbies] = React.useState('');
state.hobbies = hobbies;
return <div>
Stage3:<br/>
hobbies: <input value={hobbies} onChange={e => setHobbies(e.target.value)}/>
</div>
}
function Form() {
const [stage, setStage] = React.useState(1);
const state = React.useRef({}).current;
let Stage;
if (stage === 1) Stage = FormStage1
else if (stage === 2) Stage = FormStage2
else if (stage === 3) Stage = FormStage3
else Stage = null;
return <form onSubmit={e => {
e.preventDefault();
setStage(s => s + 1);
}}>
{Stage
? <React.Fragment>
<Stage state={state}/>
<div>
<button>Submit</button>
</div>
</React.Fragment>
: <div>
{JSON.stringify(state)}
</div>
}
</form>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Form/>);
<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="root"></div>

Why are State Updater Functions Faster Than Setting State to Plain Values?

For context, I have a web app that displays an image in a React-Bootstrap Container component (Arena) that holds an image where users are to look and find specific characters.
Separately, I created a div component (CustomCursor) where the background is set to a magnifying glass SVG image.
The Arena component tracks mouse position through an OnMouseMove handler function (handleMouseMove) and passes those coordinates as props to the CustomCursor component.
Here is my Arena component code:
import { useState, useEffect } from 'react';
import { Container, Spinner } from 'react-bootstrap';
import CustomCursor from '../CustomCursor/CustomCursor';
import Choices from '../Choices/Choices';
import { getImageURL } from '../../helpers/storHelpers';
import './Arena.scss';
export default function Arena(props) {
const [arenaURL, setArenaURL] = useState('');
const [loaded, setLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handleClick(e) {
setClicked(true);
}
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
useEffect(() => {
retreiveArena();
// FUNCTION DEFINITIONS
async function retreiveArena() {
const url = await getImageURL('maps', 'the-garden-of-earthly-delights.jpg');
setArenaURL(url);
setLoaded(true);
}
}, [])
return (
<Container as='main' fluid id='arena' className='d-flex flex-grow-1 justify-content-center align-items-center' onClick={handleClick}>
{!loaded &&
<Spinner animation="border" variant="danger" />
}
{loaded &&
<img src={arenaURL} alt='The Garden of Earthly Delights triptych' className='arena-image' onMouseMove={handleMouseMove} />
}
{clicked &&
<Choices x={x} y={y} />
}
<CustomCursor x={x} y={y} />
</Container>
)
}
Here is my CustomCursor code:
import './CustomCursor.scss';
export default function CustomCursor(props) {
const { x, y } = props;
return (
<div className='custom-cursor' style={{ left: `${x - 64}px`, top: `${y + 50}px` }} />
)
}
When I first created the OnMouseMove handler function I simply set the x and y state values by passing them into their respective state setter functions directly:
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
However, I noticed this was slow and laggy and when I refactored this function to use setter functions instead it was much faster (what I wanted):
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
Before:
After:
Why are using setter functions faster than passing in values directly?
This is interesting. First of all, we need to focus on reacts way of updating state. In the documentation of react https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous There you can see:
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
For example, this code may fail to update the counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
A pretty good article on this is written by Jan Hesters here:
https://medium.com/#jan.hesters/updater-functions-in-reacts-setstate-63c7c162b16a
And more details here:
https://learn.co/lessons/react-updating-state

React: usePrevious hook only works if there is 1 state in the component. How to make it work if there are multiple states?

The docs suggests the folllowing to get previous state:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Per my understanding, this works fine only if there is exactly one state in the component. However consider the following where there are multiple states:
import "./styles.css";
import React, { useState, useEffect, useRef, useContext } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
export default function App() {
const [count, setCount] = useState(0);
const [foo, setFoo] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<button onClick={() => setFoo(f => f+1)}> Update foo </button>
<h1>Now: {count}, before: {prevCount}</h1>
</div>);
}
Sandbox: https://codesandbox.io/s/little-feather-wow4m
When a different state (foo) is updated, the usePrevious hook returns the latest value for count, as opposed to the previous one).
Is there a way to reliably get the previous value for a state/prop when there are multiple states?
I don't think this is the right approach.
How about a custom hook that sets up the state and returns a custom setter function that handles this logic for you.
function useStateWithPrevious(initial) {
const [value, setValue] = useState(initial)
const [prev, setPrev] = useState(initial)
function setValueAndPrev(newValue) {
if (newValue === value) return // optional, depends on the logic you want.
setPrev(value)
setValue(newValue)
}
return [prev, value, setValueAndPrev]
}
Which you would use like:
function MyComponent() {
const [prevCount, count, setCount] = useStateWithPrevious(0)
}
I was able to create an altered version of your hook that does seem to work:
function usePrevious(value) {
const ref = useRef([undefined, undefined]);
if (ref.current[0] !== value) {
ref.current = [value, ref.current[0]];
}
return ref.current[1];
}
Playground here.
Keeping track of only the previous value of some state is not a very common use case. Hence there's no need to overthink it or try to "trick" React with refs to achieve a slightly shorter syntax. There's almost always a few ways to get the same result in a more straightforward and maintainable manner.
React's docs also stress that this suggested approach is only for edge cases.
This is rarely needed and is usually a sign you have some duplicate or redundant state.
If the previous count is de facto part of the application state (it's used in rendering just like the current count), it's counter productive to not just store it as such. Once it's in state, it's just a matter of making state updates in event listeners update all parts of the state in one go, making it inherently safe with React's concurrent features.
Method 1: Set multiple state variables at the same time
Just create an additional state variable for the old value, and make your handler set both values.
const initialCount = 0;
function App() {
const [count, setCount] = useState(initialCount);
const [prevCount, setPrevCount] = useState(initialCount);
return <>
<button
onClick={() => {
// You can memoize this callback if your app needs it.
setCount(count + 1);
setPrevCount(count);
}}
>Increment</button>
<span>Current: {count} </span>
<span>Previous: {prevCount} </span>
</>
}
You can almost always do this instead, it offers the same functionality as usePrevious and obviously will never lead to the application using the wrong combination of values. In fact, because of batched state updates since React 18 there's no performance penalty in calling 2 setters in one event handler.
Using a hook like usePrevious doesn't really bring any overall benefits. Clearly both the current and the previous value are pieces of state your application needs for rendering. They can both use the same simple and readable syntax. Just because usePrevious is shorter doesn't mean it's easier to maintain.
Method 2: useReducer
If you want to avoid the 2 function calls in the event listener, you can use useReducer to encapsulate your state. This hook is particularly well suited for updates of complex but closely related state. It guarantees the application state transitions to a new valid state in one go.
const initialState = { count: 0, prevCount: 0, foo: 'bar' };
function countReducer(state, action) {
switch (action.type) {
case: 'INCREMENT':
return {
...state,
count: state.count + 1,
prevCount: state.count,
};
case 'DO_SOMETHING_ELSE':
// This has no effect on the prevCount state.
return {
...state,
foo: payload.foo,
}
}
return state;
}
function App() {
const [
{ count, prevCount },
dispatch
] = useReducer(countReducer, initialState)
return <>
<button
onClick={() => {
dispatch({ type: 'INCREMENT' });
}}
>Increment</button>
<button
onClick={() => {
dispatch({
type: 'DO_SOMETHING_ELSE',
payload: { foo: `last update: ${prevCount} to ${count`} },
);
}}
>Do foo</button>
<span>Current: {count} </span>
<span>Previous: {prevCount} </span>
</>
}

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources