- Notifications
You must be signed in to change notification settings - Fork0
Reason Style Reducer Components Using ES6 Classes
License
TrendingTechnology/react-recomponent
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Reason-style reducer components for React using ES6 classes.
A number of solutions to manage state in React applications are based on the concept of a "reducer" to decouple actions from effects. The reducer is a function that transforms the state in response to actions. Examples for such solutions are theRedux library and architectures likeFlux.
Most recently this pattern was implemented inReasonReact as the built-in solution to manage local component state. Similarly to Redux, ReasonReact components implement a reducer and actions to trigger state changes but do so while staying completely inside regular React state. These components are referred as reducer components.
ReComponent borrows these ideas from ReasonReact and brings reducer components to the React ecosystem.
A reducer component is used like a regular, stateful, React component with the difference thatsetState is not allowed. Instead, state is updated through areducer which is triggered by sending actions to it.
npm install react-recomponent --saveTo create a reducer component extendReComponent fromreact-recomponent instead ofReact.Component.
WithReComponent state can only be modified by sending actions to thereducer() function. To help with that, you can usecreateSender(). Take a look at a simple counter example:
importReactfrom"react";import{ReComponent,Update}from"react-recomponent";classCounterextendsReComponent{constructor(){super();this.handleClick=this.createSender("CLICK");this.state={count:0};}staticreducer(action,state){switch(action.type){case"CLICK":returnUpdate({count:state.count+1});}}render(){return(<buttononClick={this.handleClick}> You’ve clicked this{this.state.count} times(s)</button>);}}
TheCounter component starts with an initial state of{ count: 0 }. Note that this state is in fact a regular React component state. To update it, we use a click action which we identify by its type"CLICK" (this is similar to the way actions are identified in Redux).
Thereducer will receive this action and act accordingly. In our case, it will return anUpdate() effect with the modified state.
ReComponent comes with four different types ofeffects:
NoUpdate()signalize that nothing should happen.Update(state)update the state.SideEffects(fn)run an arbitrary function which hasside effects. Side effects may never be run directly inside the reducer.A reducer should always be pure: for the same action applied onto the same state, it should return the same effects.This is to avoid bugs when React will work asynchronously.UpdateWithSideEffects(state, fn)both update the state and then trigger the side effect.
By intelligently using any of the four types above, it is possible to transition between states in one place and without the need to usesetState() manually. This drastically simplifies our mental model since changes must always go through the reducer first.
The advantages are similar to those ofRedux or really any state management tool:
Decoupling your state transformers from the rest of the code. This can be a little cumbersome when working with React alone since you will scatter a variety of setState inside your components which becomes harder to follow when the component grows. The sender/reducer system simplifies this since you will no longer focus on state changes within the various methods of your component but you’ll think of actions that you want to send which contains all the information as a standalone object. With that, adding additional behavior (like logging) becomes very easy since all you have to do is hook this logic inside the reducer.
Improvedmaintainability by forcing a structure. WithRedux orReComponent, you have a good overview of all actions that your application can send. This is an amazing property and allows others to easily understand what a component is is (actually) doing. While you can already learn so much by looking at the shape of the state object, you’ll lean even more just bylooking at the action types alone. And since it’s not allowed to use setState at all, you can also be certain that all the code inside the reducer is the only place that transforms your state.
Get rid of side effects withPure State Transformation. By keeping your state changes side effect free, you’re forced into writing code that is easier to test (given an action and a state, it mustalways return the same new state). Plus you can build extended event sourcing features on top of that since you can easily store all actions that where send to your reducers and replay them later (to go back in time and see exactly how an invalid state occurred).
To fully leverage all of the advantages outlined above, the reducer function must not have any side effects. Making the reducerstatic will enforce this behavior since you won’t have access tothis inside the function. We identified three situations that could needthis inside the reducer:
- You’re about to read class properties. In this case, make sure those properties are properly encapsulated in the state object.
- You’re about to write class properties. This is a side effect and should be handled using the
SideEffects(fn)effect. - You’re accessing a function that is pure by itself. In this case, the function does not need to be a class property but can be a regular module function instead.
Now that we‘ve learned how to use reducer components with React, it‘s time to look into more advanced use cases to effectively handle state transitions across bigger portions of your app.
We‘ve already said that ReComponent comes with four different types ofeffects. This is necessary to effectively handle side effects by keeping your reducer pure – given the same state and action, it will always return the same effects.
The following example will demonstrate the four different types of effects and show you how to use them:
importReactfrom"react";import{ReComponent,NoUpdate,Update,SideEffects,UpdateWithSideEffects}from"react-recomponent";classCounterextendsReComponent{constructor(){super();this.handleNoUpdate=this.createSender("NO_UPDATE");this.handleUpdate=this.createSender("UPDATE");this.handleSideEffects=this.createSender("SIDE_EFFECTS");this.handleUpdateWithSideEffects=this.createSender("UPDATE_WITH_SIDE_EFFECTS");this.state={count:0};}staticreducer(action,state){switch(action.type){case"NO_UPDATE":returnNoUpdate();case"UPDATE":returnUpdate({count:state.count+1});case"SIDE_EFFECTS":returnSideEffects(()=>console.log("This is a side effect"));case"UPDATE_WITH_SIDE_EFFECTS":returnUpdateWithSideEffects({count:state.count+1},()=>console.log("This is another side effect"));}}render(){return(<React.Fragment><buttononClick={this.handleNoUpdate}>NoUpdate</button><buttononClick={this.handleUpdate}>Update</button><buttononClick={this.handleSideEffects}>SideEffects</button><buttononClick={this.handleUpdateWithSideEffects}> UpdateWithSideEffects</button><div>The current counter is:{this.state.count}</div></React.Fragment>);}}
All side effect callbacks get a reference to the react component passed as the first argument. This is helpful when a side effect needs to send other actions to the reducer. The next example shows how you can leverage this to handle a more complex component that fetches data from a third party and has to handle multiple states:
importReactfrom"react";import{ReComponent,NoUpdate,Update,UpdateWithSideEffects}from"react-recomponent";import{fetchData}from"./api";classFetcherextendsReComponent{constructor(){super();this.handleRequestStart=this.createSender("REQUEST_START");this.handleRequestSuccess=this.createSender("REQUEST_SUCCESS");this.handleRequestFail=this.createSender("REQUEST_FAIL");this.state={isFetching:false,result:null};}staticreducer(action,state){switch(action.type){case"REQUEST_START":if(state.isFetching){returnNoUpdate();}else{returnUpdateWithSideEffects({isFetching:true},instance=>{fetchData().then(instance.handleRequestSuccess,instance.handleRequestFail);});}case"REQUEST_SUCCESS":returnUpdate({result:action.payload,isFetching:false});case"REQUEST_FAIL":returnUpdate({result:"The data could not be fetched. Maybe try again?",isFetching:false});}}render(){return(<React.Fragment><buttononClick={this.handleRequestStart}>Fetch</button><div>{this.state.isFetching&&<p>Loading...</p>}<p>{this.state.result ?this.state.result :'Click "Fetch" to start'}</p></div></React.Fragment>);}}
React uses a method called pooling to improve performance when emitting events (check out the guides onSyntheticEvent to learn more). Basically React recycles events once the callback is handled making any reference to them unavailable.
Since the reducer function always runs within thesetState() callback provided by React, synthetic events will already be recycled by the time the reducer is invoked. To be able to access event properties, we recommend passing the required values explicitly. The following example will show the coordinates of the last mouse click. To have control over which properties are sent to the reducer, we‘re usingsend directly in this case:
importReactfrom"react";import{ReComponent,Update}from"react-recomponent";classCounterextendsReComponent{constructor(){super();this.handleClick=this.handleClick.bind(this);this.state={x:0,y:0};}handleClick(event){this.send({type:"CLICK",payload:{x:event.clientX,y:event.clientY}});}staticreducer(action,state){switch(action.type){case"CLICK":returnUpdate({x:action.payload.x,y:action.payload.y});}}render(){const{ x, y}=this.state;conststyle={width:"100vw",height:"100vh"};return(<divstyle={style}onClick={this.handleClick}> Last click at:{x},{y}</div>);}}
Often times we want to pass state properties to descendants that are very deep in the application tree. In order to do so, the components in between need to pass those properties to their respective children until we reach the desired component. This pattern is usually calledprop drilling and it is usually what you want to do.
Sometimes, however, the layers in-between are expensive to re-render causing your application to become janky. Fortunately, React 16.3.0 introduced a new API calledcreateContext() that we can use to solve this issue by using context to pass those properties directly to the target component and skipping the update of all intermediate layers:
importReactfrom"react";import{ReComponent,Update}from"react-recomponent";const{ Provider, Consumer}=React.createContext();classCounterextendsReact.Component{render(){return(<Consumer>{({ state, handleClick})=>(<buttononClick={handleClick}> You’ve clicked this{state.count} times(s)</button>)}</Consumer>);}}classDeepTreeextendsReact.Component{render(){return<Counter/>;}}classContainerextendsReComponent{constructor(){super();this.handleClick=this.createSender("CLICK");this.state={count:0};}staticreducer(action,state){switch(action.type){case"CLICK":returnUpdate({count:state.count+1});}}render(){return(<Providervalue={{state:this.state,handleClick:this.handleClick}}><DeepTree/></Provider>);}}
If you‘re having troubles understanding this example, I recommend the fantastic documentation written by the React team aboutContext.
Flow is a static type checker for JavaScript. This section is only relevant for you if you‘re using Flow in your application.
ReComponent comes with first class Flow support built in. By default, a ReComponent will behave like a regular Component and will require props and state to be typed:
import*asReactfrom"react";import{ReComponent,Update}from"react-recomponent";typeProps={};typeState={count:number};classUntypedActionTypesextendsReComponent<Props,State>{handleClick=this.createSender("CLICK");state={count:0};staticreducer(action,state){switch(action.type){case"CLICK":returnUpdate({count:state.count+1}); default:returnNoUpdate();}}render(){return(<buttononClick={this.handleClick}> You’ve clicked this{this.state.count} times(s)</button>);}}
Without specifying our action types any further, we will allow allstring values. It is, however, recommended that we type all action types using a union of string literals. This will further tighten the type checks and will even allowexhaustiveness testing to verify that every action is indeed handled.
import*asReactfrom"react";import{ReComponent,Update}from"react-recomponent";typeProps={};typeState={count:number};typeActionTypes="CLICK";classTypedActionTypesextendsReComponent<Props,State,ActionTypes>{handleClick=this.createSender("CLICK");state={count:0};staticreducer(action,state){switch(action.type){case"CLICK":returnUpdate({count:state.count+1}); default:{returnNoUpdate();}}}render(){return(<buttononClick={this.handleClick}> You’ve clicked this{this.state.count} times(s)</button>);}}
Check out thetype definition tests for an example on exhaustive checking.
Known Limitations With Flow:
- While it is possible to exhaustively type check the reducer, Flow will still require every branch to return an effect. This is why the above examples returns
NoUpdate()even though the branch can never be reached.
ReComponentstatic reducer(action, state): effectTranslates an action into an effect. This is the main place to update your component‘s state.
Note: Reducers should never trigger side effects directly. Instead, return them as effects.
send(action): voidSends an action to the reducer. The actionmust have a
typeproperty so the reducer can identify it.createSender(actionType): fnShorthand function to create a function that will send an action of the
actionTypetype to the reducer.If the sender function is called with an argument (for example a React event), this will be available at the
payloadprop. This follows theflux-standard-actions naming convention.
RePureComponent- Same
ReComponentbut based onReact.PureComponentinstead.
- Same
NoUpdate()Returning this effect will not cause the state to be updated.
Update(state)Returning this effect will update the state. Internally, this will use
setState()with an updater function.SideEffects(this => mixed)Enqueues side effects to be run but will not update the component‘s state. The side effect will be called with a reference to the react component (
this) as the first argument.UpdateWithSideEffects(state, this => mixed)Updates the component‘s state andthen calls the side effect function.The side effect will be called with a reference to the react component (
this) as the first argument.
About
Reason Style Reducer Components Using ES6 Classes
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Languages
- JavaScript100.0%
