How To Use Redux Saga/Typescript Like a pro! II

Ernesto Jara Olveda
6 min readMay 7, 2020

React Redux

As we saw app-component has tree Routes. “First” route is the landing page. On this route we are going to fetch all the lists when the ComponentDidMount.

This is Kind of like a layout for a functional component:

import * React from "react";
import { RouteComponentProps } from "react-router-dom";
import { NewList, ListsState, ListState } from "@eneto/api-client";

This Component is a lil bit special as it is a route it comes with some special props such as history, location match and so on.

export interface LandingPageProps extends RouteComponentProps {      
lists: ListState[];
getListsRequests: ListsAction;
getListRequests: ListsAction;
postList: ListsAction;
}
  • lists — The array of lists stored in DB.
  • getListsRequests — Action creator that will trigger the Fetching.
  • getListRequestsTriggers the fetching of an specific list.
  • postListInserts new list.

PS: I usually don’t create the actions like this, but I just wanted to try some new

As this actions are related to lists then we should add that into the ./src/modules/lists/lists-actions.ts

import { AppAction } from "@eneto/api-client";export function listsAction<T, P> (type: T, payload: P): AppAction <T, P> {                           
return {
type,
payload,
};
}
export type ListsAction = typeof listsAction;

This is going to be the only Action Creator for lists-actions! :3.
To dispatch this action we need to dispatched it from a container, as containers are the ones connected to redux.

./src/containers/lists-container.ts

import { connect } from "react-redux";
import { AppState } from "@eneto/api-client"; import { LandingPage } from "../components/lists"; import { lists } from "../modules/lists"; import { listsAction } from "../modules/lists/lists-actions";

const stateToProps = (state: AppState) => ({
lists: [...lists(state)]
});

const dispatchToProps = {
getListsRequests: listsAction,
getListRequests: listsAction,
postList: listsAction,
};

export default connect(stateToProps, dispatchToProps)(LandingPage);

As we are going to pass the list of lists as prop, we need to get that selector from the store.

To do so we are going to make use of a module call: reselect this module to simplify what it does. it memorizes the selector and if it has not been modified. it just returns what it has, there’s no need to recompute if heavy process executes to get the value… not the case for our example, but does not hurt to have it.

// ./src/modules/lists/lists-selectors.tsimport { AppState, ListState } from "@eneto/api-client";
import { Selector } from "react-redux";
import { createSelector, OutputSelector } from "reselect";

We are going to separate the selector with the memorization.

// selector
const
getLists: Selector<AppState, ListState[]> = (state: AppState): ListState[] => state.lists.lists;

The selectors get the entire state as param.

export const lists: OutputSelector<AppState, ListState[], (res: ListState[]) => ListState[]> = createSelector<AppState, ListState[], ListState[]>([getLists], (lists: ListState[]) => lists);

[createSelector] get two parameters. the first one is a selector or an array of selectors, the second one is a callback.

if we reopen lists-container.ts and lets make use of it.

...
const stateToProps = (state: AppState) => ({
lists: [...lists(state)]
});
...

At the Representational Component.

...export function LandingPage (props: LadingPageProps): React.ReactElement<LadingPageProps> {  const [ listName, setListName ] = React.useState<string>("");
const [ listDescription, setListDescription ] = React.useState<string>("");
const [renderList, setRenderList] = React.useState<ListState[]>([]);
// ComponentDidMount
React.useEffect(() => {
const { getListsRequests } = props;
getListsRequests(GET_LISTS_REQUEST, undefined);
},[])
//DidComponentUpdate
React.useEffect(() => {
setRenderList([...lists])
}, [lists]);
return (
.....
);
}

In the same function LandingPage let’s add the onSubmitHandler

...
const onSubmitListHandler = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
event.preventDefault();
const { postList } = props;
postList(POST_LISTS_REQUEST, {
name: listName,
description: listDescription
} as NewList);
};
...

So Far we have getAllLists action & postList action, but if you notice. nothing happends. if we open the lists-reducer.ts

in the switch we only have GET_LISTS_SUCCESS & CLEAN_CURRENT_LIST , but if you open the presentational component, it is calling GET_LISTS_REQUEST

Open ./src/utils/constants.ts, you’ll notice some:

Every single Action” or Effects as that’s how actions are call. Well… each one has a: Request,Success,Failed State.

Well yes, not all of them has them three actions. only the ones that are thunks AKA: async.

I knd of lie a lil bit, if we look at the switch carefully we’ll C

default, which means that even tho we did not write down GET_LIST_REQUEST it is been register and as we do not do anything with it let it just fall into the default hihihih :3 so why we need a request and a failed… I mean you always have to handle exceptions.

The magic radicates within the sagas, but if we recap we are going to use two different type of sagas. Watchers/Workers.

Open ./src/modules/lists/lists-watchers.ts

There are all our REQUESTs ones. to listen to each request, we need to add its own watcher

/**
* Spawns a `saga` on each action dispatched to the Store that
* matches `pattern`.
*/
function* getAllListsWatcher () {
yield takeEvery(GET_LISTS_REQUEST, getAllListsWorker);
}

As I mentioned at the end of How To Use Redux Saga/Typescript Like a pro! I.

To handle CONCURRENCY there are two helpers

takeEvery

takeEvery allows multiple saga tasks to be forked concurrently.

takeLatest

takeLatest doesn't allow multiple Saga tasks to be fired concurrently. As soon as it gets a new dispatched action, it cancels any previously-forked task (if still running).

takeLatest can be useful to handle AJAX requests where we want to only have the response to the latest request.

So in less words every time the action/effect GET_LISTS_REQUEST is triggered the getAllListsWorker will run.

we is already here, so let’s do postListWorker.

when we call that Action in the component we past the NewUser as param.
let’s remember that this function is none but a middleware. in other words, postListWorker will be call right b4 the reducers switch is call. there for we have the action (Type & Payload). To access it set it as param.

export function* postLists (action: AppAction<POST_LISTS_REQUEST, NewList>) {
try {
const { payload } = action;
// Post Request
const res = yield call(listApi.postNewList, payload.name,
payload.description);
if (res === 201) {
yield put(listsAction(GET_LISTS_REQUEST, undefined));
}
yield put(listsAction(POST_LISTS_SUCCESS, res));
} catch (error) {
yield put(listsAction(POST_LISTS_FAILED, error));
}
}

import { call, put } from “redux-saga/effects”; call and put are imported from redux-saga/effects in case you wonder :3 also remember that CALL are blocking effects, but I told you that we were going to use only nonblocking effects!

thanks to the help of fork we is able to do so.

at the top of your watchers file we is going to import fork

import { fork, ForkEffect, takeEvery, takeLatest } from "redux-saga/effects";

and we is going to export the watchers as an array of ForkEffect

export const listsWatchers: ForkEffect[] = [
fork(getAllListsWatcher),
fork(getListWatcher),
fork(postListsWatcher),
fork(deleteListsWatcher),
];

The only problem with this is that as each saga is been fork on a separate thread there is not really a difference if we use takeEvery || takeLatest thats why I will remove takeLatest

Now let’s add the ForkEffect array into the rootSagas

./src/modules/index.ts

import { all } from "redux-saga/effects";
import {
userActions, userApi, userReducer, usersWatchers
} from "./users";
import {
listActions, listApi, listReducer, listsWatchers,
} from "./lists";
function* rootSagas () {
const watchers = [...listsWatchers, ...usersWatchers];
yield all(watchers);
}
export {
userActions,
userApi,
userReducer,
rootSagas,
listActions,
listApi,
listReducer,
listsWatchers,
};

At ./src/index.tsx we import rootSagas

Now you is ready to complete the whole project by your own!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response