An opinionated lib to create actions and reducers for Redux written in TypeScript and inspired from react-act project. The main goal is to use actions themselves as references inside the reducers rather than string constants.
# NPM
npm install @tsed/redux-utils --save
# Yarn
yarn add @tsed/redux-utils
Even if there is a function named createAction
, it actually creates an action creator
according to Redux glossary. It was just a bit overkill to name the function createActionCreator
. If you are not sure if something is an action or an action creator, just remember that actions are plain objects while action creators are functions.
TypeScript version:
// Import functions
import { createStore } from "redux";
import { createAction, createReducer } from "@tsed/redux-utils";
export type CounterState = number;
const createInitialState = (): CounterState => 0;
// Create an action creator (description is optional)
const add = createAction<CounterState>("add some stuff");
const increment = createAction<CounterState>("increment the state");
const decrement = createAction<CounterState>("decrement the state");
// Create a reducer
// (ES6 syntax, see Advanced usage below for an alternative for ES5)
const counterReducer = createReducer<CounterState>({}, 0) // <-- This is the default state
.on(increment, (state) => state + 1)
.on(decrement, (state) => state - 1)
.on(add, (state, payload) => state + payload);
// Create the store
const counterStore = createStore(counterReducer);
// Dispatch actions
counterStore.dispatch(increment()); // counterStore.getState() === 1
counterStore.dispatch(increment()); // counterStore.getState() === 2
counterStore.dispatch(decrement()); // counterStore.getState() === 1
counterStore.dispatch(add(5)); // counterStore.getState() === 6
Javascript version:
// Import functions
import { createStore } from "redux";
import { createAction, createReducer } from "@tsed/redux-utils";
const createInitialState = () => 0;
// Create an action creator (description is optional)
const add = createAction("add some stuff");
const increment = createAction("increment the state");
const decrement = createAction("decrement the state");
// Create a reducer
// (ES6 syntax, see Advanced usage below for an alternative for ES5)
const counterReducer = createReducer(
{
[increment]: (state) => state + 1,
[decrement]: (state) => state - 1,
[add]: (state, payload) => state + payload
},
createInitialState
); // <-- This is the default state
// Create the store
const counterStore = createStore(counterReducer);
// Dispatch actions
counterStore.dispatch(increment()); // counterStore.getState() === 1
counterStore.dispatch(increment()); // counterStore.getState() === 2
counterStore.dispatch(decrement()); // counterStore.getState() === 1
counterStore.dispatch(add(5)); // counterStore.getState() === 6
-
Does it work with Redux devtools? Yes.
-
Do reducers work with combineReducers? Of course, they are just normal reducers after all. Remember that according to the
combineReducers
checks, you will need to provide a default state when creating each reducer before combining them. -
How does it work? There is not much magic. A generated id is prepended to each action type and will be used inside reducers instead of the string constants used inside Redux by default.
-
Can you show how different it is from writing classic Redux? Sure, you can check both commits to update counter example and todomvc example. You can also run both examples with
npm install && npm start
inside each folder. -
Why having two syntax to create reducers? The one with only a map of
action => reduce function
doesn't allow much. This is why the other one is here, in case you would need a small state inside the reducer, having something similar as an actor, or whatever you feel like. Also, one of the syntax is ES6 only. -
Inside a reducer, why is it
(state, payload) => newState
rather than(state, action) => newState
? You can find more info about that on thecreateReducer
API below, but basically, that's because an action is composed of metadata handled by the lib and your payload. Since you only care about that part, better to have it directly. You can switch back to the full action if necessary of course. -
Why have you done that? Aren't string constants good enough? I know that the Redux doc states that such magic isn't really good, that saving a few lines of code isn't worth hiding such logic. I can understand that. And don't get me wrong, the main goal of this lib isn't to reduce boilerplate (even if I like that it does) but to use the actions themselves as keys for the reducers rather than strings which are error prone. You never know what the new dev on your project might do... Maybe (s)he will not realize that the new constant (s)he just introduced was already existing and now everything is broken and a wormhole will appear and it will be the end of mankind. Let's prevent that!
import { createStore } from "redux";
import { createAction, createReducer } from "@tsed/redux-utils";
// You can create several action creators at once
// (but that's probably not the best way to do it)
const [increment, decrement] = ["inc", "dec"].map(createAction);
// When creating action creators, the description is optional
// it will only be used for devtools and logging stuff.
// It's better to put something but feel free to leave it empty if you want to.
const replace = createAction();
// By default, the payload of the action is the first argument
// when you call the action. If you need to support several arguments,
// you can specify a function on how to merge all arguments into
// an unique payload.
let append = createAction("optional description", (...args) => args.join(""));
// There is another pattern to create reducers
// and it works fine with ES5! (maybe even ES3 \o/)
const stringReducer = createReducer(function (on) {
on(replace, (state, payload) => payload)(append, (state, payload) => (state += payload));
// Warning! If you use the same action twice,
// the second one will override the previous one.
}, "missing a lette"); // <-- Default state
// Now, when calling actions, they will be automatically dispatched
append("r"); // stringStore.getState() === 'missing a letter'
replace("a"); // stringStore.getState() === 'a'
append("b", "c", "d"); // stringStore.getState() === 'abcd'
Please read contributing guidelines here.
Thank you to all our backers! 🙏 [Become a backer]
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]
The MIT License (MIT)
Copyright (c) 2016 - 2021 Romain Lenzotti
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.