@mrtd/redux-builder

> Generate your reducers, actions and manage your data, status and side effects easily ! > This library aims to provide [Redux] / [Redux-Saga] capatibilities for highly-scalable app with reduced code. > I'm a [Typescript] library > I use it with React, bu

Usage no npm install needed!

<script type="module">
  import mrtdReduxBuilder from 'https://cdn.skypack.dev/@mrtd/redux-builder';
</script>

README

Redux-builder

Generate your reducers, actions and manage your data, status and side effects easily ! This library aims to provide Redux / Redux-Saga capatibilities for highly-scalable app with reduced code. I'm a Typescript library I use it with React, but feel free to use it with anything if the need arise !

(How to use below)

Ideas behind redux-builder

I've been using redux for a long time now, always the same way. 3 files : Actions / Reducer / Saga. In my apps, reducers are linked to one external service, usually making a request. Each reducer has three parameters :

  • status : Used to understand what state the service request is at
  • error: Used to display an error, usually a string for me (Which I then translate in my app)
  • some data, which is saved from the service result.

I have 4 status :

  • "Start": Base status for my reducer
  • "Loading": When the service has been called but did not respond yet
  • "Failure": When the service returned an error
  • "Success": When the service returned expect response

And 4 actions :

  • Start : Called with the expected parameters to call the service, change the reducer's status to "Loading", captured by redux-saga to then make the call.
  • Failure : Called when the the service throw an error, set an error inside the reducer.
  • Success : Called by redux-saga to set some expected data inside the reducer.
  • Reset : Called to put the whole reducer state back to its initial state.

This structure works very well for me, allow me to avoid any side-effect / complexity I had previously. The only downside is the amount of code needed on the redux-side and its repetitiveness.

One method to rule them all

redux-builder export a main function in order to build a reducer with its linked actions and saga.

import generateRedux from '@morintd/redux-builder';

This function is generic and takes 3-4 type variables :

  • "StartDataType" : let's say your reducer aim to fetch an article by slug, this type would be something like
type StartDataType = { 
    slug: string;
}
  • SuccessDataType : The type that is returned by your service, something like :
type SuccessDataType = {
    article: { 
            title: string; 
            slug: string; 
            content: string
        }
    }
  • ErrorDataType, mine is always a string but feel free to use any other one.
  • StoredDataType, this one is optional. By default the module will expect a SuccessDataType but you can store something else.

If you only declared Start/Success DataType and want to store a string as error, you have the following :

import generateRedux from '@morintd/redux-builder';

generateRedux<StartDataType, SuccessDataType, string>

Or, if you use a StoredDataType

import generateRedux from '@morintd/redux-builder';

generateRedux<StartDataType, SuccessDataType, string, StoredDataType>

Now let's have a look at what arguments we can pass to this function.

  • First is a string which will be used to build actions types, if my reducer aims to load an article by slug, it would be something like "FETCH_ARTICLE_BY_SLUG"
  • Then, it takes an object that will be the initial data, something that'll match the given data to store. For my article, it would be :
article: {
    slug: "",
    title: "",
    content: ""
}
  • An initial error, matching ErrorDataType
  • Finally, it takes an object that's sagas for the current reducer. This object can posses four properties, each working as a worker for redux-saga :
    • onStart: triggered when action "Start" is dispatched, receive a payload matching StartDataType and must return data matching "SuccessDataType". If this one throw an error, the inner saga will dispatch a failure action.
    • onFailure: triggered when action "failure" is dispatched receive a payload matching "ErrorDataType", must not return anything
    • onSuccess: triggered when action "success" is dispatched receive a payload matching "SuccessDataType", must return data matching "StoredDataType".
    • onReset, triggered when action "reset" is dispatched, receive and return nothing.

Putting everything together / How to use

The function generateRedux return an object with 4 properties : reducer, saga, workers, and actions.

  • "workers" is a property used for mainly for testing purpose containing every saga workers.
  • "reducer" can be used as just any other reducer
  • "saga" contains a main worker, its defined in order to intercept (with takeLatest) corresponding action in order to call the right worker.
  • "actions" : contains a list with actions for the generated reducer, always in the same order :
    • action "start"
    • action "success"
    • action "error"
    • action "reset"
    const { reducer, workers, actions } = generateRedux<StartDataType, SuccessDataType, string>(
        'FETCH_ARTICLE_BY_SLUG', // Base action
        article: { // Initial data
            slug: "",
            title: "",
            content: ""
        },
        '', // Initial error
        { // Workers
        onStart: function*(payload: StartDataType) {
          const request = (slug: string) =>
            Promise.resolve(
              [{ title: 'title', slug: 'slug', content: 'content' }].filter(
                article => article.slug === slug
              )
            )

          const articles = yield call(request, payload.slug)
          return { articles }
        }
      }
    );
    
        const [fetchArticle, fetchArticleSuccess, fetchArticlesFailure, fetchArticlesReset] = actions;
        
        export { reducer, saga, fetchArticle, fetchArticleSuccess, fetchArticlesFailure, fetchArticlesReset };

This way, i can just import my reducer and saga to use it as any other ones, and my actions with meaningful names to be used.

Advanced use

I want to use the main "generateRedux" function but i don't need a StartDataType

You can use this function with void instead of StartDataType :

    generateRedux<void, SuccessDataType, string>(...);

I want to use the main "generateRedux" function but i don't need to store anything

You can use "void" instead of StoredDataType and pass "undefined" as initialData :

    generateRedux<StartDataType, SuccessDataType, string, void>('BASE_ACTION', undefined, ...);

I would like to generate a reducer with its action, without a complex status/error handling

Another useful function exported by redux-builder is generateSimpleReducer. It takes a type variable matching data that'll be store, and two parameters :

  • A base action, used the same way as "generateRedux"
  • an initial data, matching the variable type given.

It returns a reducer and its action as a list. Here's an example for a "localReducer" aiming to store a language :


import { generateSimpleReducer } from '@mrtd/redux-builder';

const [reducer, setLocale] = generateSimpleReducer<{ language: 'fr' | 'en' }>('SET_LOCALE', { language: 'en' });

export { reducer, setLocale };

The inner functions of this package can be used if you need them, see Documentation