Awesome
DEPRECATION NOTICE
There is now an official library called @reduxjs/toolkit
which does
everything this library set out to solve and more! I have decided to deprecate
redux-ts-utils
in favor of @reduxjs/toolkit
.
redux-ts-utils
Everything you need to create type-safe applications with Redux! Flux Standard Action compliant.
Example Usage
import { createStore, Store } from 'redux';
import { createAction, handleAction, reduceReducers } from 'redux-ts-utils';
// Actions
const increment = createAction<void>('increment');
const decrement = createAction<void>('decrement');
const add = createAction<number>('add');
const override = createAction<number>('override');
// Reducer
type State = {
readonly counter: number,
};
const initialState: State = {
counter: 0,
};
const reducer = reduceReducers<State>([
handleAction(increment, (state) => {
state.counter += 1;
}),
handleAction(decrement, (state) => {
state.counter -= 1;
}),
handleAction(add, (state, { payload }) => {
state.counter += payload;
}),
handleAction(override, (_, { payload }) => ({
counter: payload,
})),
], initialState);
// Store
const store: Store<State> = createStore(reducer);
store.subscribe(() => console.log('New state!', store.getState()));
// Go to town!
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(add(10));
console.log('Final count!', store.getState().counter); // 12
Everything you see above is 100% type safe! The action creators only take specified types and both the state and action payloads passed to the reducers are strongly typed. Most types are inferred so you don't need to think about it most of the time, but your build will still fail if you do something you shouldn't.
The reducers are automatically run with immer
, which will track any
"mutations" you make and return the optimally-immutably-updated state object.
You can run the above example by cloning this repository and running the following commands:
npm install
npm run example
There is also an example React app available on GitHub which you can also see running on CodeSandbox.
API
This package exports a grand total of four functions.
A lot of the generics for these functions can be inferred (see above example). The typings below provided are optimized for readability.
createAction<T, A extends any[] = [T?]>(type: string, payloadCreator?(args: A) => T)
The createAction
returns an action creator function (a function which returns
an action object). The first argument should be a string representing the type
of action being created, and the second argument is an optional payload creator
function. The action objects returned by these action creators have two
properties: type
(a string
) and payload
(typed as T
).
Typically it is best to use the simplest signature for this function:
const myActionCreator = createAction<MyActionPayload>('MY_ACTION');
The action creator function will be typed to take whatever you provide as a payload type.
If your action creator needs to take arguments other than whatever your payload is typed as you can simply provide a typed payload creator function:
// addThreeNumbers accepts three ordinal number aguments and has a number payload:
const addThreeNumbers = createAction('ADD_THREE_NUMBERS', (a: number, b: number, c: number) => a + b + c);
If you need to customize the [SFP] meta
property you can supply a second meta
creator function:
const addThreeNumbers = createAction<number, [number, number, number], string>(
'ADD_THREE_NUMBERS',
// Create `payload`
(a, b, c) => a + b + c,
// Create `meta`
(a, b, c) => `${a} + ${b} + ${c}`,
);
Note that the payload and meta creators must accept the same arguments, but can return different types. In the example above the payload creator takes three numbers and returns a number while the meta creator takes three numbers and returns a string.
handleAction(actionCreator, (state: Draft<State>, payload) => void, initialState?: State)
The handleAction
function returns a single reducer function. The first
argument should be an action creator from the createAction
function. The
second argument should be a "mutation" function which takes the current state
and the action. The third argument is an optional initial state argument.
When provided with an action with a type that matches the type from
actionCreator
the mutation function will be run. The mutation function is
automatically run with immer
which will track all modifications you make to
the incoming state object and return the optimally-immutably-updated new state
object. immer
will also provide you with a mapped type (Draft
) of your
state with all readonly
modifiers removed (it will also remove Readonly
mapped types and convert ReadonlyArray
s to standard arrays).
If your mutation function returns a value other than undefined
, and does not mutate the
incoming state object, that return value will become the new state instead.
reduceReducers<S>(reducers: Reducer[], initialState?: S)
The reduceReducers
function takes an array of reducer functions and an
optional initial state value and returns a single reducer which runs all of the
input reducers in sequence.
createAsyncActions<T, A extends any[], ...>(type: string, startPayloadCreator, successPayloadCreator, failPayloadCreator)
Oftentimes when working with sagas, thunks, or some other asynchronous,
side-effecting middleware you need to create three actions which are named
similarly. This is a convenience function which calls createAction
three
times for you. Consider the following example:
import { noop } from 'lodash';
import { createAsyncActions } from 'redux-ts-utils';
type User = { name: string };
export const [
requestUsers,
requestUsersSuccess,
requestUsersFailure,
] = createAsyncActions('REQUEST_USERS', noop, (users: User[]) => users);
requestUsers(); // returns action of type `REQUEST_USERS`
requestUsersSuccess([{ name: 'knpwrs' }]); // returns action of type `REQUEST_USERS/SUCCESS`
requestUsersError(); // returns action of type `REQUEST_USERS/ERROR`
The first argument is the action/triad name, and the second through third
(optional) arguments are payload creators for the initial action, the success
action, and the error action, respectively. noop
is imported from lodash in
order to be explicit that in this case the payload for requestUsers
is
void
. You can just as easily use () => {}
inline. The action creators infer
their payload types from the supplied payload creators. See the
implementation for complete type information.
Design Philosophy
A Strong Emphasis on Type Safety
Nothing should be stringly-typed. If you make a breaking change anywhere in your data layer the compiler should complain.
Simplicity
Whenever possible it is best to maintain strong safety; however, this can lead to extremely verbose code. For that reason this library strongly encourages type inference whenever possible.
This library exports four functions and a handful of types. Everything you need is provided by one package. The API surface is very small and easy to grok.
Not Too Opinionated
redux-ts-utils
provides TypeScript-friendly abstractions over the most
commonly-repeated pieces of boilerplate present in Redux projects. It is not a
complete framework abstracting all of Redux. It does not dictate or abstract
how you write your selectors, how you handle asynchronous actions or side
effects, how you create your store, or any other aspect of how you use Redux.
This makes redux-ts-utils
very non-opinionated compared to other Redux
utility libraries. The closest thing to an opinion you will find in this
library is that it ships with immer
. The reason for this is that immer
has proven to be the best method for dealing with immutable data structures in
a way which is both type-safe and performant. On top of that, immer
, by its
inclusion in redux-starter-kit
, has effectively been officially endorsed as
the de facto solution for managing immutable state changes. Shipping with
immer
helps to maintain the goal of simplicity by reducing
the necessary API surface for writing reducers and by ensuring type inference
whenever possible.
Setting up a redux store and middleware is typically a one-time task per project, so this library does not provide an abstraction for that. Likewise, thunks are simple but sagas are powerful, or maybe you like promises or observables. You should choose what works best for your project. Finally, given this library's strong emphasis on type safety it doesn't necessarily make sense to provide abstractions for creating selectors at the expense of type safety.
A Note on Flux Standard Actions
This library is compliant with Flux Standard Actions. That said, there is one important distinction with the way this library is typed that you should take note of of.
The FSA docs state that the payload
property is optional and may have a
value. This makes reducers a pain to write because TypeScript will enforce that
you always check for the existence of the payload property in order to use the
resulting actions. If you want to create an action that doesn't require a
payload, the simplest (and most type-explicit) thing to do is to type the
payload as void
:
const myAction = createAction<void>('MY_ACTION');
Even with this particular distinction, the actions created by this library are FSA-compliant.
License
MIT