redux-smart-creators

Smart creators for actions and reducers

Usage no npm install needed!

<script type="module">
  import reduxSmartCreators from 'https://cdn.skypack.dev/redux-smart-creators';
</script>

README

redux-smart-creators
GitHub Workflow Status Coverage Status

Strongly typed tools for fast and declarative Redux development

Documentation

Examples

WIP

Getting Started

Usage with Typescript

Getting Started

Installation

# with npm
npm install redux-smart-creators
# with yarn
yarn add redux-smart-creators

Action creators

There are two types of action creators:

  • Basic action creators return an object with an action type, and a payload (if a payload is specified)
  • Asynchronous action creators return an object with multiple basic action creators, each of which is characteristically responsible for a specific step of the asynchronous action.

Basic action creators

To create a basic action, use the getCreator function

import { getCreator } from 'redux-smart-creators'

Basic action creators without payload

Let's imagine that we have storage for a basic counter: { count: 0 }. Let's make an action creator to increment the counter by 1. This creator does not require a payload and has a 'incrementByOne' type.

const incrementByOne = getCreator('incrementByOne');

/** equal to:
 * const incrementByOne = () => ({ 
 *   type: 'incrementByOne' 
 * }) 
 */

store.dispatch(incrementByOne()) // dispatched { type: 'incrementByOne' }

Basic action creators with payload

In some cases, we need to increment the counter by a given value. To do this, we need an action creator that will accept the payload. To create it, use the load method

const incrementByValue = getCreator('incrementByValue').load()

/** equal to:
 * const incrementByValue = (value) => ({ 
 *   type: 'incrementByValue',
 *   payload: value
 * })
 */

store.dispatch(incrementByValue(10)) // dispatched { type: 'incrementByValue', payload: 10 }

Using more complex payloads

Now we need to increase the counter by the specified number several times. To do this, we use the load method along with a function that will calculate the value to add.

const calculateValue = (value, times) => value * times;
const incrementManyTimes = getCreator('incrementManyTimes').load(calculateValue)

/** equal to:
 * const incrementManyTimes = (value, times) => ({ 
 *    type: 'incrementManyTimes', 
 *    payload: value * times
 *  })
 */

store.dispatch(incrementManyTimes(10, 2)) // dispatched { type: 'incrementManyTimes', payload: 20 }

// You also can use inline functions:
const decrementManyTimes = getCreator('decrementManyTimes').load((value, times) => value * times)

Asynchronous action creators

Asynchronous action creator is an object with multiple basic action creators, each of which is characteristically responsible for a specific step of the asynchronous action. It also has a 'load' method for getting payload for chosen steps.

import { getAsyncCreator } from 'redux-smart-creators'

Asynchronous actions without Payload

For example, we have some storage: { count: 0 }. Now we are increasing the counter using a request to the server, and we need to track the various states of this action, such as initiation, loading, success and error.

To get these actions in one package, use the getAsyncCreator function. By default, it returns an object with four steps (INIT, LOADING, SUCCESS, FAILURE), represented as basic action creators without a payload. All of them will have computed action type, consisting of the type specified in the getAsyncCreator and the step name. Let's create an asynchronous increase by a given value:

const incrementByOneAsync = getAsyncCreator('incrementByOneAsync');

store.dispatch(incrementByOneAsync.INIT()) // dispatched { type: 'incrementByOneAsync[INIT]' }
store.dispatch(incrementByOneAsync.LOADING()) // dispatched { type: 'incrementByOneAsync[LOADING]' }
store.dispatch(incrementByOneAsync.SUCCESS()) // dispatched { type: 'incrementByOneAsync[SUCCESS]' }
store.dispatch(incrementByOneAsync.FAILURE()) // dispatched { type: 'incrementByOneAsync[FAILURE]' }

Defining own steps

If you want to define own steps (instead of INIT, LOADING, SUCCESS, FAILURE) for asynchronous action creators, provide them as array in second argument:

const Async = incrementByOneAsync('incrementByOne', ['start', 'finish', 'error']);

store.dispatch(incrementByOneAsync.start()) // dispatched { type: 'incrementByOne[start]' }
store.dispatch(incrementByOneAsync.finish()) // dispatched { type: 'incrementByOne[finish]' }
store.dispatch(incrementByOneAsync.error()) // dispatched { type: 'incrementByOne[error]' }

Asynchronous actions creators with payload

For providing a payload to asynchronous action creator use load method and payload helper function. You need to provide an object with steps you want to be injected with a payload. Use the payload function at the desired step to upgrade a step's action creator:

import { payload } from 'redux-smart-creators'

const incrementByValueAsync = getAsyncCreator('incrementByValue').load({
  INIT: payload(),
  SUCCESS: payload(),
  FAILURE: payload()
})

store.dispatch(incrementByValueAsync.INIT(10)) // dispatched { type: 'incrementByValue[INIT]', payload: 10 }
store.dispatch(incrementByValueAsync.SUCCESS(true)) // dispatched { type: 'incrementByValue[SUCCESS]', payload: true }
store.dispatch(incrementByValueAsync.FAILURE('Server Error')) // dispatched { type: 'incrementByValue[FAILURE]', payload: 'Server Error' }

// The rest of the steps are left unchanged:
store.dispatch(incrementByValueAsync.LOADING()) // dispatched { type: 'incrementByValue[LOADING]' }

Providing payload functions

To create more complex payloads, use your functions instead of imported payload function, as in basic actions creators

import { payload } from 'redux-smart-creators'

const calculateValue = (value, times) => value * times;
const incrementManyTimesAsync = getAsyncCreator('incrementManyTimes').load({
  INIT: payloadFunction, // You can use declared function
  SUCCESS: payload,
  FAILURE: (error) => error.message, // or inline functions
})

store.dispatch(incrementManyTimesAsync.INIT(10, 2)) // dispatched { type: 'incrementManyTimes[INIT]', payload: 20 }
store.dispatch(incrementManyTimesAsync.SUCCESS(true)) // dispatched { type: 'incrementManyTimes[SUCCESS]', payload: true }
store.dispatch(incrementManyTimesAsync.FAILURE({ message: 'Server Error', status: 500 })) // dispatched { type: 'incrementManyTimes[FAILURE]', payload: 'Server Error' }
  
// The rest of the steps are left unchanged:
store.dispatch(incrementManyTimesAsync.LOADING()) // dispatched { type: 'incrementManyTimes[LOADING]' }

Retrieving the action type

Actions creators have a type property that contains the literal of the corresponding action. Use this property when you need to access the action type.

const incrementByOne = getCreator('incrementByOne');
console.log(incrementByOne.type) // incrementByOne

const asyncIncrement = getAsyncCretor('asyncIncrement');
console.log(incrementByOne.INIT.type) // incrementByOne[INIT]
console.log(incrementByOne.LOADING.type) // incrementByOne[LOADING]

// with own steps:
const asyncDecrement = getAsyncCretor('asyncDecrement', ['start', 'finish']);
console.log(incrementByOne.start.type) // incrementByOne[start]
console.log(incrementByOne.finish.type) // incrementByOne[finish]

Creators pack

It is often necessary to combine several actions into one pack with same label. To create a package use the getCreatorPack function. The received package will have the functions for getting action creators.

import { getCreatorsPack } from 'redux-smart-creators'

Pack's label

Specify the name of the package, and it will be added to the type of each action creator obtained from this package.

const counter = getCreatorsPack('COUNTER');

const incrementByOne = counter.getCreator('incrementByOne');
console.log(incrementByOne.type) // @@COUNTER/incrementByOne

const incrementAsync = counter.getAsyncCreator('incrementAsync');
console.log(incrementAsync.INIT.type) // @@COUNTER/incrementAsync[INIT]
console.log(incrementAsync.LOADING.type) // @@COUNTER/incrementAsync[LOADING]

Pack's asynchronous steps

As the second argument, specify the steps that will be used by default in the asynchronous action creators taken from this package.

const counter = getCreatorsPack('COUNTER', ['start', 'finish']);

const incrementAsync = counter.getAsyncCreator('incrementAsync');
console.log(incrementAsync.start.type) // @@COUNTER/incrementAsync[start]
console.log(incrementAsync.finish.type) // @@COUNTER/incrementAsync[finish]

It is possible to specify steps as the only argument, but in this case, the package label will be empty and will not be applied to types of action creators.

const counter = getCreatorsPack(['start', 'finish']);

const incrementAsync = counter.getAsyncCreator('incrementAsync');
console.log(incrementAsync.start.type) // incrementAsync[start]
console.log(incrementAsync.finish.type) // incrementAsync[finish]

Package's default steps can be overwritten for a specific action creator during a getAsyncCreator function call.

const counter = getCreatorsPack('COUNTER', ['start', 'finish']);

const incrementAsync = counter.getAsyncCreator('incrementAsync', ['init', 'success']);
console.log(incrementAsync.init.type) // @@COUNTER/incrementAsync[init]
console.log(incrementAsync.success.type) // @@COUNTER/incrementAsync[success]

Reducers

The library provides a powerful tool for declarative development of reducers. First, import the setupReducer function

import { setupReducer } from "redux-smart-creators";

Creating basic reducer

To create a basic reducer, provide an initial state and call the create method. This reducer will always return the initial state

const initialState = 0;
const reducer = setupReducer(initialState).create()

Reducer's action handlers

To add some logic to the reducer, use the handlers before calling the create method.

.On

Executes the specified function while processing the specified action creator. The function takes the current state, and the payload of the action creator's action (if the action has a payload) as arguments. The result of executing the function will be a new state.

const initialState = 0;
const incrementByOne = getCreator('incrementByOne');
const incrementByValue = getCreator('incrementByValue').load()

const reducer = setupReducer(initialState)
        .on(incrementByOne, (state) => state + 1)
        .on(incrementByValue, (state, payload) => state + payload)
        .create()

You can provide a static value instead of a function and this value will become the new state.

const initialState = 0;
const switchToOne = getCreator('switchToOne');

const reducer = setupReducer(initialState)
        .on(switchToOne, 1)
        .create()

To handle multiple creators that trigger the same logic, put an array with these creators as an argument.

const initialState = 0;
const increment = getCreator('increment');
const incrementAsync = getAsyncCreator('incrementAsync');

const reducer = setupReducer(initialState)
        .on([increment, incrementAsync.SUCCESS], (state) => state + 1)
        .create()

Usage with Typescript

Typing action creators

Action creator with payload

By default, the action creator with a payload can accept any type of argument. To define a type, use a generic for load method or payload function.

import { getCreator, getAsyncCreator, payload } from 'redux-smart-creators'

const incrementByValue = getCreator('incrementByOne').load<number>();
incrementByValue('10') // Type Error;
incrementByValue(10) // Correct type;

const incrementByValueAsync = getAsyncCreator('incrementByValueAsync').load({
  INIT: payload<number>(),
})
incrementByValueAsync.INIT('10') // Type Error;
incrementByValueAsync.INIT(10) // Correct type;

If you use your function to define a payload, its typing will be used in the action creator.

import { getCreator, getAsyncCreator } from 'redux-smart-creators'

const incrementManyTimes = getCreator('incrementManyTimes').load((value: number, times: number) => {
  return value * times;
});
incrementManyTimes('10', 15) // Type Error;
incrementManyTimes(10, 15) // Correct type;

const roundValueAsync = getAsyncCreator('roundValueAsync').load({
  INIT: Math.round,
})
roundValueAsync.INIT('10.171') // Type Error;
roundValueAsync.INIT(10.99) // Correct type;

Asynchronous action creator's steps

Steps will be automatically typed if you define them during a getAsyncCreator or getCreatorsPack function call. If you define steps as a constant and then use it during a function call, the steps remain untyped. To fix this, directly define the type for the array with steps.

import { getAsyncCreator } from 'redux-smart-creators'

// Correct usage:
getAsyncCreator('roundValueAsync', ['first', 'second']);

// Incorrect usage, steps will not be defined in the action creator's type:
const untypedSteps = ['first', 'second']
getAsyncCreator('roundValueAsync', untypedSteps);

// Correct usage:
type Steps = 'first' | 'second';
const typedSteps: Steps[] = ['first', 'second']
getAsyncCreator('roundValueAsync', typedSteps);

Inferring root action

Often you need to get the root action (a union type of multiple actions). To get it, combine all exported action creators into one object and use it in the InfetActions generic type.

/** counterActions.ts */
import { getCreator, getAsyncCreator, payload } from 'redux-smart-creators'

export const increment = getCreator('increment');
export const incrementByValue = getCreator('incrementByValue').load<number>();

export const asyncIncrementManyTimes = getAsyncCreator('asyncIncrementManyTimes').load({
  INIT: (value: number, times: number): number => value * times,
  SUCCESS: payload<boolean>(),
  FAILURE: ({ message }: Error) => message
});

/** serverActions.ts */
export const checkServerStatusAsync = getAsyncCreator('checkServerStatusAsync');

/** types.ts */
import { InferActions } from 'redux-smart-creators';
import * as counterActions from './counterActions';
import * as serverActions from './serverActions';

export type CounterActions = InferActions<typeof counterActions>
/**
 * | { type: 'increment' }
 * | { type: 'incrementByValue', payload: number }
 * | { type: 'asyncIncrementManyTimes[INIT]', payload: number }
 * | { type: 'asyncIncrementManyTimes[LOADING]' }
 * | { type: 'asyncIncrementManyTimes[SUCCESS]', payload: boolean }
 * | { type: 'asyncIncrementManyTimes[FAILURE]', payload: string }
 */

export type ServerActions = InferActions<typeof serverActions>
/**
 * | { type: 'checkServerStatusAsync[INIT]' }
 * | { type: 'checkServerStatusAsync[LOADING]' }
 * | { type: 'checkServerStatusAsync[SUCCESS]' }
 * | { type: 'checkServerStatusAsync[FAILURE]' }
 */

export type RootAction = CounterActions | ServerActions

Typing reducers

WIP