Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fluent syntax for defining typesafe reducers on top of typescript-fsa.

License

NotificationsYou must be signed in to change notification settings

dphilipson/typescript-fsa-reducers

Repository files navigation

Fluent syntax for defining typesafe Redux reducers on top oftypescript-fsa.

Build Status

Introduction

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.

Table of Contents

Usage

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.

Installation

For this library to be useful, you will also needtypescript-fsa to define youractions.

With Yarn:

yarn add typescript-fsa-reducers typescript-fsa

Or with NPM:

npm install --save typescript-fsa-reducers typescript-fsa

API

Starting a reducer chain

reducerWithInitialState(initialState)

Starts a reducer builder-chain which uses the provided initial state if passedundefined as its state. For example usage, see theUsage sectionabove.

reducerWithoutInitialState()

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);

upcastingReducer()

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");}}

Reducer chain methods

.case(actionCreator, handler(state, payload) => newState)

Mutates the reducer such that it applieshandler when passed actions matchingthe type ofactionCreator. For examples, seeUsage.

.caseWithAction(actionCreator, handler(state, action) => newState)

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.

.cases(actionCreators, handler(state, payload) => newState)

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);

.casesWithAction(actionCreators, handler(state, action) => newState)

Like.cases(), except that the handler receives the entire action as itssecond argument rather than just the payload.

.withHandling(updateBuilder(builder) => builder)

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.

.default(handler(state, action) => newState)

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),}));

.build()

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:

  1. 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.

  2. You want your package to export a reducer, but not have its types dependontypescript-fsa-reducers

    If 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 typeReducerBuilder, 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

Stars

Watchers

Forks

Packages

No packages published

Contributors5


[8]ページ先頭

©2009-2025 Movatter.jp