UNPKG

redux-smart-creators

Version:
405 lines (331 loc) 16.2 kB
<div align="center"> <h1> redux-smart-creators <br> <a href="https://github.com/ostashkin/redux-smart-creators/actions/workflows/release.yml"> <img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/ostashkin/redux-smart-creators/release"> </a> <a href='https://coveralls.io/github/ostashkin/redux-smart-creators?branch=master'> <img src='https://coveralls.io/repos/github/ostashkin/redux-smart-creators/badge.svg?branch=master' alt='Coverage Status' /> </a> </h1> <p>Strongly typed tools for fast and declarative Redux development</p> </div> ## Documentation **Examples** `WIP` **Getting Started** - [Installation](#installation) - [Action creators](#action-creators) - [Basic action creators](#basic-action-creators) - [Asynchronous action creators](#asynchronous-action-creators) - [Retrieving the action type](#retrieving-the-action-type) - [Creators pack](#creators-pack) - [Reducers](#reducers) **Usage with Typescript** - [Typing action creators](#typing-action-creators) - [Typing reducers](#typing-reducers) # Getting Started ## Installation ```bash # 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 ```typescript 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. ```typescript 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 ```typescript 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. ```typescript 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. ```typescript 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](#basic-action-creators-without-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: ```typescript 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: ```typescript 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: ```typescript 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](#using-more-complex-payloads) ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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 ```typescript 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 ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript /** 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`