React's useState
hooks is very powerful tool to manage states withing functional components. It's simple and perfect for handling simple state changes like updating a boolean, a string or a counter. but as the state grows in complexity like the state of form fields useState
hooks show it's limitations.
Dealing with multiple interrelated pieces of state or complex state logic can be very difficult to manage using useState
. This is where useReducer
hook comes in, It provider a structured and maintainable approach for dealing with complex state scenarios.
The benefits of useReducer hook.
useReducer
hook centralize state logic and creates a single source of truth for updating state by encapsulating state logic within reducer function. The reducer is a pure function which ensure that given same input always produce same output, which makes the state more predictable and testable.
This hook also improve and organize code by separating state logic from component which also result in performance enhancement sometimes.
Understanding useReducer.
there is 3 core concepts that must be understand in useReducer
hook which are:
-
State: similar to
useState
it represent the data that the component need to render. -
Action: an Object that has
type
property to identify the action and custom payload for additional data. - Reducer: it's a pure function that accepts the state and action as parameters and return the state it is within the reducer function how the state change is determined.
In this example we will create a simple counter by pressing a button using useReducer
, Then later add a name and updating to show an example of complex and unrelated state updates.
Implementing useReducer logic.
First create a constant with initial state above the component.
const initialState = {counter: 0}
const App = () => {
...
}
export default App;
In the component the reducer function must be created outside the functional component. That function will accept a state and action as parameters, then will use switch statement to determine the type of the action and change the state if the action is unknown then will return just the state and the component won't re-render because the state hasn't changed as it's determined by Object.is.
const initialState = {counter: 0}
const reducer = (state, action) => {
switch(action.type) {
case "increment_counter":
return {
counter = state.counter + 1
}
default:
return state
}
}
const App = () => {
...
}
export default App;
Inside the component we call useReducer
, which accepts the reducer function and an initial state.
PS: the initial state can be calculated and passed as function but we must pass reducer the props of state function then the state function like this
const initialStateFn = (name) => {...}
const [state dispatch] = useReducer(reducer, name, initialStateFn)
we can access the state and dispatch function from useReducer
hook. the dispatch function accepts an object with the type of the action and options parameter to determine how the state will be updated in reducer function.
import {useReducer} from 'react'
const initialState = {counter: 0}
const reducer = (state, action) => {
switch(action.type) {
case "increment_counter":
return {
counter = state.counter + 1
}
default:
return state
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (...)
}
export default App;
now we can display and update the state inside the return statement of component like so
const App = () => {
...
return (
<div>
<button onClick={() => dispatch({type: 'increment_count'})>Increment counter</button>
<p>{state.counter}</p>
</div>
)
}
Making state more complex
Now we also want to add input and type something on it, Will have to update the logic in 3 different place.
Update the initial state to include like a name
const initialState = {
counter: 0,
name: ''
}
Update switch statement to handle the action of updating the input will call it updateName
. we also need return new state object to keep the previous state intact in this example the counter will kept it's previous state when the state of the name is been updated.
also notice we are using action.nextName
which is an optional parameter passed to dispatch function dispatch({type: 'changeName', nextName: nextName})
, well add next.
const reducer = (state, action) => {
switch(action.type) {
case "increment_counter":
return {
...state,
counter = state.counter + 1
}
case "updateName":
return {
...state,
name = action.nextName
}
default:
return state
}
}
- Update the markup in component.
const App = () => {
...
return(
<div>
...
<input value={state.value}
onChange={(event) => dispatch({
type: 'updateName',
nextName: event.target.value
})}
/>
</div>
)
}
In the end will have something like this.
import {useReducer} from 'react'
const initialState = {
counter: 0,
name: ''
}
const reducer = (state, action) => {
switch(action.type) {
case "increment_counter":
return {
...state,
counter = state.counter + 1
}
case "updateName":
return {
...state,
name = action.nextName
}
default:
return state
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<button onClick={() => dispatch({type: 'increment_count'})>Increment counter</button>
<p>{state.counter}</p>
<input value={state.value}
onChange={(event) => dispatch({
type: 'updateName',
nextName: event.target.value
})} />
</div>
)
}
export default App;
Conclusion
By understanding and utilizing the useReducer
hook, you can handle complex state logic, promote code organization and improve the predictability of the state, making this hook invaluable for building scalable and maintainable components.
Top comments (0)