DEV Community

Hunter Chang
Hunter Chang

Posted on • Originally published at codebushi.com

Higher Order Reducers with React Hooks

This post was originally published at codebushi.com

When managing React state with the new useReducer hook, you may find that there are commonly repeated action types and logic in your reducer functions. Here is an easy way of using higher order functions to make your custom hooks and reducer logic more flexible and re-useable.

To demonstrate this, let's pretend that we're fetching some Todos and will be controlling the loading and error states. We will also be able to delete a todo using the id.

import React, { useEffect, useReducer } from 'react';

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

function todosReducer(state, action) {
  switch (action.type) {
    case 'LOADING':
      return {
        ...state,
        loading: action.loading // Should be true/false
      };
    case 'ERROR':
      return {
        ...state,
        loading: false,
        error: action.error
      };
    case 'SET_DATA':
      return {
        loading: false,
        error: false,
        data: action.data
      };
    case 'DELETE_DATA':
      return {
        ...state,
        data: state.data.filter(datum => datum.id !== action.id)
      };
    default:
      return {
        ...state
      };
  }
}

const TodosPage = () => {
  const [state, dispatch] = useReducer(todosReducer, initialState);
  return (
    <div>
      {state.data.map(todo => (
        <TodoComponent key={todo.id} />
      ))}
    </div>
  );
};

export default TodosPage;
Enter fullscreen mode Exit fullscreen mode

To keep this example simple, I'm not going to actually fetch any data, we'll just pretend that it looks something like this:

// Sample Todos Data
const todos = [
  {
    id: 1,
    title: 'Go Shopping'
  },
  {
    id: 2,
    title: 'Go To Gym'
  }
];
Enter fullscreen mode Exit fullscreen mode

This is pretty standard when dealing with fetching data of any kind. If there are multiple pages that need this reducer logic, we can pull it out into a custom hook.

// Our useFetchData Custom Hook
import React, { useEffect, useReducer } from 'react';

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

function dataReducer(state, action) {
  switch (action.type) {
    case 'LOADING':
      return {
        ...state,
        loading: action.loading
      };
    case 'ERROR':
      return {
        ...state,
        loading: false,
        error: action.error
      };
    case 'SET_DATA':
      return {
        loading: false,
        error: false,
        data: action.data
      };
    case 'DELETE_DATA':
      return {
        ...state,
        data: state.data.filter(datum => datum.id !== action.id)
      };
    default:
      return {
        ...state
      };
  }
}

const useFetchData = ({ url }) => {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    const getInitialData = async () => {
      try {
        const response = await fetch(url);
        const data = await response.json();
        dispatch({
          type: 'SET_DATA',
          data
        });
      } catch (error) {
        dispatch({ type: 'ERROR', error });
      }
    };
    getInitialData();
  }, [url]);

  return [state, dispatch];
};

export default useFetchData;
Enter fullscreen mode Exit fullscreen mode

To use the custom hook in the original TodosPage looks like this:

import useFetchData from '../hooks/useFetchData';

const TodosPage = () => {
  const [state, dispatch] = useFetchData({
    url: 'https://someTodosApi'
  });

  return (
    <div>
      {state.data.map(todo => (
        <TodoComponent key={todo.id} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

So far we haven't done anything tricky yet. If we have a different page, we can easily re-use the custom hook by passing in a different url for the api. However, what if on the other page the data looks a bit different? Instead of Todos, what if there were Contacts that needs to be displayed and deleted?

// Sample Contacts Data
const contacts = [
  {
    contactId: 1,
    name: 'John Doe'
  },
  {
    contactId: 2,
    name: 'Jane Doe'
  }
];
Enter fullscreen mode Exit fullscreen mode

Notice how the keys are now contactId instead of just id. This is just one of many examples of how data can be slightly different. We can still use most of our custom hook, but when we go to delete the data, we'll need to use contactId instead of id.

case 'DELETE_DATA':
  return {
    ...state,
    data: state.data.filter(datum => datum.contactId !== action.id)
  };
Enter fullscreen mode Exit fullscreen mode

How can we tweek just this tiny part of our custom hook so that we can re-use it? Well, since a reducer is just a function, we can call upon the power of higher order functions in Javascript by having our dataReducer function return another function. Some call this a higher order reducer.

What we want is, on the Contacts page, to pass in a string of whatever the key is so we can filter on that string, instead of the hard coded id that is currently in our hook.

// Contacts Page
const ContactsPage = () => {
  const [state, dispatch] = useFetchData({
    url: 'https://someContactsApi',
    recordKey: 'contactId'
  });

  return (
    <div>
      {state.data.map(contact => (
        <ContactComponent key={contact.contactId} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

We'll need to adjust our custom hook to take in this new recordKey variable and use it in our dataReducer.

import React, { useEffect, useReducer } from 'react';

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

function dataReducer(recordKey) {
  return function(state, action) {
    switch (action.type) {
      case 'LOADING':
        return {
          ...state,
          loading: action.loading
        };
      case 'ERROR':
        return {
          ...state,
          loading: false,
          error: action.error
        };
      case 'SET_DATA':
        return {
          loading: false,
          error: false,
          data: action.data
        };
      case 'DELETE_DATA':
        return {
          ...state,
          data: state.data.filter(datum => datum[recordKey] !== action.id)
        };
      default:
        return {
          ...state
        };
    }
  };
}

const useFetchData = ({ url, recordKey }) => {
  const [state, dispatch] = useReducer(dataReducer(recordKey), initialState);

  useEffect(() => {
    const getInitialData = async () => {
      try {
        const response = await fetch(url);
        const data = await response.json();
        dispatch({
          type: 'SET_DATA',
          data
        });
      } catch (error) {
        dispatch({ type: 'ERROR', error });
      }
    };
    getInitialData();
  }, [url]);

  return [state, dispatch];
};

export default useFetchData;
Enter fullscreen mode Exit fullscreen mode

Our custom hook can now handle any kind of keys we throw at it! This was a pretty simple example, but keep in mind we can pass anything into our higher order reducer and have the conditional logic live inside the returned reducer function. With React hooks, it's a lot easier to recognize common logic that is shared between components. It's also easier to re-use component logic and share that throughout your application.

Top comments (2)

Collapse
 
ematipico profile image
Emanuele Stoppa

Why did you call it high order reducer and not factory reducer or high order function?

From what I can see, it's not a real reducer because it doesn't handle state and/or action.

Collapse
 
changoman profile image
Hunter Chang

Hey! I actually got the name from this Redux article: bit.ly/2Yg2Svy

The example they show something similar, a function that returns a reducer. I guess you can call it a reducer factory or just a higher order function that returns a reducer. I just wanted to emphasize that a reducer is being returned.