immer-reducer
Advanced tools
Comparing version 0.2.1 to 0.3.0
@@ -58,2 +58,3 @@ import { Draft } from "immer"; | ||
export declare function createReducerFunction<T extends ImmerReducerClass>(immerReducerClass: T, initialState?: ImmerReducerState<T>): ImmerReducerFunction<T>; | ||
export declare function setPrefix(prefix: string): void; | ||
/** | ||
@@ -60,0 +61,0 @@ * INTERNAL! This is only for tests! |
@@ -7,3 +7,3 @@ "use strict"; | ||
var immer_1 = __importDefault(require("immer")); | ||
var PREFIX = "IMMER_REDUCER"; | ||
var actionTypePrefix = "IMMER_REDUCER"; | ||
/** The actual ImmerReducer class */ | ||
@@ -73,3 +73,3 @@ var ImmerReducer = /** @class */ (function () { | ||
} | ||
var type = PREFIX + ":" + getReducerName(immerReducerClass) + "#" + key; | ||
var type = actionTypePrefix + ":" + getReducerName(immerReducerClass) + "#" + key; | ||
var actionCreator = function () { | ||
@@ -100,3 +100,3 @@ var args = []; | ||
} | ||
if (!action.type.startsWith(PREFIX + ":")) { | ||
if (!action.type.startsWith(actionTypePrefix + ":")) { | ||
return state; | ||
@@ -119,12 +119,9 @@ } | ||
}); | ||
// XXX Since Immer 1.10.3 the produce function returns immutable version | ||
// of the object passed to it which is not ok for reducers since | ||
// reducers should always return the same type. So we must cast the | ||
// return value to any to avoid type errors. There is no changes from | ||
// this to the end users because this function return value is | ||
// explicitly typed in the parent function signature. Also there's no | ||
// changes in the runtime behaviour. | ||
}; | ||
} | ||
exports.createReducerFunction = createReducerFunction; | ||
function setPrefix(prefix) { | ||
actionTypePrefix = prefix; | ||
} | ||
exports.setPrefix = setPrefix; | ||
/** | ||
@@ -131,0 +128,0 @@ * INTERNAL! This is only for tests! |
{ | ||
"name": "immer-reducer", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "", | ||
@@ -40,4 +40,4 @@ "main": "lib/immer-reducer.js", | ||
"dependencies": { | ||
"immer": "^1.10.4" | ||
"immer": "^1.10.5" | ||
} | ||
} |
193
README.md
# immer-reducer | ||
Create Redux reducers using [Immer](https://github.com/mweststrate/immer)! | ||
Create terse Type-Safe Redux reducers using [Immer](https://github.com/mweststrate/immer) and Typescript! | ||
Typescript [friendly](#100-type-safety-with-typescript) too. | ||
Read an introductory [blog post here](https://medium.com/@esamatti/type-safe-boilerplate-free-redux-906844ec6325). | ||
@@ -13,27 +11,55 @@ | ||
## Usage | ||
## Motivation | ||
Reducers are defined by extending from the `ImmerReducer` class | ||
Turn this 💩 | ||
```js | ||
import {ImmerReducer} from "immer-reducer"; | ||
```ts | ||
interface SetFirstNameAction { | ||
type: "SET_FIRST_NAME"; | ||
firstName: string; | ||
} | ||
// The class represents the classic switch-case reducer | ||
class MyImmerReducer extends ImmerReducer { | ||
interface SetLastNameAction { | ||
type: "SET_LAST_NAME"; | ||
lastName: string; | ||
} | ||
// each method becomes an Action Creator | ||
setFirstName(firstName) { | ||
// State updates are simple as assigning a value to | ||
// the draftState property thanks to Immer | ||
this.draftState.firstName = firstName; | ||
type Action = SetFirstNameAction | SetLastNameAction; | ||
function reducer(action: Action, state: State): State { | ||
switch (action.type) { | ||
case "SET_FIRST_NAME": | ||
return { | ||
...state, | ||
user: { | ||
...state.user, | ||
firstName: action.firstName, | ||
}, | ||
}; | ||
case "SET_LAST_NAME": | ||
return { | ||
...state, | ||
user: { | ||
...state.user, | ||
lastName: action.lastName, | ||
}, | ||
}; | ||
default: | ||
return state; | ||
} | ||
} | ||
``` | ||
setLastName(lastName) { | ||
this.draftState.lastName = lastName; | ||
into this! 🚀 | ||
```ts | ||
import {ImmerReducer} from "immer-reducer"; | ||
class MyImmerReducer extends ImmerReducer<State> { | ||
setFirstName(firstName: string) { | ||
this.draftState.user.firstName = firstName; | ||
} | ||
// You can combine methods to a single Action Creator | ||
setName(firstName, lastName) { | ||
this.setFirstName(firstName); | ||
this.setLastName(lastName); | ||
setLastName(lastName: string) { | ||
this.draftState.user.lastName = lastName; | ||
} | ||
@@ -43,5 +69,11 @@ } | ||
Generate Action Creators and the actual reducer function for Redux | ||
**Without losing the type-safety!** 🔥 | ||
```js | ||
Oh, and you get the action creators for free! 🤗 🎂 | ||
## Usage | ||
Generate Action Creators and the actual reducer function for Redux from the class with | ||
```ts | ||
import {createActionCreators, createReducerFunction} from "immer-reducer"; | ||
@@ -55,8 +87,10 @@ | ||
```js | ||
```ts | ||
import {createStore} from "redux"; | ||
const initialState = { | ||
firstName: "", | ||
lastName: "", | ||
user: { | ||
firstName: "", | ||
lastName: "", | ||
}, | ||
}; | ||
@@ -69,10 +103,46 @@ | ||
```js | ||
```ts | ||
store.dispatch(ActionCreators.setFirstName("Charlie")); | ||
store.dispatch(ActionCreators.setLastName("Brown")); | ||
expect(store.getState().firstName).toEqual("Charlie"); | ||
expect(store.getState().lastName).toEqual("Brown"); | ||
expect(store.getState().user.firstName).toEqual("Charlie"); | ||
expect(store.getState().user.lastName).toEqual("Brown"); | ||
``` | ||
## Typed Action Creators! | ||
This library by no means requires you to use Typescript but it was written | ||
specifically Typescript usage in mind because I was unable to find any other | ||
libraries that make Redux usage both terse and 100% type safe. | ||
The generated `ActionsTypes` object respect the types used in the class | ||
```ts | ||
const action = ActionCreators.setFirstName("Charlie"); // OK | ||
action.payload[0]; // OK string type | ||
action.payload[1]; // Type error. Only one argument. | ||
ActionCreators.setFirstName(1); // Type error. Needs string. | ||
ActionCreators.setWAT("Charlie"); // Type error. Unknown method | ||
``` | ||
The reducer function is also typed properly | ||
```ts | ||
const reducer = createReducerFunction(MyImmerReducer); | ||
const initialState: State = { | ||
user: { | ||
firstName: "", | ||
lastName: "", | ||
}, | ||
}; | ||
reducer(initialState, ActionCreators.setFirstName("Charlie")); // OK | ||
reducer(initialState, {type: "WAT"}); // Type error | ||
reducer({wat: "bad state"}, ActionCreators.setFirstName("Charlie")); // Type error | ||
``` | ||
## How | ||
Under the hood the class is deconstructed to following actions: | ||
@@ -101,3 +171,3 @@ | ||
# Integrating with the Redux ecosystem | ||
## Integrating with the Redux ecosystem | ||
@@ -122,3 +192,3 @@ To integrate for example with the side effects libraries such as | ||
// action.payload - recognized as string | ||
map(action => action.payload.toUpperCase()), | ||
map(action => action.payload[0].toUpperCase()), | ||
... | ||
@@ -128,58 +198,2 @@ ); | ||
# 100% Type Safety with Typescript | ||
This library by no means requires you to use Typescript but it was written | ||
specifically Typescript usage in mind because I was unable to find any other | ||
libraries that make Redux usage both boilerplate free and 100% type safe. | ||
Pretty advanced Typescript sorcery was required and so this library requires | ||
Typescript 3.1 or later. But the end results is really simple for the end user. | ||
The Typescript usage does not differ that much from the Javascript usage. | ||
Just pass your state type as the type argument for the class | ||
```ts | ||
interface State { | ||
// The state can be defined as read only | ||
readonly firstName: string; | ||
readonly lastName: string; | ||
} | ||
class MyImmerReducer extends ImmerReducer<State> { | ||
setFirstName(firstName: string) { | ||
// draftState has the State type but the readonly | ||
// flags are removed here to allow type safe mutation | ||
this.draftState.firstName = firstName; | ||
} | ||
setLastName(lastName: string) { | ||
this.draftState.lastName = lastName; | ||
} | ||
} | ||
``` | ||
The generated `ActionsTypes` object now respects the types used in the class | ||
```ts | ||
const ActionCreators = createActionCreators(MyImmerReducer); | ||
ActionCreators.setFirstName("Charlie"); // OK | ||
ActionCreators.setFirstName(1); // Type error | ||
ActionCreators.setWAT("Charlie"); // Type error | ||
``` | ||
The reducer function is also typed properly | ||
```ts | ||
const reducer = createReducerFunction(MyImmerReducer); | ||
const initialState: State = { | ||
firstName: "", | ||
lastName: "", | ||
}; | ||
reducer(initialState, ActionCreators.setFirstName("Charlie")); // OK | ||
reducer(initialState, {type: "WAT"}); // Type error | ||
reducer({wat: "bad state"}, ActionCreators.setFirstName("Charlie")); // Type error | ||
``` | ||
## Examples | ||
@@ -190,1 +204,8 @@ | ||
<https://github.com/epeli/typescript-redux-todoapp> | ||
## API | ||
- `createReducerFunction(klass: ImmerReducer: initialState?: Object)` | ||
- `createActionCreators(klass: ImmerReducer)` | ||
- `setPrefix(prefix: string)` | ||
- The default prefix in actions is is `IMMER_REDUCER`. Call this customize it for you app. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
204
18156
192
Updatedimmer@^1.10.5