DEV Community

Cover image for Display modal with network errors using a Redux reusable wrapper
Fernando González Tostado
Fernando González Tostado

Posted on • Updated on

Display modal with network errors using a Redux reusable wrapper

Hey,

Sometimes we want to share to our users what’s going on with his network requests, because as we all know, unfortunately those the network requests can fail, and when they do, handling and displaying them in an automated and reusable way without not repeating ourselves rewriting try/catch blocks all over and then somehow handling those errors and displaying them in the UI.

I will assume that you already have basic knowledge of Redux, ReduxToolkit and React.

Lets start creating a simple create-react-app with a reducer that will set the reusable modals whenever the network requests

First, we'll create actions using the createAsyncThunk method, whether they are actually async or not. You can still use them to dispatch data to the reducers as if the were in the reducers object of the slice .

The actions would look like this:

import { createAsyncThunk } from '@reduxjs/toolkit';

export interface ModalProps {
  title: string;
  children: React.ReactNode;
}

export const openModal = createAsyncThunk(
  'modals/open',
  async (props: ModalProps, { dispatch }) => {
    const randomNumber = Math.floor(Math.random() * 100);

    return { id: randomNumber, ...props };
  }
);

export const closeModal = createAsyncThunk(
  'modals/close',
  async (id: number, { dispatch }) => {

    return { id };
  }
);
Enter fullscreen mode Exit fullscreen mode

And the slice/reducer:

import { createSlice } from '@reduxjs/toolkit';
import { closeModal, ModalProps, openModal } from '../actions/modals';

interface ModalsInitialState {
  [key: number]: ModalProps;
}

// we want this to be an object with keys
// so we can select the modal by the key id
const initialState: ModalsInitialState = {};

const modalsSlice = createSlice({
  name: 'modals',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(openModal.fulfilled, (state, action) => {
      state[action.payload.id] = action.payload;
    });
    builder.addCase(closeModal.fulfilled, (state, action) => {
      delete state[action.payload.id];
    });
  },
});

export default modalsSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Great, so we can now dispatch open and close a modal with an id and any content that we want. But that doesn’t do a lot at the moment.

Now comes the magic. Let’s add a wrapper that we will call asyncThunkHandleError that will act as a high order component for all our thunks and will catch any error.

import { createAsyncThunk, AsyncThunkPayloadCreator, AsyncThunk } from '@reduxjs/toolkit';
import { openModal } from './actions/modals';

interface ThunkApiConfig {}
// apologies but I'm not sure how to type this one
// if you know, please let me know in the comments
export const asyncThunkHandleError = <Returned, ThunkArg = any> (
  typePrefix: string,
  thunk: AsyncThunkPayloadCreator<Returned, ThunkArg>,
  errorMessage?: string
): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> =>
  createAsyncThunk(typePrefix, async (arg, thunkAPI) => {
    try {
      // actually call the thunk
      return await thunk(arg, thunkAPI);
    } catch (error: any) {
      // here we will dispatch the modal with the error message
      thunkAPI.dispatch(
        openModal({
          title: `Error in ${typePrefix}`,
          children: `${
            error instanceof Error ? error.message : 'Unknown error'
          }`,
        })
      );
      return thunkAPI.rejectWithValue(error.response?.data || errorMessage);
    }
  });
Enter fullscreen mode Exit fullscreen mode

Here's the js version to make it more clear

import { createAsyncThunk } from '@reduxjs/toolkit';
import { openModal } from './actions/modals';

export const asyncThunkHandleError =  (
  typePrefix,
  thunk,
  errorMessage
) =>
  createAsyncThunk(typePrefix, async (arg, thunkAPI) => {
    try {
      // actually call the thunk
      return await thunk(arg, thunkAPI);
    } catch (error: any) {
      // here we will dispatch the modal with the error message
      thunkAPI.dispatch(
        openModal({
          title: `Error in ${typePrefix}`,
          children: `${
            error instanceof Error ? error.message : 'Unknown error'
          }`,
        })
      );
      return thunkAPI.rejectWithValue(error.response?.data || errorMessage);
    }
  });
Enter fullscreen mode Exit fullscreen mode

Next, we’ll create two thunks, one to get a positive response and one to force an error.

import { asyncThunkHandleError } from '../utils';

export const getPokemonApiOkReq = asyncThunkHandleError<Response, number>(
  'pokemon/api-ok',
  async (arg, thunkAPI) => {
    const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${arg}`);
    return response.json();
  },
  'Pokemon not found'
);

export const getPokemonApiFailedRequest = asyncThunkHandleError<Response, any>(
  'pokemon/api-failed',
  async (arg, thunkAPI) => {
    // just make the url to 404 fail
    const badResponse = await fetch(`https://pokeapi.co/api/v2/pokemonFooError/${arg}`);
    return badResponse.json();
  },
  'Backup error message'
);
Enter fullscreen mode Exit fullscreen mode

Now let’s add a button in the component that will trigger these two requests. Our component — I did this project with create-react-app— will look like this:

import './App.css';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from './store';
import {
  getPokemonApiFailedRequest,
  getPokemonApiOkReq,
} from './store/actions/pokemon';
import { Modal } from './Modal';
import { closeModal } from './store/actions/modals';

function App() {
  const dispatch = useDispatch<AppDispatch>();
  const { modals } = useSelector((state: RootState) => state);
  console.log(modals);

  const handleGetSuccessPokemonPayload = () => {
    dispatch(getPokemonApiOkReq(1));
  };

  const handleGetErrorPokemonPayload = () => {
    dispatch(getPokemonApiFailedRequest(null));
  };

  const handleModalClose = (id: number) => {
    dispatch(closeModal(id));
  }

  return (
    <div className='App'>
      <header className='App-header'>
        <p>
          <button onClick={handleGetSuccessPokemonPayload}>
            Fetch Pokemon Success
          </button>
        </p>
        <p>
          <button onClick={handleGetErrorPokemonPayload}>
            Fetch Pokemon Error
          </button>
        </p>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

If we console.log the store modals we’ll see that a new modal object is added at every new network error, that means that if we perform a set of several requests at once we’ll have one modal element, per failed request. Cool huh?

console-log

Now we should somehow display this information to the user. Let’s add a simple modal css-only modal.

import { useDispatch } from "react-redux";
import { AppDispatch } from "./store";
import { getPokemonApiFailedRequest } from "./store/actions/pokemon";

interface ModalProps {
  title: string;
  id: number;
  onClose: () => void;
  children: React.ReactNode;
}

export const Modal = ({
  title,
  children,
  id,
  onClose,
}: ModalProps): JSX.Element => {
  const dispatch = useDispatch<AppDispatch>();

  const handleCreateMoreModals = () => {
    dispatch(getPokemonApiFailedRequest(null));
  }

  return (
    <div className='modal-container'>
      <div className='modal-content'>
        <div className='title'>{title}</div>
        <div className='modal-body'>{children}</div>
        <p>{`Modal id : ${id}`}</p>
        <div className='modal-footer'>
          <button className='closes-modal btn-red' onClick={onClose}>
            Ok
          </button>
          <button className='closes-modal btn-red' onClick={handleCreateMoreModals}>
            Generate more modals
          </button>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

And finally, once we have this modal, we have to map every modal by id. This we’ll have one modal per error. If N requests failed, then Nmodals will be open and the user will be able to see one modal per failed request.

// get all the modals from the state
const { modals } = useSelector((state: RootState) => state);

// in your jsx
{Object.values(modals).map((modal) => (
  <Modal
    key={modal.id}
    id={modal.id}
    title={modal.title}
    children={modal.children}
    onClose={() => handleModalClose(modal.id)}
  />
))}
Enter fullscreen mode Exit fullscreen mode

Resulting into this:

err-1

To evidentiate the multi-modal feature, I’ve added the option to dispatch more errors inside the modal component. If you click the error button N times you’ll get N more modals, one displayed over another, so you’ll be able to get one error per failed request without missing any errors.

err-2

Every ok click will close that specific modal.

And that was it. I hope that this article will make your error handling easier in your next projects..

If you want to see the sample code you can find it here and run it by yourself.

First posted in Medium

Top comments (0)