Everything you need to create type-safe applications with Redux! Flux Standard Action compliant.
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');
// 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;
}),
], 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.
This package exports a grand total of three functions.
A lot of the generics for these functions can be inferred (see above example). The typings below provided are optimized for readability.
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 provide types in the generic signature:
// a, b, and c are inferred below:
const addThreeNumbers = createAction<number, [number, number, number]>('ADD_THREE_NUMBERS', (a, b, c) => a + b + c);
If you need to customize the [SFP] meta
property you can supply a second meta
customizer function:
const addThreeNumbers = createAction<number, [number, number, number], number>(
'ADD_THREE_NUMBERS',
// Create `payload`
(a, b, c) => a + b + c,
// Create `meta`
(a, b, c) => `${a} + ${b} + ${c}`,
);
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).
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.
Nothing should be stringly-typed. If you make a breaking change anywhere in your data layer the compiler should complain.
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 three functions and a handful of types. Everything you need is provided by one package. The API surface is very small and easy to grok.
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.
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.
MIT