vdr-reduxtoolkit-observable

tbd

Usage no npm install needed!

<script type="module">
  import vdrReduxtoolkitObservable from 'https://cdn.skypack.dev/vdr-reduxtoolkit-observable';
</script>

README

vdr-reduxtoolkit-observable

This library facilitates the use of redux-observable with redux-toolkit.

Installation

  • npm
    • npm install vdr-reduxtoolkit-observable
  • yarn
    • yarn add vdr-reduxtoolkit-observable

prerequisite

To know more about redux-toolkit and redux-observable, please visit:

https://redux-toolkit.js.org/

https://redux-observable.js.org/

Why to use this library

Basically, when we interact with an api, for each api call, we have 2 steps:

  • call the api
  • manage the api call result (success or error)

With redux-observable, we can convert these steps to 3 actions: Redux action | Description --- | --- fetch |this action will trigger the api call fetchSuccess |manage a successfull api call fetchError |manage a failed api call

These steps exist for each interaction with the api. If we have a read and an update api call, we will have 6 actions ( 3 for the read api call, 3 for the update api call)

  1. read_fetch ==> call the api
  2. read_fetchSuccess ==> manage the sucess call
  3. read_fetchError ==> manage the error call
  4. update_fetch ==> call the api
  5. update_fetchSuccess ==> manage the sucess call
  6. update_fetchError ==> manage the error call

The library will create and manage for you these steps. All you have to do is to create the reducers for these actions.

More information

AbstractEpicReducer

This class represents your action reducers (fetch, fetchSuccess, fetchError) + the api call. By extending the class you will implement 4 methods:

Method Description
fetchApiCall call the api
fetch reducer for the fetch action
fetchSuccess reducer for the fetch sucess action
fetchError reducer for the fetch error action

AbstractSingleReducer

This class represents the reducer for a single action.

You can of course create an action which does not interact with redux-observable. You will use this class in order to manage a single action. By extending the class you will implement one method:

Method Description
consumeAction reducer for your action

AbstractStateSlice

This class represents your state slice. It will generate the actions and the epics. By extending the class you will implement two methods:

Method Description
getSliceName define the reduxtoolkit slice name
getInitialState get the inital state

The constructor will take 2 parameters.

constructor(
  (epicReducers: Array<AbstractEpicReducer<State, any, any>> | null),
  (singleReducers: Array<AbstractSingleReducer<State, any>> | null),
);

Use case

1. Api information

In our example, we will work with the rest point https://jsonplaceholder.typicode.com/posts/{postId}

This request will return a json with 4 information ( userId, id, title, body)

/*  https://jsonplaceholder.typicode.com/posts/1 */
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

2. App requirements and behaviours

a) the user will search a post by encoding a digit

b) In addition to displaying the post information, we want to inform the user of the request status. The request status can be:

Request status Description
pending request on going
success request succeded
error request failed

c) If an error occured, we want to display the error message.

d) Via a reset button the user will be able to clear the screen

3. Actions

Regarding the app requirements and behaviours, we will have four actions:

Action Type Action Payload Description
post/read_fetch post id to search this action will trigger the request call
post/read_fetchSuccess contains the post information this action will be triggered if the request succeds
post/read_fetchError request has failed this action will be triggered if the request fails
post/reset null this action will be triggered when the user wants to clear the post information

Action types are generated by the library.

4. Designing the state

In addition of the post information, we will add:

  • the request status information.
  • the error message if the request has failed
State field Description
userId post information
id post information
title post information
body post information
errorMessage error message to display if the request fails
fetchStatus request status
/* represents the request possibles status */
enum EFetchStatus {
  NONE = 'NONE',
  PENDING = 'PENDING',
  SUCCESS = 'SUCCESS',
  ERROR = 'ERROR',
}

/* represents the request success result */
interface PostInfos {
  userId: Number;
  id: Number;
  title: string;
  body: string;
}

/* represents our slice state */
interface PostState extends PostInfos {
  errorMessage: string;
  fetchStatus: EFetchStatus;
}

5. Manage the api call

When the user searches a post

1 an action post/read_fetch is triggered
2,7 the reducer consumes the action
3,8 the reducer updates the state
4 check if a side effect exists for the action
5 make the api call
6a Request succeded -> trigger a post/read_fetchSuccess action
6b Request failed -> trigger a post/read_fetchError action

alt text

The redux-observable part is managed by the library.

To manage our api call, we will use the class AbstractEpicReducer. By using this class we will implement four methods:

Method Description schema step
fetchApiCall call the api 5
fetch reducer for the fetch action 3
fetchSuccess reducer for the fetch sucess action 8
fetchError reducer for the fetch error action 8

 extends AbstractEpicReducer<A,B,C>
State field Description
A state type
B fetch action payload type
C fetch action success payload type
// represents the post/read_fetch action payload */
interface ReadPostFetchPayload {
  postId: number;
}

// represents the post/read_fetchSuccess action payload */
interface ReadPostFetchSuccessPayload extends PostInfos {}

// /!\ This interface is provided by the library
// represents the post/read_fetchError action payload
interface FetchErrorPayload {
  errorCode: string;
  errorMsg: string;
  errorData: any;
}

export class ReadPostReducer extends AbstractEpicReducer<PostState, ReadPostFetchPayload, ReadPostFetchSuccessPayload> {
  // Schema step 5 - make the api call
  fetchApiCall = (payload: ReadPostFetchPayload) => findPostById(payload.postId);

  // Schema step 3 - reducer for the post/read_fetch action
  fetch = (state: PostState, payload: ReadPostFetchPayload) => ({
    ...POST_STATE_INTITIAL_STATE,
    fetchStatus: EFetchStatus.PENDING,
  });

  // Schema step 8 - reducer for the post/read_fetchSuccess action
  fetchSuccess = (state: PostState, payload: ReadPostFetchSuccessPayload): PostState => ({
    ...payload,
    errorMessage: '',
    fetchStatus: EFetchStatus.SUCCESS,
  });

  // Schema step 8 - reducer for the post/read_fetchError action
  // /!\ FetchErrorPayload is provided by the library
  fetchError = (state: PostState, payload: FetchErrorPayload) => {
    return {
      ...POST_STATE_INTITIAL_STATE,
      errorMessage: payload.errorMsg,
      fetchStatus: EFetchStatus.ERROR,
    };
  };
}

6. Manage the reset button with AbstractSingleReducer

When the user clicks on the reset button.

1 an action post/reset is triggered
2 the reducer consumes the action
3 the reducer updates the state

alt text

To manage this action, we will use the class AbstractSingleReducer. By using this class we will implement one method:

Method Description schema step
consumeAction(state, actionPayload) reducer for the action 3

 extends AbstractSingleReducer<A,B>
State field Description
A state type
B action payload type

export class ResetPostReducer extends AbstractSingleReducer<PostState, null> {
  consumeAction = (state: PostState, payload: null): PostState => POST_STATE_INTITIAL_STATE;
}

7.Create your state slice

Now that we have our reducers, we want to glue them with our store. We will use the AbstractStateSlice

const SLICE_NAME = 'post';
class PostStateSlice extends AbstractStateSlice<PostState> {
  // all our actions will be prefixed by this value
  getSliceName = () => SLICE_NAME;
  // provide the initial state
  getInitialState = () => POST_STATE_INTITIAL_STATE;
}

const postSlice = new PostStateSlice(
  [
    //provide a list of AbstractEpicReducer
    // the library will generate and manage three actions  post/read_fetch, post/read_fetchSuccess, post/read_fetchError,
    new ReadPostReducer(SLICE_NAME, 'read'),
  ],
  [
    //provide a list of  AbstractSingleReducer
    new ResetPostReducer('reset'), // the library will generate an action 'post/reset'
  ],
);

//get the reducers in order to add them to the state
export const postReducers = postSlice.slice.reducer;

//get the epics in order to add them to the state epic
export const postEpic = postSlice.epic;

//action to trigger to load a post
export const getActionFindPostById = (x: ReadPostFetchPayload) => postSlice.slice.actions.read_fetch(x);

//action to trigger to reset the state
export const getActionResetPost = () => postSlice.slice.actions.reset(null);
 extends AbstractStateSlice<A>
State field Description
A state type

8. Configure our store (app.store.ts)

We configure our store with the epic and reducer generated by the class PostStateSlice

const epicMiddleware = createEpicMiddleware();

const appStore = configureStore({
  devTools: true,
  reducer: combineReducers({
    post: postReducers,
  }),
  middleware: [epicMiddleware],
});

epicMiddleware.run(combineEpics(postEpic));

export default appStore;
  1. Add the store to our application (index.tsx)
ReactDOM.render(
  <Provider store={appStore}>
    <App />
  </Provider>,
  document.getElementById('root'),
);

9. Screenshots

alt text

alt text

alt text

10. Code

Full code is here: https://github.com/valentino-drappa/vdr-reduxtoolkit-epic-example

react #redux-toolkit #axios #redux-observable #rxjs