1.0.0 • Published 3 years ago
use-dispatch-action v1.0.0
use-dispatch-action
Typed utilities for improving the experience with useReducer
.
Problem
When using useReducer
- Dispatching actions are not type safe
- Action creators, while testable, introduce additional boilerplate code
- Changing an action type can lead to a reducer to no longer work properly, if you don't have tests
Solution
use-dispatch-action
is a collection of utilities to improve the experience when using the useReducer
hook.
Getting started
Install
yarn add use-dispatch-action
Or
npm install use-dispatch-action
Usage
import * as React from 'react';
import { useDispatchAction } from 'use-dispatch-action';
type Actions =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'addValue'; payload: number };
type State = { counter: number };
const reducer = (state: State, action: Actions): State => {
switch (action.type) {
case 'increment':
return { ...state, counter: state.counter + 1 };
case 'decrement':
return { ...state, counter: state.counter - 1 };
case 'addValue':
return { ...state, counter: state.counter + action.payload };
default:
return state;
}
};
const Component = () => {
const [state, dispatch] = React.useReducer(reducer, { counter: 0 });
const increment = useDispatchAction(dispatch, 'increment');
const decrement = useDispatchAction(dispatch, 'decrement');
const addValue = useDispatchAction(dispatch, 'addValue');
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={() => addValue(2)}>Add Two</button>
</div>
);
};
API
type Action
A utilty type for defining actions
type Action<TActionType extends string, TPayload = never> = {
type: TActionType;
payload?: TPayload;
};
Example
type Actions = Action<'incrementOne'> | Action<'increment', number>;
useDispatchAction
Creates type safe dispatch functions for the specified action
useDispatchAction<TAction>(
dispatch: React.Dispatch<TAction>,
action: string
) : DispatchAction<TActionPayload>
Arguments
dispatch: React.Dispatch<TAction>
- A dispatch method retured fromReact.useReducer
action: string
- The type of the action
Returns
DispatchAction<TActionPayload>
- Function to dispatch action
// For actions without a payload
() => void;
// For actions with a payload
(payload: TPayload) => void;
Example (types/reducer)
const Component = () => {
const [state, dispatch] = React.useReducer(reducer, { counter: 0 });
const increment = useDispatchAction(dispatch, 'increment');
const decrement = useDispatchAction(dispatch, 'decrement');
const addValue = useDispatchAction(dispatch, 'addValue');
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={() => increment()}>Increment</button>
<button onClick={() => decrement()}>Decrement</button>
<button onClick={() => addValue(2)}>Add Two</button>
</div>
);
};
useDispatchReducer
Creates a reducer with a type safe dispatch method
useDispatchReducer<TState, TAction> (
reducer: React.Reducer<TState, TAction>,
initialState: TState
) : [state: TState, ActionDispatcher<TAction>]
TState
and TAction
can be infered by providing the type of a reducer.
useDispatchReducer<TReducer>(
reducer: TReducer,
initialState: TState
) : [state: TState, ActionDispatcher<TAction>]
Arguments
reducer: React.Reducer<TState, TAction>
- The reducerinitialState: TState
- State to initialize the reducer with. A note,useDispatchReducer
does not implement lazy loading the state
Returns
A tuple with:
state: TState
- State of the reducerActionDispatcher<TAction>
- Function to dispatch actions in the form of tuples// For actions without a payload ([type: string]) => void; // For actions with a payload ([type: string, payload: TPayload]) => void;
Examples (types/reducer)
With type inference
const Component = () => {
const [state, dispatch] = useDispatchReducer(reducer, { counter: 0 });
const increment = () => dispatch(['increment']);
const decrement = () => dispatch(['decrement']);
const addValue = (number: number) => dispatch(['addValue', number]);
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={() => addValue(2)}>Add Two</button>
</div>
);
};
With known State and Action types
const Component = () => {
const [state, dispatch] = useDispatchReducer<State, Action>(reducer, {
counter: 0,
});
const increment = () => dispatch(['increment']);
const decrement = () => dispatch(['decrement']);
const addValue = (number: number) => dispatch(['addValue', number]);
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={() => addValue(2)}>Add Two</button>
</div>
);
};
Only know the State type? The Action type can be inferred from the reducer as long as the actions are typed.
const Component = () => {
const [state, dispatch] = useDispatchReducer<State>(reducer, { counter: 0 });
const increment = () => dispatch(['increment']);
const decrement = () => dispatch(['decrement']);
const addValue = (number: number) => dispatch(['addValue', number]);
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={() => addValue(2)}>Add Two</button>
</div>
);
};
DispatchContext
A context based dispatcher used to prevent prop drilling
export type DispatchContextProps = {
initialState: TState;
reducer: React.Reducer<TState, TAction>;
};
props
initialState: TState
- state to initialize the reducer withreducer: React.Reducer<TState, TAction>
- The reducer
Examples (types/reducer)
Using a consumer
const DispatchContext = () => {
return (
<DispatchContextProvider reducer={reducer} initialState={{ counter: 0 }}>
<DispatchContextConsumer>
{({ state, dispatch }: DispatchProps<typeof reducer>) => (
<div>
<div title="counter">{state.counter}</div>
<button onClick={() => dispatch(['increment'])}>Increment</button>
<button onClick={() => dispatch(['decrement'])}>Decrement</button>
<button onClick={() => dispatch(['addValue', 2])}>Add Two</button>
</div>
)}
</DispatchContextConsumer>
</DispatchContextProvider>
);
};
Using useDispatchContext
const Component = () => {
return (
<DispatchContextProvider initialState={{ counter: 0 }} reducer={reducer}>
<Counter />
</DispatchContextProvider>
);
};
const Counter = () => {
const [state, dispatch] = useDispatchContext<typeof reducer>();
return (
<div>
<div title="counter">{state.counter}</div>
<button onClick={() => dispatch(['increment'])}>Increment</button>
<button onClick={() => dispatch(['decrement'])}>Decrement</button>
<button onClick={() => dispatch(['addValue', 2])}>Add Two</button>
</div>
);
};
Types and reducer for examples
type Actions =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'addValue'; payload: number };
type State = { counter: number };
const reducer = (state: State, action: Actions): State => {
switch (action.type) {
case 'increment':
return { ...state, counter: state.counter + 1 };
case 'decrement':
return { ...state, counter: state.counter - 1 };
case 'addValue':
return { ...state, counter: state.counter + action.payload };
default:
return state;
}
};