Display modal with network errors using a Redux reusable wrapper
Tags: typescript, redux, reduxtoolkit
Published: 2022-11-17 23:02:33

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:
```tsx
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 };
  }
);
```
And the slice/reducer:

```tsx
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;
```

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.

```tsx
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);
    }
  });
```

Here's the js version to make it more clear
```js
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);
    }
  });
```

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

```tsx
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'
);
```

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:

```tsx
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;
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4mkdpv4t3jop12s796yj.png)

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

```tsx
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>
  );
};
```

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 `N`modals will be open and the user will be able to see one modal per failed request.

```tsx
// 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)}
  />
))}
```

Resulting into this:

![err-1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5trzhinx622r521b3ndc.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ed3909tdt99129zenjd.png)

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](https://github.com/esponges/thunk-network-error-wrapper-with-modals) and run it by yourself.

First posted in [Medium](https://medium.com/@esponges/display-modal-with-network-errors-using-a-redux-reusable-wrapper-12c306971852)