The useReducer
hook allows you to use a reducer for state management in your application. A reducer, of course, is simply a pure function that takes an action and state, and returns a copy of that state after applying the action on that state.
This allows for a lot finer control than the simplistic useState
hook, and basically allows you to have a redux-like store within the functional component to perform complex state management that would be cumbersome to write otherwise.
How to use useReducer step by step
Step 1 import {useReducer} from 'react';
Step 2 Create an initialState
object.
const initialState = {
imgWidth: 900,
imgSrc: '',
imgTitle: 'No title available',
imgDesc: 'No Description Available',
imgDate: '',
imgCopy: 'No copyright infomration available',
};
Step 3 Define action types your reducer will react to.
const GET_DATA_SUCCEEDED = 'GET_DATA_SUCCEEDED';
const GET_DATA_FAILED = 'GET_DATA_FAILED';
const SET_WIDTH = 'SET_WIDTH';
const SET_IMG_SRC = 'SET_IMG_SRC';
Step 4 Write a reducer
function that accepts state and some action and returns a new state that is the result of performing that action on previous state.
const apodReducer = (state, action) => {
const { type, data } = action;
switch (type) {
case GET_DATA_SUCCEEDED:
return {
...state,
imgSrc: data.url || '',
imgSrcHD: data.hdurl || '',
imgTitle: data.title || ' No title available',
imgDesc: data.explanation || 'No Description Available',
imgCopy: data.copyright || 'No copyright information available',
imgDate: getDate(), // this function exists somewhere
};
case SET_WIDTH:
return {
...state,
imgWidth: data,
};
case SET_IMG_SRC:
return {
...state,
imgSrc: data,
};
case GET_DATA_FAILED:
default:
return state;
}
};
Step 5 const [state, dispatch] = useReducer(apodReducer, initialState);
Step 6 Now you can call dispatch
with action type and data to set state within the app.
For example making an API call and updating state according to the response:
// make the apod request and set state
useEffect(() => {
const init = async () => {
const res = await getApod();
if (res) {
dispatch({
type: GET_DATA_SUCCEEDED,
data: res,
});
} else {
dispatch({
type: GET_DATA_FAILED,
});
}
};
init();
}, []);
This way you can perform complex state management within your component without having to use either Redux or useState!
Complete Example Code Component
I recently refactored my dummy nasa-apod app from 2016 style class component using setState to contemporary functional component using React Hooks.
Live Link
Old Class Component Code
In the old app I was using a class component with heavy use of setState
function to manage state and it was not very performant to say the least.
import React, { Component } from 'react';
import getApod from './services/apodService';
import {DIV, H1, H2, IMG, INPUT, P, SMALL} from './styles/styledComponents';
export default class Apod extends Component {
constructor(props){
super(props);
// initial state
this.state = {
imgSrc: '',
imgTitle: 'No title available',
imgDesc: 'No Description Available',
imgDate: this.getDate(),
imgCopy: 'No copyright infomration available'
};
}
componentDidMount(){
// call init
this.init();
}
// returns a date string
getDate(){
return new Date().toDateString();
}
// make the apod request and set state
init(){
getApod()
.then(function(res) {
console.info('Fetch complete',res);
this.setState({
imgSrc: res.url||'',
imgSrcHD: res.hdurl||'',
imgTitle: res.title||' No title available',
imgDesc: res.explanation||'No Description Available',
imgCopy: res.copyright||'No copyright information available',
imgWidth:'',
imgDate: this.getDate()
});
}.bind(this))
.catch((err)=>{
console.error(err);
});
}
render() {
return (
<DIV>
<H1>Astronomy Picture of the Day ({this.state.imgDate})</H1>
<IMG src={this.state.imgSrc} style={{ width: this.state.imgWidth + 'px'}}/>
<H2><a href={this.state.imgSrc}>{this.state.imgTitle}</a></H2>
<SMALL>© {this.state.imgCopy}</SMALL>
<br/>
<span>
<strong>Set Image Width:</strong>
<INPUT type="number" min="0" default="900" placeholder="enter a pixel value" value={this.state.imgWidth} onChange={this.setWidth.bind(this)} />
</span>
<P>
<strong>Explanation:</strong> {this.state.imgDesc}
</P>
</DIV>
)
}
// set dynamic image width
setWidth(e){
// if width more than 1000px use hdurl as src
if(e.target.value>=1000){
this.setState({imgSrc: this.state.imgSrcHD});
}
// change width on input
this.setState({imgWidth:e.target.value});
}
}
Refactored functional component version with React Hooks (useEffect, useReducer, useRef)
I refactored the old class version into a functional component with React hooks: using useEffect
to fetch the data, useReducer
to manage state, and useRef
to update the image's width.
Needless to say it is now much cleaner, performant and easier to maintain.
import React, { useEffect, useReducer, useRef } from "react";
import getApod from "./apodService";
const getDate = () => new Date().toDateString();
const Apod = () => {
const GET_DATA_SUCCEEDED = "GET_DATA_SUCCEEDED";
const GET_DATA_FAILED = "GET_DATA_FAILED";
const SET_WIDTH = "SET_WIDTH";
const SET_IMG_SRC = "SET_IMG_SRC";
const initialState = {
imgWidth: 900,
imgSrc: "",
imgTitle: "No title available",
imgDesc: "No Description Available",
imgDate: getDate(),
imgCopy: "No copyright infomration available"
};
const apodReducer = (state, action) => {
const { type, data } = action;
switch (type) {
case GET_DATA_SUCCEEDED:
return {
...state,
imgSrc: data.url || "",
imgSrcHD: data.hdurl || "",
imgTitle: data.title || " No title available",
imgDesc: data.explanation || "No Description Available",
imgCopy: data.copyright || "No copyright information available",
imgDate: getDate()
};
case SET_WIDTH:
return {
...state,
imgWidth: data
};
case SET_IMG_SRC:
return {
...state,
imgSrc: data
};
case GET_DATA_FAILED:
default:
return state;
}
};
const [state, dispatch] = useReducer(apodReducer, initialState);
const apod = useRef(null);
// set dynamic image width
const setWidth = e => {
// change width on input
dispatch({ type: SET_WIDTH, data: e.target.value });
};
// make the apod request and set state
useEffect(() => {
const init = async () => {
const res = await getApod();
if (res) {
dispatch({
type: GET_DATA_SUCCEEDED,
data: res
});
} else {
dispatch({
type: GET_DATA_FAILED
});
}
};
init();
}, []);
useEffect(() => {
// if width more than 1000px use hdurl as src
if (state.imgWidth >= 1000) {
dispatch({ type: SET_IMG_SRC, data: state.imgSrcHD });
}
if (apod.current) {
apod.current.style.width = `${state.imgWidth}px`;
}
}, [state.imgWidth, state.imgSrcHD, apod]);
return (
<div>
<h1>Astronomy Picture of the Day ({state.imgDate})</h1>
<img ref={apod} src={state.imgSrc} alt={state.imgTitle} />
<h2>
<a href={state.imgSrc}>{state.imgTitle}</a>
</h2>
<small>© {state.imgCopy}</small>
<br />
<span>
<strong>Set Image Width:</strong>
<input
type="number"
min="0"
placeholder="enter a pixel value"
defaultValue={state.imgWidth}
onChange={e => setWidth(e)}
/>
</span>
<p>
<strong>Explanation:</strong> {state.imgDesc}
</p>
</div>
);
};
export default Apod;
CodeSandbox Embed 👇