Photo by Lautaro Andreani on Unsplash
How to Share State Variables Between React Components
Stop writing the same thing over and over again
When developing React apps, it's quite common to write reusable functional components which internal state will be shared by other Hooks. I had to do this recently while working on Redux Chess, a React Redux chessboard connected to a chess server.
Click here to view a demo.
I managed to share a state variable between multiple components as shown in the animations below.
As you can see, a dialog pops up when clicking on any of these buttons:
- Play Online
- Play a Friend
- Play Computer
The thing is you don't want to reinvent the wheel and write the same color selection stuff over and over again. All three dialogs (parents) use the same functional component (child) to allow users select the color of the pieces.
Here are the corresponding React files.
- src/features/dialog/CreateInviteCodeDialog.js
- src/features/dialog/createInviteCodeDialogSlice.js
- src/features/dialog/PlayOnlineDialog.js
- src/features/dialog/playOnlineDialogSlice.js
- src/features/dialog/PlayComputerDialog.js
- src/features/dialog/playComputerDialogSlice.js
Each dialog has been implemented according to Redux Toolkit best practices. The MUI code is written in a file ending with the word Dialog
while its state management logic is encoded in a slice.
It's two files for each dialog.
SelectColorButtons
is a reusable, self-contained component meaning it doesn't need any slice because its state is internal. In other words, it doesn't actually make sense for it to be accessed by other components globally via a Redux store.
// src/features/dialog/SelectColorButtons.js
import * as React from 'react';
import { Avatar, ButtonGroup, IconButton } from '@mui/material';
import { makeStyles } from '@mui/styles';
import wKing from '../../assets/img/pieces/png/150/wKing.png';
import wbKing from '../../assets/img/pieces/png/150/wbKing.png';
import bKing from '../../assets/img/pieces/png/150/bKing.png';
import Pgn from '../../common/Pgn';
const useStyles = makeStyles({
buttonGroup: {
marginBottom: 10,
},
selected: {
backgroundColor: '#d8d8d8',
},
});
const SelectColorButtons = ({ props }) => {
const classes = useStyles();
const [color, setColor] = React.useState('rand');
const handleSelectColor = (color) => {
setColor(color);
};
React.useEffect(() => {
props.color = color;
}, [color]);
return (
<ButtonGroup className={classes.buttonGroup}>
<IconButton
aria-label="white"
title="White"
onClick={() => handleSelectColor(Pgn.symbol.WHITE)}
>
<Avatar
src={wKing}
sx={{ width: 55, height: 55 }}
className={color === Pgn.symbol.WHITE ? classes.selected : null}
/>
</IconButton>
<IconButton
aria-label="random"
title="Random"
onClick={() => handleSelectColor('rand')}
>
<Avatar
src={wbKing}
sx={{ width: 55, height: 55 }}
className={color === 'rand' ? classes.selected : null}
/>
</IconButton>
<IconButton
aria-label="black"
title="Black"
onClick={() => handleSelectColor(Pgn.symbol.BLACK)}
>
<Avatar
src={bKing}
sx={{ width: 55, height: 55 }}
className={color === Pgn.symbol.BLACK ? classes.selected : null}
/>
</IconButton>
</ButtonGroup>
);
}
export default SelectColorButtons;
SelectColorButtons
basically receives the props
from a parent component and changes its value whenever an icon button is clicked on.
const handleSelectColor = (color) => {
setColor(color);
};
This is achieved by the handleSelectColor()
function working along with React's useEffect
Hook which listens for a state change in the color
variable.
React.useEffect(() => {
props.color = color;
}, [color]);
That's it!
Now let's have a look at the other way around.
The code of the parent components is perhaps a different story. A harsh truth nobody wants to hear about but it's worth saying is that full-stack web development may not be too obvious at times. Probably this is the case with Redux Chess provided that it communicates with both an API and a WebSocket server.
It is not the purpose of this post to go into the details of how Redux Chess works.
So I've removed the lines of code that deal with the WebSocket server and other stuff for the sake of simplicity to focus on what really matters. Remember, we want a state variable to be shared between components in a simple way.
Let's have a look at PlayComputerDialog
as an example.
import * as React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import CloseIcon from '@mui/icons-material/Close';
import {
Button,
Dialog,
DialogContent,
DialogTitle,
Grid,
IconButton,
Slider,
Typography
} from '@mui/material';
import Pgn from '../../common/Pgn';
import Dispatcher from '../../common/Dispatcher';
import * as mainButtons from '../../features/mainButtonsSlice';
import * as mode from '../../features/modeSlice';
import * as playComputerDialog from '../../features/dialog/playComputerDialogSlice';
import SelectColorButtons from '../../features/dialog/SelectColorButtons';
import WsAction from '../../ws/WsAction';
const PlayComputerDialog = () => {
const state = useSelector(state => state);
const dispatch = useDispatch();
const [fields, setFields] = React.useState({
level: 1,
color: 'rand'
});
const handleCreateGame = () => {
// The fields are now available for sending to the server
// console.log(fields);
};
const handleLevelChange = (event: Event) => {
setFields({
...fields,
level: event.target.value
});
};
// ...
return (
<Dialog open={state.playComputerDialog.open} maxWidth="xs" fullWidth={true}>
<DialogTitle>
<Grid container>
<Grid item xs={11}>
Play Computer
</Grid>
<Grid item xs={1}>
<IconButton onClick={() => dispatch(playComputerDialog.close())}>
<CloseIcon />
</IconButton>
</Grid>
</Grid>
</DialogTitle>
<DialogContent>
<Typography
id="level"
gutterBottom
align="center"
>
Difficulty level
</Typography>
<Slider
name="level"
aria-label="Level"
defaultValue={1}
valueLabelDisplay="auto"
step={1}
min={0}
max={3}
marks
onChange={handleLevelChange}
/>
<Grid container justifyContent="center">
<SelectColorButtons props={fields} />
</Grid>
<Button
fullWidth
variant="outlined"
onClick={() => handleCreateGame()}
>
Create Game
</Button>
</DialogContent>
</Dialog>
);
}
export default PlayComputerDialog;
The useState
Hook is used in PlayComputerDialog
to manage the internal state of the dialog. The object variable fields
holds the two values required to play the computer: level
and color
.
const [fields, setFields] = React.useState({
level: 1,
color: 'rand'
});
The dialog fields are passed to the child component as props
.
<SelectColorButtons props={fields} />
Thus, the user can select a color in the child component while the parent gets automatically updated via the fields
variable.
Conclusion
A simple way to share a state variable between a parent and a child component is to make sure that the former passes it to the latter as props
.
In this example, we've seen how the fields
of PlayComputerDialog
are passed to SelectColorButtons
.
props.color
is then updated in the child component whenever its internal state changes. This is achieved by React's useEffect
Hook which listens for a state change in the color
variable.
Did you find this article valuable?
Support Jordi Bassaganas by becoming a sponsor. Any amount is appreciated!