Type-safe, composable, boilerplateless reducers

npm install --save @betafcc/red
importReact,{useReducer}from'react'import{render}from'react-dom'import{red}from'@betafcc/red'constinputApp=red.withState({input:''}).handle({setInput:(state,input:string)=>({ input})})consttodoApp=inputApp.withState({todos:[]asArray<{msg:string;done:boolean}>}).handle({add:(state,msg:string)=>({input:'',todos:[...s.todos,{ msg,done:false}]}),complete:(state,id:number)=>({todos:state.todos.map((e,i)=>i!==id ?e :{ ...e,done:true})}),})exportconstTodoApp=()=>{const[{ input, todos},{ setInput, add, complete}]=todoApp.useHook(useReducer)return<><inputvalue={input}onChange={e=>setInput(e.target.value)}/><buttononClick={_=>add(input)}>add</button>{todos.map((e,id)=><likey={id}style={e.done ?{textDecoration:'line-through'} :{}}>{e.msg}<buttononClick={_=>complete(id)}>done</button></li>)}</>}render(<TodoApp/>,document.getElementById('root'))If every action has this shape:
typeActionType<Kextendsstring,PextendsArray<unknown>>={type:Kpayload:P}We can automatically provide action creators and strong-typed reducer from simple handlers definitions:
constapp=red.withState({input:'',todo:[]asArray<{msg:string,id:number,done:boolean}>}).handle({setInput(state,input:string){return{ input}},addTodo(state,msg:string,id:number){return{todo:[...state.todo,{msg, id,done:false}]}},completeTodo(state,id:number){return{todo:todo.map(t=>t.id!==id ?t :{...t,done:true})}}})const{ intial,// the initial state reducer,// the reducer made by combining the handlers actions,// the action creators}=redThe arguments in the handlers define the payload, and their keys define the 'type', the revealed signature is:
import{StateOf,ActionOf}from'@betafcc/red'typeState=StateOf<typeofred>// { input: string, todo: Array<{msg: string, id: number, done: boolean}>}typeAction=ActionOf<typeofred>// { type: 'setInput', payload: [string] } | { type: 'addTodo', payload: [string, number] } | { type: 'completeTodo', payload: [number] }And you also have action creators that matches the signature:
const{ addTodo, completeTodo}=app.actionsaddTodo('buy milk',0)// { type: 'addTodo', payload: ['buy milk', 0] }completeTodo(0)// { type: 'completeTodo', payload: [number] }If your prefer to define the actions yourself, you can useActionType helper andwithActions method:
import{ActionType}from'@betafcc/red'typeAction=|ActionType<'setInput',[string]>|ActionType<'addTodo',[string,number]>|ActionType<'completeTodo',[number]>constapp=red.withState({input:'',todo:[]asArray<{msg:string,id:number,done:boolean}>}).withActions<Action>({// annotate heresetInput(state,input){// so arguments will have infered type, no need to annotate herereturn{input}},addTodo(state,msg,id){return{todo:[...state.todo,{msg, id,done:false}]}},completeTodo(state,id){return{todo:todo.map(t=>t.id!==id ?t :{...t,done:true})}}})The simplest way to use is to extract the generatedreducer,initial and theactions creators:
const{reducer, initial, actions}=appconstApp=()=>{const[state,dispatch]=React.useReducer(reducer,initial)return<>{state.count}<buttononClick={_=>dispatch(actions.increment())}>+</button></>}But you can usered.useHook for some extra magic:
constApp=()=>{// the action creators will become dispatchersconst[state,{increment}]=app.useHook(React.useReducer)return<>{state.count}<buttononClick={_=>increment()}>+</button></>}You can usered.merge to combine apps together:
constinputApp=red.withState({input:''}).handle({setInput:(s,value:string)=>({input:value})})consttodoApp=red.withState({todos:[]asArray<{id:number,done:boolean,msg:string}>}).handle({add:(s,msg:string,id:number)=>({todos:[...s.todos,{id, msg,done:false}]})})constapp=red.merge(inputApp).merge(todoApp)// same asconstapp=inputApp.merge(todoApp)// same asconstapp=red.withState({input:''}).handle({setInput:(s,value:string)=>({input:value})}).withState({todos:[]asArray<{id:number,done:boolean,msg:string}>}).handle({add:(s,msg:string,id:number)=>({todos:[...s.todos,{id, msg,done:false}]})})Or you can combine them by namespacing withred.combine, similar to redux'scombineReducers:
constinputApp=red.withState({input:''}).handle({setInput:(s,input:string)=>({input})})constcounterApp=red.withState({count:0}).handle({increment:s=>({count:s.count+1})})constapp=red.combine({ui:inputApp,counter:counterApp})// equivalent to:constapp=red.withState({ui:{input:''},counter:{count:0}}).handle({// note that you have to manually namespace the statesetInput:(s,input:string)=>({...s,ui:{input}}),increment:s=>({...s,counter:{count:s.counter.count+1}})})