

GitHub - philipp-spiess/react-recomponent: Reason Style Reducer Components Using...
source link: https://github.com/philipp-spiess/react-recomponent
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

README.md
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 the Redux library and architectures like Flux.
Most recently this pattern was implemented in ReasonReact 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 that setState
is not allowed. Instead, state is updated through a reducer
which is triggered by sending actions to it.
Installation
npm install react-recomponent --save
Getting Started
To create a reducer component extend ReComponent
from react-recomponent
instead of React.Component
.
With ReComponent
state can only be modified by sending actions to the reducer()
function. To help with that, you can use createSender()
. Take a look at a simple counter example:
import React from "react"; import { ReComponent, Update } from "react-recomponent"; class Counter extends ReComponent { constructor() { super(); this.handleClick = this.createSender("CLICK"); this.state = { count: 0 }; } reducer(action, state) { switch (action.type) { case "CLICK": return Update({ count: state.count + 1 }); } } render() { return ( <button onClick={this.handleClick}> You’ve clicked this {this.state.count} times(s) </button> ); } }
The Counter
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).
The reducer
will receive this action and act accordingly. In our case, it will return an Update()
effect with the modified state.
ReComponent comes with four different types of effects:
NoUpdate()
signalize that nothing should happen.Update(state)
update the state.SideEffects(fn)
run an arbitrary function which has side 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 use setState()
manually. This drastically simplifies our mental model since changes must always go through the reducer first.
Advanced Usage
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.
Side Effects
We‘ve already said that ReComponent comes with four different types of effects. 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:
import React from "react"; import { ReComponent, NoUpdate, Update, SideEffects, UpdateWithSideEffects } from "react-recomponent"; class Counter extends ReComponent { 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 }; } reducer(action, state) { switch (action.type) { case "NO_UPDATE": return NoUpdate(); case "UPDATE": return Update({ count: state.count + 1 }); case "SIDE_EFFECTS": return SideEffects(() => console.log("This is a side effect")); case "UPDATE_WITH_SIDE_EFFECTS": return UpdateWithSideEffects({ count: state.count + 1 }, () => console.log("This is another side effect") ); } } render() { return ( <React.Fragment> <button onClick={this.handleNoUpdate}>NoUpdate</button> <button onClick={this.handleUpdate}>Update</button> <button onClick={this.handleSideEffects}>SideEffects</button> <button onClick={this.handleUpdateWithSideEffects}> UpdateWithSideEffects </button> <div>The current counter is: {this.state.count}</div> </React.Fragment> ); } }
Handling Events
React uses a method called pooling to improve performance when emitting events (check out the guides on SyntheticEvent
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 the setState()
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 using send
directly in this case:
import React from "react"; import { ReComponent, Update } from "react-recomponent"; class Counter extends ReComponent { 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 } }); } reducer(action, state) { switch (action.type) { case "CLICK": return Update({ x: action.payload.x, y: action.payload.y }); } } render() { const { x, y } = this.state; const style = { width: "100vw", height: "100vh" }; return ( <div style={style} onClick={this.handleClick}> Last click at: {x}, {y} </div> ); } }
Manage State Across the Tree
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 called prop 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 called createContext()
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:
import React from "react"; import { ReComponent, Update } from "react-recomponent"; const { Provider, Consumer } = React.createContext(); class Counter extends React.Component { render() { return ( <Consumer> {({ state, handleClick }) => ( <button onClick={handleClick}> You’ve clicked this {state.count} times(s) </button> )} </Consumer> ); } } class DeepTree extends React.Component { render() { return <Counter />; } } class Container extends ReComponent { constructor() { super(); this.handleClick = this.createSender("CLICK"); this.state = { count: 0 }; } reducer(action, state) { switch (action.type) { case "CLICK": return Update({ count: state.count + 1 }); } } render() { return ( <Provider value={{ 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 about Context.
Flow
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 * as React from "react"; import { ReComponent, Update } from "react-recomponent"; type Props = {}; type State = { count: number }; class UntypedActionTypes extends ReComponent<Props, State> { handleClick = this.createSender("CLICK"); state = { count: 0 }; reducer(action, state) { switch (action.type) { case "CLICK": return Update({ count: state.count + 1 }); default: return NoUpdate(); } } render() { return ( <button onClick={this.handleClick}> You’ve clicked this {this.state.count} times(s) </button> ); } }
Without specifying our action types any further, we will allow all string
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 allow exhaustiveness testing to verify that every action is indeed handled.
import * as React from "react"; import { ReComponent, Update } from "react-recomponent"; type Props = {}; type State = { count: number }; type ActionTypes = "CLICK"; class TypedActionTypes extends ReComponent<Props, State, ActionTypes> { handleClick = this.createSender("CLICK"); state = { count: 0 }; reducer(action, state) { switch (action.type) { case "CLICK": return Update({ count: state.count + 1 }); default: { return NoUpdate(); } } } render() { return ( <button onClick={this.handleClick}> You’ve clicked this {this.state.count} times(s) </button> ); } }
Check out the type 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.
API Reference
Classes
-
ReComponent
-
reducer(action, state): effect
Translates 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): void
Sends an action to the reducer. The action must have a
type
property so the reducer can identify it. -
createSender(actionType): fn
Shorthand function to create a function that will send an action of the
actionType
type to the reducer.If the sender function is called with an argument (for example a React event), this will be available at the
payload
prop. This follows the flux-standard-actions naming convention.
-
-
RePureComponent
- Same
ReComponent
but based onReact.PureComponent
instead.
- Same
Effects
-
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(fn)
Enqueues side effects to be run but will not update the component‘s state.
-
UpdateWithSideEffects(state, fn)
Updates the component‘s state and then calls the side effect function.
License
Recommend
-
44
with-rematch A HoC that implement reducer a la Rematch Inpired By Rematch.js ♡ Instalation npm install -g with-rematch or yarn add with-rematch Getting St...
-
16
上午有同学在AINLP交流群里询问机器翻译书籍,看到后第一想到的就是 Philipp Koehn 大神,我读书...
-
31
React with/without Redux Using a Global Reducer in React
-
46
Exploring Large Data Files with pxiSoftware developers, data engineers, data scientists, statisticians, project managers, and people of many other professions work with data sets every day. A recurring task...
-
36
Redux is an amazing tool if you take the time to get to know it. One of the things about Redux that commonly trips people up...
-
6
A simple program reducer for any language · CombyI’ve been fuzzing compilers for less familiar languages like Solidity and Diem (for smart contracts...
-
11
Introduction Have you ever had to reset your Redux state to its initial state in your reducers? Resetting the state is something lots of apps need to do. A typical example of when the app state must be reset could be when the user l...
-
3
什么是 redux? 三大原则? 什么是 redux Redux 是一个基于 js 的全局可预测状态容器,主要用于现代前端框架中进行全局状态管理,能够在不同组件之间进行状态共享 Redux 常与 React 配合使用,但它并非只能用于 React,由...
-
6
@rzhelieznovRomanJavascript and React fan. Also love reading, traveling, pc gaming :)
-
2
Closed Bug 1822293...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK