Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
immer-reducer
Advanced tools
Create terse type-safe Redux and `useReducer` Hook reducers using [Immer](https://github.com/mweststrate/immer) and Typescript!
Create terse type-safe Redux and useReducer
Hook reducers using Immer and Typescript!
Read an introductory blog post here.
npm install immer-reducer
Turn this 💩 💩 💩
interface SetFirstNameAction {
type: "SET_FIRST_NAME";
firstName: string;
}
interface SetLastNameAction {
type: "SET_LAST_NAME";
lastName: string;
}
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;
}
}
✨✨ Into this! ✨✨
import {ImmerReducer} from "immer-reducer";
class MyImmerReducer extends ImmerReducer<State> {
setFirstName(firstName: string) {
this.draftState.user.firstName = firstName;
}
setLastName(lastName: string) {
this.draftState.user.lastName = lastName;
}
}
🔥🔥 Without losing type-safety! 🔥🔥
Oh, and you get the action creators for free! 🤗 🎂
Generate Action Creators and the actual reducer function for Redux from the class with
import {createStore} from "redux";
import {createActionCreators, createReducerFunction} from "immer-reducer";
const initialState: State = {
user: {
firstName: "",
lastName: "",
},
};
const ActionCreators = createActionCreators(MyImmerReducer);
const reducerFunction = createReducerFunction(MyImmerReducer, initialState);
const store = createStore(reducerFunction);
Dispatch some actions
store.dispatch(ActionCreators.setFirstName("Charlie"));
store.dispatch(ActionCreators.setLastName("Brown"));
expect(store.getState().user.firstName).toEqual("Charlie");
expect(store.getState().user.lastName).toEqual("Brown");
The generated ActionCreator
object respect the types used in the class
const action = ActionCreators.setFirstName("Charlie");
action.payload; // Has the type of string
ActionCreators.setFirstName(1); // Type error. Needs string.
ActionCreators.setWAT("Charlie"); // Type error. Unknown method
If the reducer class where to have a method which takes more than one argument the payload would be array of the arguments
// In the Reducer class:
// setName(firstName: string, lastName: string) {}
const action = ActionCreators.setName("Charlie", "Brown");
action.payload; // will have value ["Charlie", "Brown"] and type [string, string]
The reducer function is also typed properly
const reducer = createReducerFunction(MyImmerReducer);
reducer(initialState, ActionCreators.setFirstName("Charlie")); // OK
reducer(initialState, {type: "WAT"}); // Type error
reducer({wat: "bad state"}, ActionCreators.setFirstName("Charlie")); // Type error
Because the useReducer()
API in React Hooks is the same as with Redux
Reducers immer-reducer can be used with as is.
const initialState = {message: ""};
class ReducerClass extends ImmerReducer<typeof initialState> {
setMessage(message: string) {
this.draftState.message = message;
}
}
const ActionCreators = createActionCreators(ReducerClass);
const reducerFuntion = createReducerFunction(ReducerClass);
function Hello() {
const [state, dispatch] = React.useReducer(reducerFuntion, initialState);
return (
<button
data-testid="button"
onClick={() => {
dispatch(ActionCreators.setMessage("Hello!"));
}}
>
{state.message}
</button>
);
}
The returned state and dispatch functions will be typed as you would expect.
Under the hood the class is deconstructed to following actions:
{
type: "IMMER_REDUCER:MyImmerReducer#setFirstName",
payload: "Charlie",
}
{
type: "IMMER_REDUCER:MyImmerReducer#setLastName",
payload: "Brown",
}
{
type: "IMMER_REDUCER:MyImmerReducer#setName",
payload: ["Charlie", "Brown"],
args: true
}
So the class and method names become the Redux Action Types and the method arguments become the action payloads. The reducer function will then match these actions against the class and calls the appropriate methods with the payload array spread to the arguments.
🚫 The format of the action.type
string is internal to immer-reducer. If
you need to detect the actions use the provided type guards.
The generated reducer function executes the methods inside the produce()
function of Immer enabling the terse mutatable style updates.
To integrate for example with the side effects libraries such as
redux-observable and
redux-saga, you can access the
generated action type using the type
property of the action creator
function.
With redux-observable
// Get the action name to subscribe to
const setFirstNameActionTypeName = ActionCreators.setFirstName.type;
// Get the action type to have a type safe Epic
type SetFirstNameAction = ReturnType<typeof ActionCreators.setFirstName>;
const setFirstNameEpic: Epic<SetFirstNameAction> = action$ =>
action$
.ofType(setFirstNameActionTypeName)
.pipe(
// action.payload - recognized as string
map(action => action.payload.toUpperCase()),
...
);
With redux-saga
function* watchFirstNameChanges() {
yield takeEvery(ActionCreators.setFirstName.type, doStuff);
}
// or use the isActionFrom() to get all actions from a specific ImmerReducer
// action creators object
function* watchImmerActions() {
yield takeEvery(
(action: Action) => isActionFrom(action, MyImmerReducer),
handleImmerReducerAction,
);
}
function* handleImmerReducerAction(action: Actions<typeof MyImmerReducer>) {
// `action` is a union of action types
if (isAction(action, ActionCreators.setFirstName)) {
// with action of setFirstName
}
}
Warning: Due to how immer-reducers action generation works, adding default parameters to the methods will NOT pass it to the action payload, which can make your reducer impure and the values will not be available in middlewares.
class MyImmerReducer extends ImmerReducer<State> {
addItem (id: string = uuid()) {
this.draftState.ids.push([id])
}
}
immerActions.addItem() // generates empty payload { payload: [] }
As a workaround, create custom action creator wrappers that pass the default parameters instead.
class MyImmerReducer extends ImmerReducer<State> {
addItem (id) {
this.draftState.ids.push([id])
}
}
const actions = {
addItem: () => immerActions.addItem(id)
}
Here's a more complete example with redux-saga and redux-render-prop:
https://github.com/epeli/typescript-redux-todoapp
The module exports following helpers
function isActionFrom(action, ReducerClass)
Type guard for detecting whether the given action is generated by the given reducer class. The detected type will be union of actions the class generates.
Example
if (isActionFrom(someAction, ActionCreators)) {
// someAction now has type of
// {
// type: "setFirstName";
// payload: string;
// } | {
// type: "setLastName";
// payload: string;
// };
}
function isAction(action, actionCreator)
Type guard for detecting specific actions generated by immer-reducer.
Example
if (isAction(someAction, ActionCreators.setFirstName)) {
someAction.payload; // Type checks to `string`
}
type Actions<ImmerReducerClass>
Get union of the action types generated by the ImmerReducer class
Example
type MyActions = Actions<typeof MyImmerReducer>;
// Is the same as
type MyActions =
| {
type: "setFirstName";
payload: string;
}
| {
type: "setLastName";
payload: string;
};
function setPrefix(prefix: string)
The default prefix in the generated action types is IMMER_REDUCER
. Call
this customize it for your app.
Example
setPrefix("MY_APP");
function composeReducers<State>(...reducers)
Utility that reduces actions by applying them through multiple reducers.
This helps in allowing you to split up your reducer logic to multiple ImmerReducer
s
if they affect the same part of your state
Example
class MyNameReducer extends ImmerReducer<NamesState> {
setFirstName(firstName: string) {
this.draftState.firstName = firstName;
}
setLastName(lastName: string) {
this.draftState.lastName = lastName;
}
}
class MyAgeReducer extends ImmerReducer<AgeState> {
setAge(age: number) {
this.draftState.age = 8;
}
}
export const reducer = composeReducers(
createReducerFunction(MyNameReducer, initialState),
createReducerFunction(MyAgeReducer, initialState)
)
FAQs
Type-safe and terse reducers with Typescript for React Hooks and Redux using [Immer](https://immerjs.github.io/immer/)!
The npm package immer-reducer receives a total of 2,767 weekly downloads. As such, immer-reducer popularity was classified as popular.
We found that immer-reducer demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.