In this paper, the React component design modes of container and presentation components, high-order components, and render props are summarized

HBaseCon Asia 2019 Track 3


As React evolved, various component design patterns emerged. The React official document also has a number of related articles, but the organization is a little messy. This article introduces common design patterns for components from the perspective of problems. By the end of this article, you’ll be in a good position to handle most React components.
Before we begin, let’s explain what design patterns are. A pattern is a common solution to a problem in some scenarios. The design patterns described in this paper are not common programming design patterns, such as the familiar singleton pattern, factory pattern and so on. Here are some solutions and tips for designing the React component:
(1) Containers and display components
(2) Higher-order components
(3)render props
(4) the context model
(5) Composite components
(6) inheritance
In order to better understand, you can take the corresponding source download check: (https://github.com/imalextu/learn-react-patterns)
As the content is more, divided into two. First introduction:
(1) Containers and display components
(2) Higher-order components
(3) Render props.

Container and Presentational components



>>>>

The concept is introduced

Let’s start with a simpler usage pattern, container components versus presentation components. This pattern is also variously called fat and thin components, stateful and stateless components, smart and dumb components, and so on.
There are many names, but the essence is the same: when a component interacts with external data, we can split the component into two parts:
Container component: mainly responsible for the interaction (communication) with external data, such as data binding with Redux, fetching data through ordinary FETCH, etc.
Presentation component: Render only according to its own state and props received from the parent component, and do not communicate directly with external data sources.
>>>>

The sample

Let’s look at a simple example. Construct a component that takes text and displays it.

export default class GetText extends React.Component {  state = {    text: null,  }   componentDidMount() {    fetch('https://api.com/',      { headers: { Accept: 'application/json' } }).then(response => {        return response.json()      }).then(json => {        this.setState({ text: json.joke })      })  }  render() {    return(< div > < div > external access to data: {this. State. The text} < / div > < div > UI code < / div > < / div >)}}Copy the code

Seeing the GetText component above, there is logic to communicate with external data sources. Then we can split this component into two parts.

One part is dedicated to external communication (container components) and one part is responsible for UI logic (presentation components). Let’s break down the above example.

Container components:

export default class GetTextContainer extends React.Component {  state = {    text: null,  }   componentDidMount() {    fetch('https://api.com/',      { headers: { Accept: 'application/json' } }).then(response => {        return response.json()      }).then(json => {        this.setState({ text: json.joke })      })  }  render() {    return (<div><GetTextPresentational text={this.state.text}/></div>    )  }}Copy the code

Display components:

export default class GetTextPresentational extends React.Component {  render() {    return(< div > < div > external access to data: {this. Props. Text} < / div > < div > UI code < / div > < / div >)}}Copy the code
SRC /pattern1(http://t.cn/AiYbWWak)

>>>>

Problems solved by patterns

One of the principles of software design is the Separation of Responsibility, which means that a module should have as few responsibilities as possible. If a module is found to have too many functions, it should be broken up into multiple modules, each focusing on a single function, which makes it easier to maintain the code.


The problem with the container presentation component model is that when we switch data retrieval methods, we only need to change the logic in the container component, not the presentation component. For example, now we obtain the data source through the ordinary FETCH request, so we will change to Redux or MOBX as the data source in the future. We only need to focus on the container component to modify the corresponding logic, and the display component can be completely unchanged, and the display component has higher reusability.
However, the disadvantage of this pattern is that it splits a component into two parts, increasing the cost of code hopping. It is not necessary to split a component into a container component and a presentation component. You need to weigh the advantages and disadvantages of a split. For a more in-depth look at this pattern, read this article:
Presentational and Container Components (http://t.cn/RqMyfwV).

Second, high order components



>>>>

The concept is introduced

HOC and render callbacks come in handy when you want to reuse a component’s logic. We’ll start with higher-order components, which essentially make use of a function that takes a React component as an argument and returns a new component.
We’ve certainly seen a lot of situations where we need to reuse business logic, such as when we had a female e-commerce site and all the components had to be identified as female before they were displayed. For example, in the List component, the message is not open to men if it is a male, and the specific clothing List is displayed if it is a female. In the ShoppingCart component, the same logic would indicate that the cart is not open to men if it is male, and the corresponding cart would be displayed if it is female.
>>>>

The sample

As we’ve already said, higher-order components actually use a function that takes the React component as an argument and returns the new component.

We’re going to create a new judgeWoman function here, take the specific presentation component, and determine if it’s female,

Const judgeWoman = (Component) => {const NewComponent = (props) => {// Determine if the user is femaleletIsWoman = math.random () > 0.5?true : false    if (isWoman) {      const allProps = { add: 'Properties added to higher-order components'. props }return<Component {... allProps} />; }else {      return<span> <span> }}returnNewComponent; };Copy the code

Pass the List and ShoppingCart components as arguments to this function. At this point, we have two enhanced components WithList and WithShoppingCart. The logic of whether it was female was repeated.

const List = (props) => {  return<div> <div> <div>{props. Add}</div></div>)}const WithList = judgeWoman(List)const ShoppingCart = (props) => {return<div> <div>{props. Add}</div></div>) const WithShoppingCart = judgeWoman(ShoppingCart)Copy the code
As the simple example above shows, we can also pass in multiple components to this function. Let’s say we pass in two components, the first component seen by women and the second component seen by men. Is reusability more powerful?
Const judgeWoman = (Woman,Man) => {const NewComponent = (props) => {// Determine if the user is femaleletIsWoman = math.random () > 0.5?true : false    if (isWoman) {      const allProps = { add: 'Properties added to higher-order components'. props }return<Woman {... allProps} />; }else {      return <Man/>    }  }  returnNewComponent; };Copy the code

Even more powerful, since functions also return components, higher-order components can be nested for use! For example, we judge gender first and then age.

const withComponet =judgeAge(judgeWoman(ShoppingCart))Copy the code

The code is visible SRC /pattern2(http://t.cn/AiYbYy5g)

>>>>

Problems solved by patterns

We should never write the same logic more than once. Higher-order components serve to extract common logic. At the same time, the nesting of higher-order components makes code reuse more flexible.

React-redux uses this pattern. Look at the code below. Connect (mapStateToProps, mapDispatchToProps) generates a higher-order component function that takes TodoList as an argument. Finally, the high-level component VisibleTodoList is returned.

import { connect } from 'react-redux'const VisibleTodoList = connect(  mapStateToProps,  mapDispatchToProps)(TodoList)Copy the code
>>>>

Precautions for Use

Advanced components are good, but we should pay attention to the following points when using them.

1, packaging display name for easy debugging

Debugging is cumbersome when using higher-order components. When React renders an error, the component’s displayName static property is used to determine the error component class. Container components created by HOC will show up in React Developer Tools just like any other component. To facilitate debugging, we need to select a display name to indicate that it is a product of HOC.

The most common approach is to use HOC to wrap the display name of the wrapped component. For example, a higher-order component called withSubscription and the display name for the wrapped component called CommentList would be withSubscription (CommentList) :

function withSubscription(WrappedComponent) {  class WithSubscription extends React.Component {/* ... */}  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)}) `;returnWithSubscription; }function getDisplayName(WrappedComponent) {  return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }Copy the code

2. Don’t use HOC in render methods

The React diff algorithm (called coordination) uses component identifiers to determine whether it should update an existing subtree or drop it and mount a new one. If the component returned from Render is the same as the component in the previous render (===), React updates the subtree recursively by differentiating it from the new one. If they are not equal, the previous subtree is completely unloaded.

Usually, you don’t have to think about that. But this is important with HOC, because it means you shouldn’t apply HOC to a component in its render method:

render// EnhancedComponent is created every time the render function is called // EnhancedComponent1! == EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // This will cause subtrees to be unmounted and remounted every time they are rendered!return<EnhancedComponent />; }Copy the code
This is not just a performance issue; remounting a component causes a loss of state for that component and all its children.
If you create HOC outside of the component, then the component will only be created once. Therefore, the same component is rendered each time. Generally speaking, this is in line with your expected performance. In rare cases, you need to call HOC dynamically. You can do this in a component’s lifecycle method or in its constructor.
3. Be sure to copy static methods
Sometimes it’s useful to define static methods on the React component. For example, the Relay container exposes a static method getFragment to facilitate composing GraphQL fragments.
However, when you apply HOC to a component, the original component will be wrapped with a container component. This means that the new component does not have any static methods of the original component.
/ / define static function WrappedComponent staticMethod =function() {/ *... }// now use HOCconst EnhancedComponent = enhance(WrappedComponent); . / / enhancement components without staticMethodtypeof EnhancedComponent staticMethod = = ='undefined' // trueCopy the code
To solve this problem, you can copy these methods to the container component before returning:
functionenhance(WrappedComponent) { class Enhance extends React.Component {/*... * /} / / must be accurate know which way to copy: (Enhance) staticMethod. = WrappedComponent staticMethod;returnEnhance; }Copy the code
But to do that, you need to know which methods should be copied. You can automatically copy all non-React static methods using hoist non-react statics:
import hoistNonReactStatic from 'hoist-non-react-statics';functionenhance(WrappedComponent) { class Enhance extends React.Component {/*... */} hoistNonReactStatic(Enhance, WrappedComponent);returnEnhance; }Copy the code
In addition to exporting components, another possible solution is to export the static method in addition.
// Use this method instead of... MyComponent.someFunction = someFunction;exportdefault MyComponent; / /... Export the method separately... export { someFunction }; / /... And import them import MyComponent, {someFunction} from among the components to use'./MyComponent.js';Copy the code

Refs will not be passed

Although the convention for higher-order components is to pass all props to the wrapped component, this does not apply to Refs. That’s because a REF is not really a prop, just like a key, which is handled specifically by React. If you add ref to HOC’s return component, the REF reference points to the container component, not the wrapped component.

The solution to this problem is to use the React. ForwardRef API (introduced in React 16.3).

Three, Render the props



>>>>

The concept is introduced

The term “render props” refers to a simple technique for sharing code between React components using a prop with a value of function.
As with higher-order components, render props were introduced to address the reuse of business logic. As with the higher-order component example, let’s look at how this is implemented using render props.
>>>>

The sample

A component with render props expects the subcomponent to be a function, and all it does is call the subcomponent as a function, with the parameters being the props passed in, and render the returned result.

<Provider>   {props => <List add={props.add} />}</Provider>Copy the code

Let’s look at how a Provider component is defined. With this code, we call the function passed in. Props. Children (allProps).

Const Provider = (props) => {// Check whether the user is femaleletIsWoman = math.random () > 0.5?true : false  if (isWoman) {    const allProps = { add: 'Properties added to higher-order components'. props }return props.children(allProps)  } else {    return<div> for women only, men are not allowed to browse </div>; }}Copy the code

It seems that any higher-order component that render props can do can also do it, and the higher-order component is easier to understand. Let’s look at one of the more powerful aspects of render props: more flexibility for new props. Assuming that our List component accepts a Plus attribute and our ShoppingCart component accepts an Add attribute, we can write this directly without changing the List component or the Provider itself. Achieving the same effect with higher-order components is much more complicated.

<Provider>  {props => {    const { add } = props    return < List plus={add} />  }}</Provider><Provider>  {props => <ShoppingCart add={props.add} />}</Provider>Copy the code
The use of render props is not limited to children. Any prop property of the component can achieve the same effect. For example, we used test prop to achieve the same effect.
Const Provider = (props) => {// Check whether the user is femaleletIsWoman = math.random () > 0.5?true : false  if (isWoman) {    const allProps = { add: 'Properties added to higher-order components'. props }return props.test(allProps)  } else {    return<div> for women only, men are not allowed to browse </div>; }}const ExampleRenderProps = () => {return (    <div>      <Provider test={props => <List add={props.add} />} />      <Provider test={props => <ShoppingCart add={props.add} />} />    </div>  )}Copy the code
The code is visible SRC /pattern3 (http://t.cn/AiYG7916)
>>>>

Problems solved by patterns

As with higher-order components, render props serve to extract common logic. At the same time, render props can highly customize the properties needed by the incoming component.
The React Router we are familiar with and the context mode we will cover in the next article all have render props.
>>>>

Precautions for Use

Be careful when using Render Props with react. PureComponent! If you create functions in the Provider property, using render props will offset the advantages of using react. PureComponent. Because shallow comparisons of props always give false, and in this case each render will generate a new value for render props.

For example, continuing with the <List> component we used earlier, if List inherited from react. PureComponent instead of react.ponent, our example would look like this:

class ExampleRenderProps extends React.Component {  render() {    return(<div> {/* This is not good! Each render of 'testThe value of prop will be different. */} <Providertest={props => <List add={props.add} />} />      </div>    )  }}Copy the code
In this case, each time the

is rendered, it generates a new function as a prop for the , thus cancelling the effects of the component inherited from the react. PureComponent!
To get around this problem, sometimes you can define a prop as an instance method, something like this:
class ExampleRenderProps extends React.Component {  renderList=()=>{    return <List add={props.add} />  }  render() {    return (      <div>        <Provider test={this.renderList} />      </div>    )  }}Copy the code

If you cannot define a prop statically (for example, because you need to turn off props and/or state for the component), then <List> should be extended from react.component.component.component.props.

summary



In fact, the react official documentation can be seen, but the official documentation is a little messy. You can also check out the official tutorial after reading this article.


Reference Documents:
  • React

(http://t.cn/AiYGz4Na)
  • React Component Patterns

(http://t.cn/EvsJ8gj)
  • React Combat: Design patterns and best practices

(http://t.cn/EUy09Ml)
  • Presentational and Container Components

(http://t.cn/RqMyfwV)


Reprint please indicate the source “Miui Cloud technology”