typestately
v5.1.1
Published
Recomposed approach of using redux with TypeScript
Downloads
34
Readme
typestately
Recomposed approach of using redux with TypeScript. An idea showing how you can deal with state management using redux.
Some goals
- Reduce needed type annotation by making use of type inference
- Reduce boilerplate code
- Encapsulate state details/concerns (e.g. key used for a reducer in the stores state object) in one place
- Easy way to plug new parts of global state in and out
- Support code-splitting
- Support multiple stores
Examples/HowTo
A complete example can be found here: https://github.com/hmuralt/typestately-example
It shows the usage with state handlers implemented as classes and an alternative usage with plain objects & functions.
Store
This is an example of how stores could be setup and registered.
StoreContexts.ts
import { setupMainStoreContext } from "./StoreContextSetups";
const storeContexts = {
Main: setupMainStoreContext()
};
export default storeContexts;StoreContextSetups.ts
import { createStore } from "redux";
import { createStoreContext } from "typestately";
export function setupMainStoreContext() {
const store = createStore((state) => state || {});
return createStoreContext(store, {});
}Counter example with state handler classes
State
CounterState.ts
export default interface State {
value: number;
clicked: Date;
}
export const defaultState: State = {
value: 0,
clicked: new Date()
};Actions
CounterActions.ts
export enum ActionType {
Increment = "INCREMENT",
Decrement = "DECREMENT"
}
export interface ChangeAction extends Action<ActionType> {
type: ActionType;
clicked: Date;
}State handler
CounterStateHandler.ts
class CounterStateHandler extends StateHandler<State, ActionType> {
@StateHandler.nested
public readonly loaderStateHandler: LoaderStateHandler;
constructor(loaderStateHandler: LoaderStateHandler) {
super("counter", defaultState);
this.loaderStateHandler = loaderStateHandler;
}
public increment(clicked: Date) {
this.dispatch<ChangeAction>({
type: ActionType.Increment,
clicked
});
}
public decrement(clicked: Date) {
this.dispatch<ChangeAction>({
type: ActionType.Decrement,
clicked
});
}
public incrementAsync(clicked: Date) {
this.loaderStateHandler.setStatus(Status.Updating);
window.setTimeout(() => {
this.increment(clicked);
this.loaderStateHandler.setStatus(Status.Done);
}, 2000);
}
@StateHandler.reducer<State, ActionType>(ActionType.Increment)
protected reduceIncrement(state: State, action: ChangeAction) {
return {
value: state.value + 1,
clicked: action.clicked
};
}
@StateHandler.reducer<State, ActionType>(ActionType.Decrement)
protected reduceDecrement(state: State, action: ChangeAction) {
return {
value: state.value - 1,
clicked: action.clicked
};
}
}
// Ideally managed by IOC container...
const counterStateHandler = new CounterStateHandler(new LoaderStateHandler());
export default counterStateHandler;Component
Counter.tsx
export interface Props {
value: number;
clicked: Date;
onIncrement: (clicked: Date) => void;
onIncrementAsync: (clicked: Date) => void;
onDecrement: (clicked: Date) => void;
}
export default class Counter extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.increment = this.increment.bind(this);
this.incrementAsync = this.incrementAsync.bind(this);
this.decrement = this.decrement.bind(this);
}
public render() {
return (
<div>
<p>
Value: {this.props.value} (clicked: {this.props.clicked.toLocaleString()})
</p>
<p>
<button onClick={this.decrement}>-</button>
<button onClick={this.increment}>+</button>
</p>
<p>
<button onClick={this.incrementAsync}>+ (async)</button>
</p>
</div>
);
}
private increment() {
this.props.onIncrement(new Date());
}
private incrementAsync() {
this.props.onIncrementAsync(new Date());
}
private decrement() {
this.props.onDecrement(new Date());
}
}Container
Counter.ts
counterStateHandler.attachTo(storeContexts.Main.hub);
export default withStateToProps(
counterStateHandler,
(counterState): Props => {
return {
value: counterState.value,
clicked: counterState.clicked,
onIncrement: (clicked: Date) => counterStateHandler.increment(clicked),
onIncrementAsync: (clicked: Date) => counterStateHandler.incrementAsync(clicked),
onDecrement: (clicked: Date) => counterStateHandler.decrement(clicked)
};
}
)(Counter);Counter example with state handler functions (alternative to classes)
State
export default interface CounterState {
value: number;
clicked: Date;
}
export const defaultCounterState: CounterState = {
value: 0,
clicked: new Date()
};Actions
CounterActions.ts
export enum ActionType {
Increment = "INCREMENT",
Decrement = "DECREMENT"
}
export interface ChangeAction extends Action<ActionType> {
type: ActionType;
clicked: Date;
}Reducer
function increment(state: CounterState, action: ChangeAction) {
return {
value: state.value + 1,
clicked: action.clicked
};
}
function decrement(state: CounterState, action: ChangeAction) {
return {
value: state.value - 1,
clicked: action.clicked
};
}
const counterReducer = createExtensibleReducer<CounterState, ActionType>()
.handling(ActionType.Increment, increment)
.handling(ActionType.Decrement, decrement);
export default counterReducer;State handler
const counterStateDefinition = defineState(defaultCounterState)
.makeStorableUsingKey("counter")
.setReducer(() => counterReducer)
.setActionDispatchers({
increment(dispatch: Dispatch<ActionType>, clicked: Date) {
dispatch<ChangeAction>({
type: ActionType.Increment,
clicked
});
},
decrement(dispatch: Dispatch<ActionType>, clicked: Date) {
dispatch<ChangeAction>({
type: ActionType.Decrement,
clicked
});
}
});
export function createCounterStateHandler(hub: Hub) {
const counterStateHandler = counterStateDefinition.createStateHandler(hub);
const loaderStateHandler = createLoaderStateHandler(hub, counterStateHandler.contextId);
const extensions = {
incrementAsync(clicked: Date) {
loaderStateHandler.setStatus(Status.Updating);
window.setTimeout(() => {
counterStateHandler.increment(clicked);
loaderStateHandler.setStatus(Status.Done);
}, 2000);
}
};
return Object.assign(counterStateHandler, extensions, {
loaderStateProvider: withStateProvider(loaderStateHandler)({})
});
}Container
const CounterContainer: React.FC = () => {
const counterStateHandler = React.useMemo(() => createCounterStateHandler(storeContexts.FunctionsExample.hub), []);
const counterState = useStateProvider(counterStateHandler);
return (
<Counter
value={counterState.value}
clicked={counterState.clicked}
onIncrement={counterStateHandler.increment}
onIncrementAsync={counterStateHandler.incrementAsync}
onDecrement={counterStateHandler.decrement}
/>
);
};State without using redux
You can also create a standalone state handler which isn't attached to the redux store and has it's own standalone state.
interface CounterState {
value: number;
clicked: Date;
}
const defaultCounterState: CounterState = {
value: 0,
clicked: new Date()
};
function increment(state: CounterState, clicked: Date) {
return {
value: state.value + 1,
clicked
};
}
function decrement(state: CounterState, clicked: Date) {
return {
value: state.value - 1,
clicked
};
}
const counterStateDefinition = defineState(defaultCounterState, { increment, decrement });
const counterStateHandler = counterStateDefinition.createStandaloneStateHandler();
counterStateHandler.increment(new Date());And, you can extend the existing an state definition with Redux if needed.
counterStateDefinition
.makeStorableUsingKey("counter")
.setReducer((stateOperations) => ...) // stateOperations = { increment, decrement } object from defineState call.
...