by Francesco Agnoletto

React hooks vs Redux part 2

Async the easy way

Photo by Solstice Hannan on Unsplash

In the first article, we explored how a React hook can simplify a basic Redux app.

The real value of Redux comes with more complex needs, such as fetching data from external APIs. Countless articles already explain how to do it.

In this article, we will build a simple application fetching data from a server.

I’ll take some example code from Dave Ceddia’s article on how to fetch data with Redux. Dave Ceddia provides a good explanation of how Redux handles everything. His article provides a more in-depth look at the code I will use below.

Redux

Redux can’t handle asynchronous actions by default. We are going to need redux-thunk as an extra dependency to handle them.

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

We will define our async fetch in its own file for simplicity. I took the liberty of rewriting it using async/await.

// fetchProducts.js
const fetchProducts = async () => {
  return dispatch => {
    try {
      dispatch(fetchProductsBegin());
      const res = await fetch("./products");
      const data = await res.json();
      dispatch(fetchProductsSuccess(json.products));
    } catch (error) {
      dispatch(fetchProductsFailure(error));
    }
  };
}

After that, we are going to need three actions, one for requesting the data, a success and an error one.

// actions.js
export const FETCH_PRODUCTS_BEGIN = 'FETCH_PRODUCTS_BEGIN';
export const FETCH_PRODUCTS_SUCCESS = 'FETCH_PRODUCTS_SUCCESS';
export const FETCH_PRODUCTS_FAILURE = 'FETCH_PRODUCTS_FAILURE';

export const fetchProductsBegin = () => ({
  type: FETCH_PRODUCTS_BEGIN
});

export const fetchProductsSuccess = products => ({
  type: FETCH_PRODUCTS_SUCCESS,
  payload: { products }
});

export const fetchProductsFailure = error => ({
  type: FETCH_PRODUCTS_FAILURE,
  payload: { error }
});

Our actions are then imported into a reducer where we will handle all three cases.

// reducer.js
import {
  FETCH_PRODUCTS_BEGIN,
  FETCH_PRODUCTS_SUCCESS,
  FETCH_PRODUCTS_FAILURE
} from './productActions';

const initialState = {
  items: [],
  loading: false,
  error: null
};

const productReducer = (state = initialState, action) => {
  switch(action.type) {
    case FETCH_PRODUCTS_BEGIN:
      return {
        ...state,
        loading: true,
        error: null
      };

    case FETCH_PRODUCTS_SUCCESS:
      return {
        ...state,
        loading: false,
        items: action.payload.products
      };

    case FETCH_PRODUCTS_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload.error,
        items: []
      };

    default:
      return state;
  }
}

export default productReducer;

In the end, the component responsible for rendering our application.

import React from "react";
import { connect } from "react-redux";

import fetchProducts from "./fetchProducts"

class ProductList extends React.Component {
  componentDidMount() {
    this.props.dispatch(fetchProducts());
  }

  render() {
    const { error, loading, products } = this.props;

    if (error) {
      return <div>Error! {error.message}</div>;
    }

    if (loading) {
      return <div>Loading...</div>;
    }

    return (
      <ul>
        {products.map(product =>
          <li key={product.id}>{product.name}</li>
        )}
      </ul>
    );
  }
}

const mapStateToProps = state => ({
  products: state.products.items,
  loading: state.products.loading,
  error: state.products.error
});

export default connect(mapStateToProps)(ProductList);

The need for code organization becomes important as we deal with a growing number of actions.

React hooks

Can React hooks solve the same problem with the same elegance?

First, we need to rewrite the fetchProducts function to be more generic.

export const fetchProducts = async () => {
  try {
    const res = await fakeGetProducts();
    return res.products;
  } catch (error) {
    return error;
  }
};

We are going to use the UseReducer hook, it offers a similar structure to what Redux gives us. The same actions defined above can be re-used, as well as the reducer itself.

import React, { useReducer, useEffect } from "react";
import {
  fetchProducts,
  fetchProductsSuccess,
  fetchProductsFailure
} from "./productActions";
import reducer from "./reducer"

const initialState = {
  loading: true,
  error: null,
  products: []
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  // equal to componentDidMount in this particular case,
  // the empty array as second argument will make it only run once
  // instead of every re-render
  // https://reactjs.org/docs/hooks-reference.html#timing-of-effects
  useEffect(() => {
    fetchProducts()
      .then(response => dispatch(fetchProductsSuccess(response)))
      .catch(error => dispatch(fetchProductsFailure(error)));
  }, []);

  const { error, loading, products } = state;

  if (error) {
      return <div>Error! {error.message}</div>;
    }

    if (loading) {
      return <div>Loading...</div>;
    }

    return (
      <ul>
        {products.map(product =>
          <li key={product.id}>{product.name}</li>
        )}
      </ul>
    );
}

Full codesandbox can be found here.

The APIs are pretty similar and it’s quite easy to change one for the other. Small and atomic reducers offer a way to decrease the complexity of your application.

Switching Redux for Hooks can help simplify the codebase by making it more local. Not everything should be in the global scope of the application. The added benefit of dropping classes is a nice cherry on top.