createModule

Detailed description of createModule helper

createModule: (opts: ModuleOpts) => SagaSlice

The secret sauce for saga-slice is the createModule helper. It brings together types, actions, reducers and sagas into 1 file, dramatically reducing the amount of boilerplate needed to create an object store. At the simplest level, this can be used to manage your redux state; but if you wanted to add asynchronous functionality and deal with side-effects, you can also add sagas to the same file.

Sample usage

import { createModule } from 'saga-slice';

export default createModule({
    name: 'todos',
    intialState: { /* ... */ },
    reducers: { /* ... */ },
    sagas: { /* ... */ }, // optional
    takers: { /* ... */ }, // optional
});
// Returns:
// {
//     name: 'todos',
//     actions: {/* ... */},
//     sagas: [/* ... */]
//     reducer: /* ... */
// }

How it works

  • When you create your saga slice, every reducer you create generates a subsequent type and action.

  • Types are created in the format of{name}/{key} where name is derived from opts.name and type is the key name defined in reducer object.

  • Reducers is a map of key/value pairs where key is used to generate types, and value is a reducer function.

    • Reducer functions have the signature (state, payload) => {} , where state is clone of the current state, which can be directly manipulated, and payload is the data that was dispatched into the action.

    • These functions are essentially immer producersarrow-up-right.

    • The way that you manipulate the state is by hoisting to the object directly. There is no need to return anything. You can minimally pass an empty function to simply declare a type and action.

Take this example:

This would generate slice.actions.randomAction and the todos/randomAction type, which would be dispatched using the action function:

Because we did not tell the reducer to manipulate the state in any way, this would do nothing but serve as a way to create types and actions; however, if we wanted to do something with what we passed into the action, we could do the following:

Options

Option Name

Required

Data Type

Description

name

yes

string

Name used to identify reducer in the global state and format redux types.

initialState

yes

object

The initial reducer state

yes

object

Map of reducers. Object values must be functions.

no

function

Sagas function (actions: any) => { [key: string]: SagaObject }

no

object | string | generator

Takers to be used. Can be a string that names a redux saga taker such as takeLatest. Can also be a generator function or map of { reducerName: takeLatest } or { takeLatest: ['reducerName'] }.

Actions Map

The resulting object from a createModule has an actions property which is a map of functions to dispatch redux actions. Action functions have a type property which returns the generated type. Take the following example:

slice.actions would have the following:

Sagas

Finally, we can define sagas. This entire section assumes that you have a basic understand of redux sagas. The sagas option is a function with the signature (actions) => ({}). The returned value should be an object of key value pairs where keys are action types to be passed into redux saga effects, and value is either a generator function or configuration object. This option is the only option not required for creating a saga-slice module.

circle-info

Meta programming ahead:

In the following example, you will notice a strange implementation that will not immediately make sense unless you understand the magic behind it.

Javascript is quirky and allows us to do weird things. Sometimes that's a good thing and we can do things like:

This happens because xyz.toString() generates the string 'function xyz () {}', and javascript coerces types into string in order to successfully create an object. We leverage this behavior for the creation of our saga configuration object.

.... wait... what?

Let's take it step by step by addressing the meta programming:

Under the hood, when actions are created, certain builtins are overwritten. In particular, the default Function.prototype.toString is overwritten for actions to always return its type. Essentially, actions.fetch.type is the same as actions.fetch.toString(). The latter is added for the convenience of using our actions as both keys and functions.

Theoretical example:

You could technically also do:

Really, whatever you feel comfortable with. The convenience of passing the action function is there so that you don't have to guess or potentially misspell the action types while you're writing sagas.

Next, let's address the saga configuration object. A saga config can either be a generator function, or a config with a generator function and/or a taker. If the value is a generator function, the saga will be ran using the takeEvery effect.

Option A: Generator function

Option B: Config object

For option B, essentially, any taker that can follow the signature function (type, saga) can be used to run the saga. In the cases where a taker doesn't exactly match that signature, such as debounce, you can always bind it to fulfill that effect's required arity.

See redux sagas API referencearrow-up-right for more details on what you can use.

Takers

Takers can be declared as part of configuration options in a multitude of ways. This is good for setting the default taker for all you sagas, or setting a taker for one or many of your sagas. You can use it as follows:

Allowed non-custom takers

When takers option is a string, or when a value in takers is an array, you can only apply the following effects:

  • takeEvery

  • takeLatest

  • takeMaybe

  • takeLeading

  • debounce

  • throttle

The following are acceptable configuration options

Last updated

Was this helpful?