- Notifications
You must be signed in to change notification settings - Fork16
Fluent syntax for defining typesafe reducers on top of typescript-fsa.
License
dphilipson/typescript-fsa-reducers
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Fluent syntax for defining typesafe Redux reducers on top oftypescript-fsa.
This library will allow you to write typesafe reducers that look like this:
constreducer=reducerWithInitialState(INITIAL_STATE).case(setName,setNameHandler).case(addBalance,addBalanceHandler).case(setIsFrozen,setIsFrozenHandler);
It removes the boilerplate normally associated with writing reducers, includingif-else chains, the default case, and the need to pull the payload field off ofthe action.
- Usage
- Installation
- API
- Starting a reducer chain
- Reducer chain methods
.case(actionCreator, handler(state, payload) => newState).caseWithAction(actionCreator, handler(state, action) => newState).cases(actionCreators, handler(state, payload) => newState).casesWithAction(actionCreators, handler(state, action) => newState).withHandling(updateBuilder(builder) => builder).default(handler(state, action) => newState).build()
This library allows you to define reducers by chaining a series of handlers fordifferent action types and optionally providing an initial value. It builds ontop of and assumes familiarity with the excellenttypescript-fsa.
Suppose we have usedtypescript-fsato define our state and some actions:
importactionCreatorFactoryfrom"typescript-fsa";constactionCreator=actionCreatorFactory();interfaceState{name:string;balance:number;isFrozen:boolean;}constINITIAL_STATE:State={name:"Untitled",balance:0,isFrozen:false,};constsetName=actionCreator<string>("SET_NAME");constaddBalance=actionCreator<number>("ADD_BALANCE");constsetIsFrozen=actionCreator<boolean>("SET_IS_FROZEN");
Using vanillatypescript-fsa, we might define a reducer as follows:
import{Action}from"redux";import{isType}from"typescript-fsa";functionreducer(state=INITIAL_STATE,action:Action):State{if(isType(action,setName)){return{ ...state,name:action.payload};}elseif(isType(action,addBalance)){return{ ...state,balance:state.balance+action.payload,};}elseif(isType(action,setIsFrozen)){return{ ...state,isFrozen:action.payload};}else{returnstate;}}
Using this library, the above is exactly equivalent to the following code:
import{reducerWithInitialState}from"typescript-fsa-reducers";constreducer=reducerWithInitialState(INITIAL_STATE).case(setName,(state,name)=>({ ...state, name})).case(addBalance,(state,amount)=>({ ...state,balance:state.balance+amount,})).case(setIsFrozen,(state,isFrozen)=>({ ...state, isFrozen}));
Note that unlike the vanilla case, there is no need to pull the payload off ofthe action, as it is passed directly to the handler, nor is it necessary tospecify a default case which returnsstate unmodified.
Everything is typesafe. If the types of the action payload and handler don'tline up, then TypeScript will complain. If you find it easier to read, you canof course pull out the handlers into separate functions, as shown in theIntroduction.
If the full action is needed rather than just the payload,.caseWithAction()may be used in place of.case(). This may be useful if you intend to pass theaction unchanged to a different reducer, or if you need to read themeta fieldof the action. For example:
import{Action}from"typescript-fsa";constsetText=actionCreator<string>("SET_TEXT");constreducer=reducerWithInitialState({text:"",lastEditBy:"",}).caseWithAction(incrementCount,(state,{ payload, meta})=>({text:payload,lastEditBy:meta.author,}));// Returns { text: "hello", lastEditBy: "cbrontë" }.reducer(undefined,setText("hello",{author:"cbrontë"}));
Further, a single handler may be assigned to multiple action types at once using.cases() or.casesWithAction():
constreducer=reducerWithInitialState(initialState).cases([setName,addBalance],(state,payload)=>{// Payload has type SetNamePayload | AddBalancePayload.// ...// Make sure to return the updated state, or TypeScript will give you a// rather unhelpful error message.returnstate;},);
The reducer builder chains are mutable. Each call to.case() modifies thecallee to respond to the specified action type. If this is undesirable, see the.build() method below.
For this library to be useful, you will also needtypescript-fsa to define youractions.
With Yarn:
yarn add typescript-fsa-reducers typescript-fsaOr with NPM:
npm install --save typescript-fsa-reducers typescript-fsaStarts a reducer builder-chain which uses the provided initial state if passedundefined as its state. For example usage, see theUsage sectionabove.
Starts a reducer builder-chain without special logic for an initial state.undefined will be treated like any other value for the state.
Redux seems to really want you to provide an initial state for your reducers.ItscreateStore API encourages it andcombineReducers function enforces it.For the Redux author's reasoning behind this, seethisthread. For this reason,reducerWithInitialState will likely be the more common choice, but the optionto not provide an initial state is there in case you have some means ofcomposing reducers for which initial state is unnecessary.
Note that since the type of the state cannot be inferred from the initial state,it must be provided as a type parameter:
constreducer=reducerWithoutInitialState<State>().case(setName,setNameHandler).case(addBalance,addBalanceHandler).case(setIsFrozen,setIsFrozenHandler);
Starts a builder-chain which produces a "reducer" whose return type is asupertype of the input state. This is most useful for handling a state which maybe in one of several "modes", each of which responds differently to actions andcan transition to the other modes. Many applications will not have a use forthis.
Note that the function produced is technically not a reducer because the initialand updated states are different types.
Example usage:
typeState=StoppedState|RunningState;interfaceStoppedState{ type:"STOPPED";}interfaceStartedState{ type:"STARTED"; count:number;}constINITIAL_STATE:State={type:"STOPPED"};conststartWithCount=actionCreator<number>("START_WITH_COUNT");constaddToCount=actionCreator<number>("ADD_TO_COUNT");conststop=actionCreator<void>("STOP");functionstartWithCountHandler(state:StoppedState,count:number):State{return{type:"STARTED", count};}functionaddToCountHandler(state:StartedState,count:number):State{return{ ...state,count:state.count+count};}functionstopHandler(state:StartedState):State{return{type:"STOPPED"};}conststoppedReducer=upcastingReducer<StoppedState,State>().case(startWithCount,startWithCountHandler);conststartedReducer=upcastingReducer<StartedState,State>().case(addToCount,addToCountHandler).case(stop,stopHandler);functionreducer(state=INITIAL_STATE,action:Redux.Action):State{if(state.type==="STOPPED"){returnstoppedReducer(state,action);}elseif(state.type==="STARTED"){returnstartedReducer(state,action);}else{thrownewError("Unknown state");}}
Mutates the reducer such that it applieshandler when passed actions matchingthe type ofactionCreator. For examples, seeUsage.
Like.case(), except thathandler receives the entire action as its secondargument rather than just the payload. This is useful if you want to read otherproperties of the action, such asmeta orerror, or if you want to pass theentire action unmodified to some other function. For an example, seeUsage.
Like.case(), except that multiple action creators may be provided and thesame handler is applied to all of them. That is,
reducerWithInitialState(initialState).cases([setName,addBalance,setIsFrozen],handler,);
is equivalent to
reducerWithInitialState(initialState).case(setName,handler).case(addBalance,handler).case(setIsFrozen,handler);
Note that the payload passed to the handler may be of the type of any of thelisted action types' payloads. In TypeScript terms, this means it has typeP1 | P2 | ..., whereP1, P2, ... are the payload types of the listed actioncreators.
The payload type is inferred automatically for up to four action types. Afterthat, it must be supplied as a type annotation, for example:
reducerWithInitialState(initialState).cases<{documentId:number}>([selectDocument,editDocument,deleteDocument,sendDocument,archiveDocument,],handler);
Like.cases(), except that the handler receives the entire action as itssecond argument rather than just the payload.
Convenience method which applies the provided function to the current builderand returns the result. Useful if you have a sequence of builder updates (callsto.case(), etc.) which you want to reuse across several reducers.
Produces a reducer which applieshandler when no previously added.case(),.caseWithAction(), etc. matched. The handler is similar to the one in.caseWithAction(). Note that.default() ends the chain and internally doesthe same as.build(), because it is not intended that the chain bemutated after calling.default().
This is useful if you have a "delegate" reducer that should be called on anyaction after handling a few specific actions in the parent.
constNESTED_STATE={someProp:"hello",};constnestedReducer=reducerWithInitialState(NESTED_STATE).case(...);constINITIAL_STATE={someOtherProp:"world"nested:NESTED_STATE};constreducer=reducerWithInitialState(INITIAL_STATE).case(...).default((state,action)=>({ ...state,nested:nestedReducer(state.nested,action),}));
Returns a plain reducer function whose behavior matches the current state of thereducer chain. Further updates to the chain (through calls to.case()) willhave no effect on this function.
There are two reasons you may want to do this:
You want to ensure that the reducer is not modified further
Calling
.build()is an example of defensive coding. It prevents someonefrom causing confusing behavior by importing your reducer in an unrelatedfile and adding cases to it.You want your package to export a reducer, but not have its types dependon
typescript-fsa-reducersIf the code that defines a reducer and the code that uses it reside inseparate NPM packages, you may run into type errors since the exportedreducer has type
ReducerBuilder, which the consuming package does notrecognize unless it also depends ontypescript-fsa-reducers. This isavoided by returning a plain function instead.
Example usage:
constreducer=reducerWithInitialState(INITIAL_STATE).case(setName,setNameHandler).case(addBalance,addBalanceHandler).case(setIsFrozen,setIsFrozenHandler).build();
Copyright © 2017 David Philipson
About
Fluent syntax for defining typesafe reducers on top of typescript-fsa.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors5
Uh oh!
There was an error while loading.Please reload this page.