This article was originally published on www.aboutmonica.com.
I recently created a Stimulus Check Calculator based on figures from the CARES Act and the Washington Post to help people estimate the amount of their stimulus check under the CARES Act.
This article will walk through how I refactored the calculator's state management by consolidating multiple useState()
React hooks into a single useReducer()
. useReducer()
is an alternative that can be considered when using useState()
to manage state in functional React components. This article assumes some familiarity with state management in React and React Hooks.
Screenshot of the Stimulus Check Calculator.
Overview:
- Managing form state with useState();
- Managing form state with useReducer();
- Full Source Code of Examples
Managing form state with useState();
In order to use the useState()
React hook for state management of the calculator I first needed to import useState
from React.
import { useState } from "react";
Setting the initial state with useState()
Then within the function that is returning the Form
component I setup the useState()
hooks for taxYear
, filingStatus
,income
, children
and stimulusAmount
.
const { SINGLE, HEADOFHOUSE, MARRIED } = filingStatuses;
const [taxYear, setTaxYear] = useState(2019);
const [filingStatus, setFilingStatus] = useState(SINGLE);
const [income, setIncome] = useState("75000");
const [children, setChildren] = useState(0);
const [stimulusAmount, setStimulusAmount] = useState(-1);
The parameter passed into useState()
represents the default value for that particular state. Meaning that the below line is setting the default value of taxYear
in state to 2019
.
const [taxYear, setTaxYear] = useState(2019);
Updating form state with useState()
Event handlers, such as onChange
or onClick
can be used to update the component's state when data when a form changes. Managing form state by updating the component's internal state is considered a "controlled component" vs. having the DOM manage the state of the form.
In order to update the taxYear
's value to the selected year, there's an onClick
event handler that calls setTaxYear(year)
with the year
parameter being the current year
that is selected.
{[2019, 2018].map(year => (
<button
onClick={() => setTaxYear(year)}
className={year == taxYear ? "selectedButton" : ""}
key={year}
name="tax-year"
>
{year == 2019 ? "Yes" : "No"}
</button>
))}
Similar logic is used to update filingStatus
income
and children
, stimulusAmount
and handleSubmit
when form data is updated or submitted.
## Managing form state with useReducer();
In order to use the useReducer()
React hook for state management of the calculator I first needed to import useReducer
from React. If you are not familiar with reducers
in JavaScript check out my article on Understanding Reduce in Javascript
import { useReducer } from "react";
Setting the initial state with useReducer()
Then I set the initial state for the component like:
const initialState = {
taxYear: 2019,
filingStatus: SINGLE,
income: "75000",
children: 0,
stimulusAmount: -1,
};
const [state, dispatch] = useReducer(reducer, initialState);
Similar to useState
, useReducer
returns the related state along with a method to update the state. With useReducer
instead of updating the state by passing a value to setState()
an action should be dispatched which will call the reducer
.
In my case the reducer
function looked like:
function reducer(state, action) {
const { type, payload } = action;
return { ...state, [type]: payload };
}
Updating form state with useReducer()
Each time dispatch
is called it should be called with an action
item that contains a type
and in this particular case a payload
as well. The state of tax year can be updated onClick
by firing
onClick={() => dispatch({ type: "taxYear", payload: year })}
instead of
onClick={() => setTaxYear(year)}
reducer(state, action)
is expecting to receive an action
that is an object with type
and payload
. Within the reducer function the action's type
and payload
are used to return the current state
with the [type]: payload
overwritten.
const { type, payload } = action;
return { ...state, [type]: payload };
In the case of updating the state from 2017 if the current state was:
const initialState = {
taxYear: 2019,
filingStatus: SINGLE,
income: "75000",
children: 0,
stimulusAmount: -1,
};
Then firing onClick={() => dispatch({ type: "taxYear", payload: 2018 })}
would result in the reducer returning the current state but with only the value of taxYear
overwritten and set to 2018. Note: this works as written becasue for each action in this example the type
of action is the same as its corresponding key value in state
.
Full Source Code of Examples
The full source code below compares the full implementations of the state management methods above. As was illustrated above, useReducer()
is another React hook that can be used for state management and can be implemented in a way that allows logic from useState()
hooks to be consolidated. The related source code for the current version of the calculator is available on GitHub.
source code using useState():
import { filingStatuses } from "../utils/constants";
import { getStimulusAmount } from "../utils/calculateStimulus";
import { useState } from "react";
function Form() {
const { SINGLE, HEADOFHOUSE, MARRIED } = filingStatuses;
const [taxYear, setTaxYear] = useState(2019);
const [filingStatus, setFilingStatus] = useState(SINGLE);
const [income, setIncome] = useState("75000");
const [children, setChildren] = useState(0);
const [stimulusAmount, setStimulusAmount] = useState(-1);
function handleSubmit(e) {
e.preventDefault();
setStimulusAmount(calculateStimulus(income, filingStatus, children));
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="tax-year">Have you filed your 2019 taxes yet?</label>
{[2019, 2018].map(year => (
<button
onClick={() => setTaxYear(year)}
className={year == taxYear ? "selectedButton" : ""}
key={year}
name="tax-year"
>
{year == 2019 ? "Yes" : "No"}
</button>
))}
<label htmlFor="filing-status">
What was your filing status in your {taxYear} taxes?{" "}
</label>
{[SINGLE, MARRIED, HEADOFHOUSE].map(status => (
<button
onClick={() => setFilingStatus(status)}
className={status == filingStatus ? "selectedButton" : ""}
name="filing-status"
key={status}
>
{" "}
{status}
</button>
))}
<br />
<label htmlFor="adjusted-income">
What was your adjusted gross income in {taxYear}?
</label>
${" "}
<input
type="number"
inputMode="numeric"
pattern="[0-9]*"
value={income}
onChange={e => setIncome(e.target.value)}
min={0}
name="adjusted-income"
/>
<br />
<label htmlFor="children">
How many children under age 17 did you claim as dependents in{" "}
{taxYear}?
</label>
<input
type="number"
inputMode="numeric"
pattern="[0-9]*"
value={children}
onChange={e => setChildren(e.target.value)}
min={0}
name="label"
/>
<br />
<button type="submit" className="calculateButton">
Calculate
</button>
<p>
{" "}
{stimulusAmount >= 0 &&
(stimulusAmount > 0
? `Your stimulus amount is expected to be $${stimulusAmount}.`
: `You are not expected to receive a stimulus.`)}
</p>
<br />
</form>
);
}
export default Form;
source code using useReducer():
import { useReducer } from "react";
import { filingStatuses } from "../utils/constants";
import { getStimulusAmount } from "../utils/calculateStimulus";
function reducer(state, action) {
const { type, payload } = action;
return { ...state, [type]: payload };
}
function Form() {
const { SINGLE, HEADOFHOUSE, MARRIED } = filingStatuses;
const initialState = {
taxYear: 2019,
filingStatus: SINGLE,
income: "75000",
children: 0,
stimulusAmount: -1,
};
const [state, dispatch] = useReducer(reducer, initialState);
function handleSubmit(e) {
e.preventDefault();
dispatch({
type: "stimulusAmount",
payload: getStimulusAmount(income, filingStatus, children),
});
}
const { taxYear, filingStatus, income, children, stimulusAmount } = state;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="tax-year">Have you filed your 2019 taxes yet?</label>
{[2019, 2018].map((year) => (
<button
onClick={() => dispatch({ type: "taxYear", payload: year })}
className={year == taxYear ? "selectedButton" : ""}
key={year}
name="tax-year"
>
{year == 2019 ? "Yes" : "No"}
</button>
))}
<label htmlFor="filing-status">
What was your filing status in your {taxYear} taxes?{" "}
</label>
{[SINGLE, MARRIED, HEADOFHOUSE].map((status) => (
<button
onClick={() => dispatch({ type: "filingStatus", payload: status })}
className={status == filingStatus ? "selectedButton" : ""}
name="filing-status"
key={status}
>
{" "}
{status}
</button>
))}
<br />
<label htmlFor="adjusted-income">
What was your adjusted gross income in {taxYear}?
</label>
${" "}
<input
type="string"
inputMode="numeric"
pattern="[0-9]*"
value={income}
onChange={(e) => dispatch({ type: "income", payload: e.target.value })}
min={0}
/>
<br />
<label htmlFor="children">
How many children under age 17 did you claim as dependents in {taxYear}?
</label>
<input
type="number"
inputMode="numeric"
pattern="[0-9]*"
value={children}
onChange={(e) =>
dispatch({ type: "children", payload: e.target.value })
}
min={0}
name="label"
/>
<br />
<button type="submit" className="calculateButton">
Calculate
</button>
<p>
{" "}
{stimulusAmount >= 0 &&
(stimulusAmount > 0
? `Your stimulus amount is likely to be ${new Intl.NumberFormat(
"en-US",
{ style: "currency", currency: "USD" }
).format(stimulusAmount)}.`
: `You are not expected to receive a stimulus.`)}
</p>
<br />
</form>
);
}
export default Form;
Top comments (8)
Good article - but could you elaborate on why you would choose one over the other? Pros/cons ? Or is it just a "here's a React hook for you redux folks..." difference?
I decided to convert from useState() to useReducer() as useReducer() is more DRY and I believe will be easier for me to maintain and extend over time. Both approaches work so I think I'd leave it up to personal preference.
During reading this article, I came up with an idea of what it might look like using a new vue 3 composition API.
And here we go:
And according to @dgreene 's question - at first glance, it reduces the amount of written code and simplifies it at all.
Furthermore, It would be easier to deal with initialState if the data structure has changed or has occurred another way to handle the given state or - like in my vue example - we need to add some kind of middleware during dispatching an action (console.log).
Correct me if I am wrong.
Oooo thanks for sharing your approach in Vue. I agree that useReducer() makes the hooks logic more reusable/DRY than useState() but that both approaches achieve the same thing.
Hi! I like the simplicity of the reducer in this case, but I would rename the references like so to make it more readable for me (the achilles heel of reducers imho)
Thanks for the naming suggestion! I'm always trying to figure out how to name things better.
The example is very clear and detailed. Thank you
Thanks for reading!