Redux Saga Routines

I can now say that I officially used to be a fan of the Redux Saga Routines library. This library was great for simplifying the process of creating a common set of routines to describe the common stages of a saga fetch action: Trigger, Request, Success, Failure, and Fulfill.

AI Workflow
Microsoft Designer Prompt

A workflow to fetch items from a web server with stages of Trigger, Request, Success, Failure, and Fulfill. The workflow is often triggered by clicking a “Save” button. The stages are used to display a loading indicator to the user while waiting for a response from the server, and display potential errors.

Why so many actions? Here is a general overview of the lifecycle of a fetch request:

  • Trigger
    • When: Clicked “Save” button
    • Dispatched by components to perform an action
    • Captured by saga to start working
  • Request
    • When: About to make a request to a web server
    • Dispatched by working saga before it makes its request
    • Processed by reducer to remember we are “loading”
  • Success
    • When: Response from web server API request
    • Dispatched by saga as data is received
    • Processed by reducer to update state with new data
  • Failure
    • When: File not found, Server Error, Timed Out, Aborted
    • Dispatched by saga, often in the ‘catch’ block of try/catch
    • Processed by reducer to store the error state
  • Fulfill
    • When: Nothing left to do
    • Dispatched by saga, often in the ‘finally’ of a try/catch
    • Processed by reducer to remove “loading”

I’m still a fan of it’s use and standardization of the various stages, but the library is incompatible with typescript. Although typescript definitions exist for parts of the library, the typings of the actions created are wrong and I have to keep casting the proper types everywhere that they are used. For example, to dispatch an action, I would have to do the following:

dispatch(actions.load() as ReturnType<any>);

Dispatch is used everywhere. I was creating my actions using the createRoutine helper. No matter how I setup the generic type arguments, I couldn’t get any valid action creator functions that would be recognized in TypeScript that dispatch would accept.

Action Builder

I’ve written a helper function in the past that I use with this library to make it much simpler to create many routines for a given slice of my redux store. Here is how I use it with JavaScript, and how the routines are meant to be used.

actions.ts – build action creators
// Create the actions
const build = actionBuilder("types", emoji.barChart);
export const load = build("load");
export const reposition = build("reposition");
Component.ts – dispatch trigger action
export const Component = () => {
  const dispatch = useDispatch();
  const [id, setId] = useState(5);
  const onClickLoad = () => {
    dispatch(load({ id }));
  }
  return <button onClick={onClickLoad}>Load</button>
}
reducer.ts – update state as actions occur
const initialState = {
  loadError: undefined,
  isLoading: false,
  loaded: undefined
};
const loadTrigger = (state, action) => 
    ({...state, loadError: undefined });

const loadRequest = (state, action) => 
    ({ ...state, isLoading: true });

const loadSuccess = (state, {payload: loaded }) => 
    ({...state, loaded });
 
const loadError = (state, {payload: { error }} ) => 
    ({...state, loadError: error });

const loadFulfill = (state, action) => 
    ({...state, isLoading: false })

handleActions({
  [load.TRIGGER]: loadTrigger,
  [load.REQUEST]: loadRequest,
  [load.SUCCESS]: loadSuccess,
  [load.ERROR]: loadError,
  [load.FULFILL]: loadFulfill
  },
  initialState
);
saga.ts – perform fetch request and dispatch updates
export default takeEvery(
  load.TRIGGER, 
  function* worker(action) {
    yield put(load.request());
    try {
      const results = yield call(apiPost, 'search', action.payload);
      yield put(load.success(results))
    } catch (error) {
      yield put(load.failure({ error }));
    } finally {
      yield put(load.fulfill());
    }
  }
);

You can see how the common stages of Saga actions are implemented, and much of the repeated logic of creating the same actions are encapsulated within the action builder.

The action builder is meant to be created for each slice of the redux store, and so it accepts parameters to identify which slice it belongs to. I also add an emoji so that I can visually differentiate slices that the actions belong to when looking at the history in redux dev tools.

In addition to the default stages, I also added a helper that would also add “PROGRESS” and “ABORT” that allowed me to make additional requests if needed, and stop processing all together.

The action builder used createRoutine, createRoutineCreator, and defautlRoutineStages. As I started running into trouble making my helper utility work properly with TypeScript, I initially found that createRoutineCreator and defaultRoutineStages were not recognized as being exposed from the library, and found issue #75: CreateRoutineCreator – how to use with TypeScript? from four years ago. I’m using also @types/redux-saga-routines 3.1.8. The type definition library was last published from the Definitely Typed monorepo 10 months ago. Given that this issue is four year old, I don’t think that this type library gets much real attention other than fixing occasional linting problems or version compatibilities with node or other libraries.

The codebase hadn’t been touched since 2020. It looked like I was dealing with a library that was no longer maintained. I wasn’t using everything that the full library, and it seemed that what I was after specifically wasn’t all that difficult to implement. I just want five typed action creators.

Since I already had the action builder that encapsulated the underlying saga-action-routines, I only had to fix one place. Once I got it worked out, I realized that this could be modularized and thrown in its own library since it’s a bit of code I often reuse. Thus we have:

git: @CodeJamboree/action-builder
npm: @codejamboree/action-builder

Minified, it’s less than 1,000 characters of code and could probably be classified as a micro-library.

Minified Code of Action Builder 1.0

It feels great to have this encapsulated in its own library. Everything in the new system is switched over. I even added a few unit tests with the @codejamboree/js-test library and caught a few issues. I used rollup to bundle the the code for ESM and CommonJS along with the TypeScript definitions. I didn’t implement custom payload builder functions or meta data that Redux Actions permit as I don’t need them (at the moment). This is simply the bare functionality of what I primarily use now and in the past and lets me drop unnecessary code and third party dependencies.

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading