Generating Multiple React useState Hooks For Multiple State Variables
6 min read
React state hooks are great for managing state locally, within a single component. I love how simple it all is.
import React, { useState } from 'react'
function HabitTracker() {
// Declare a new state variable, called "numberRuns"
const [numberRuns, setNumberRuns] = useState(0)
return (
<div>
<p>Julia went for {numberRuns} runs this week.</p>
<button onClick={() => setNumberRuns(numberRuns + 1)}>Another run</button>
</div>
)
}
If there are more than one state variable you want to monitor, it's simple enough to add a second or third useState
hook.
import React, { useState } from 'react'
function HabitTracker() {
const [numberRuns, setNumberRuns] = useState(0)
const [numberManga, setNumberManga] = useState(0)
const [numberDrummingHours, setNumberDrummingHours] = useState(0)
return (
<div>
<p>Julia went for {numberRuns} runs this week.</p>
<p>Julia read {numberManga} manga this week.</p>
<p>Julia did {numberDrummingHours} hours of drum practice this week.</p>
<button onClick={() => setNumberRuns(numberRuns + 1)}>Another run</button>
<button onClick={() => setNumberManga(numberManga + 1)}>Another manga</button>
<button onClick={() => setNumberDrummingHours(numberDrummingHours + 1)}>Another hour on drums</button>
</div>
)
}
But what happens if you need to add even more state variables? We can carry on like this but there might be a cleaner way to do this. The problem I needed to solve involved tracking 20 state variables - the method above definitely would've been a bit unwieldy. Is there a way of generating useState hooks from a constant?
It turns out that the answer is yes. Here's the solution I ended up with (note that what I'm trying to do here is to show/hide a hobby question in the DOM). The idea behind the solution was to:
- Set up a single object (
hobbyStates
) to store all the various hobby states (in this case, to either show/hide the hobby as a question). - Similarly, set up a single object
setHobbyStates
to store all the state setting functions for each hobby. - Save all the various hobbies you want to track the state of in an array of objects (
const HOBBIES
). - To make iterating through the
HOBBIES
list easier, I created a newquestionOrder
variable which just provides me with an array of thestateName
variable for each hobby inHOBBIES
. - Because I wanted the first hobby question to be displayed upon render, I set the first item in my
questionOrder
array (in this case,running
) to have a state ofshow
. - I then iterated through the rest of the
questionOrder
array and set the states for each remaining hobby to an empty string''
(in the child component, this hides the question).
const HOBBIES = [
{ question: 'How many times did you run?', stateName: 'running' },
{ question: 'How many manga comics did you read?', stateName: 'manga' },
{
question: 'How many hours did you spend on the drums?',
stateName: 'drums',
},
]
const questionOrder: string[] = HOBBIES.map((hobby) => {
return hobby.stateName
})
export const HobbyForm: React.FC = () => {
// set up 1st state hook for the first question, so that it displays a "show" state
const [hobbyState, setHobbyState] = useState('show')
// create a hobbyStates object that will store all the individual hobbyState variables.
// set the first key-value pair for running
const hobbyStates: { [key: string]: string } = {
[questionOrder[0]]: hobbyState,
}
// similarly, do the same by setting up a setHobbyStates object
const setHobbyStates: { [key: string]: Dispatch<string> } = {
[questionOrder[0]]: setHobbyState,
}
// now set up the remaining questions' state hooks as ''. This hides the question as you'll
// see in the HobbyQuestion component below.
for (let i = 1; i < questionOrder.length; i++) {
const [hobbyState, setHobbyState] = useState('')
hobbyStates[questionOrder[i]] = hobbyState
setHobbyStates[questionOrder[i]] = setHobbyState
}
return (
<>
{HOBBIES.map((hobby) => {
return (
<HobbyQuestion
key={hobby.stateName}
question={hobby.question}
status={hobbyStates[hobby.stateName]}
updateStatus={setHobbyStates[hobby.stateName]}
/>
)
})}
</>
)
}
From here, I return a HobbyQuestion
component that receives a status
and updateStatus
prop. I won't write out the whole component below, but it could look something like this. Note how the updateStatus
callback is called upon choosing an answer to each hobby question. What this does it to setHobbyStates[hobby.stateName]
to a value of the RadioButton
value selected.
export const HobbyQuestion: React.FC<HobbyQuestionProps> = ({ question, status, updateStatus }) => {
...
const FREQUENCY_CHOICES = (
<RadioButtonGroup>
<div className="grid gap-2 grid-cols-2">
<RadioButton clicked={updateStatus} ... e.g. 1x a week />
<RadioButton clicked={updateStatus} ... e.g. 2x a week />
<RadioButton clicked={updateStatus} ... e.g. 3x a week />
<RadioButton clicked={updateStatus} ... e.g. 4x a week />
</div>
</RadioButtonGroup>
const displayChoices = (status: string) => {
if (status === 'show') {
return FREQUENCY_CHOICES
} else {
// i.e. if status === ''
return null
}
}
return (
<>
<div className="mb-4 flex align-middle">
<p className="text-base-secondary light">
{question}
</p>
</div>
{displayChoices(status)}
</>
)
}
When a hobby question has been answered, we want to hide it and show the next question. To do this, we need to amend the original HobbyForm
component and add a useEffect
hook.
// other values this could take is an answer like '1x_a_week'
const INVALID_STATUSES = ['', 'show']
useEffect(
() => {
// run through questions and check for next question in line to update to 'show' state
for (let i = 0; i < questionOrder.length - 1; i++) {
const currentQuestion = questionOrder[i]
const nextQuestion = questionOrder[i + 1]
!INVALID_STATUSES.includes(hobbyStates[currentQuestion]) && INVALID_STATUSES.includes(hobbyStates[nextQuestion])
? setHobbyStates[nextQuestion]('show')
: null
}
},
questionOrder.map((question) => {
return hobbyStates[question]
}),
)
If you're wondering what the last bit of code with questionOrder.map...
is about, that's the dependency array that you should include whenever you want useEffect
to only re-run when when specific values change (rather than on every re-render). Here's what it normally looks like - I just used a map to render all of various hobby state variables automatically rather than manually typing each of them out. 🤖
useEffect(() => {
document.title = `You clicked ${count} times`
}, [count]) // Only re-run the effect if count changes
I've also written a follow up post on how to generate a Yup validation object dynamically, if you're interested.