Using useReducer React Hook

Using useReducer React Hook

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>&copy; {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>&copy; {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 👇

Show Comments