Learn design Patterns from React: Master programming “routines” to build high-quality apps

This lecture will focus on design patterns in React.

Similar to the performance tuning perspective, when you talk about the React design pattern, you’re really talking about “the React component design pattern.” React Design Patterns The React design patterns are the most important and most talked about:

  • High order Component (HOC)

  • Render Props

  • Strip stateful and stateless components

In addition, each team may have its own methodology of design patterns that it has developed in practice. Although these React design modes have different implementation ideas, they basically aim to achieve the ultimate goal — reuse of component logic with as elegant a posture as possible.

And none of the three models we’ll explore in this lecture are any better.

The first thing to do in this lecture is to figure out exactly what each design pattern is and how to use it. In this process, we will understand the two very important design principles of “single responsibility” and “open and closed”.

On top of that, we need to ask the question: Are design patterns universal? If not, is there a deeper, more radical solution to the framework?

In the end, it comes back to Hooks. Let’s take the first step to familiarize/refresh ourselves with the concept of higher-order components.

1. HOC: The most classical way of logic reuse of components

1) What are higher-order components

  • HOC is an advanced technique used in React to reuse component logic. HOC itself is not part of the React API; it is a design pattern based on the composite features of React. – the React official

Higher-order Components follow higher-order functions in concept. In lecture 21, having dealt with higher-order functions, let’s review the concept of higher-order functions: a class of functions that receives a function as input or outputs another function.

A higher-order component, in turn, is a function that takes a component and returns a new component. Yes, a higher-order component is essentially a function. Here is a simple example of a higher-order component:

const withProps = (WrappedComponent) => { const targetComponent = (props) => ( <div className="wrapper-container"> <WrappedComponent {... props} /> </div> ); return targetComponent; };Copy the code

In this code, the withProps is a higher-order component.

2) How is logic reuse realized for higher-order components?

Now consider the following situation: there is a method called checkUserAccess which is used to check whether the user’s identity is valid. If it is not, then some components must adjust their display logic based on the invalid identity (for example, the display of the personal information screen requires “please verify identity”).

If A, B, C, D, and E are required to check whether the user identity is valid, then each of the five components should first request the checkUserAccess interface. CheckUserAccess = checkUserAccess = checkUserAccess = checkUserAccess = checkUserAccess = checkUserAccess = checkUserAccess = checkUserAccess = checkUserAccess

This layer of generic logic can be defined in higher-order components as follows:

Import checkUserAccess from './utils // Wrap the target component const with a higher-order component WithCheckAccess = (WrappedComponent) => {// This part is generic logic: Const isAccessible = checkUserAccess() // pass isAccessible to the targetComponent const targetComponent = (props) =>  ( <div className="wrapper-container"> <WrappedComponent {... props} isAccessible={isAccessible} /> </div> ); return targetComponent; };Copy the code

So when you need to reuse this layer of request logic for a component, you simply wrap the component with withCheckAccess directly. Take component A as an example. Assuming that the original version of component A is AComponent, the form of wrapping it is as follows:

const EnhancedAComponent = withCheckAccess(Acomponent);
Copy the code

By simply introducing the higher-order component withCheckAccess, EnhancedAComponent easily has the ability to verify the user’s legitimacy. If five more components wanted to introduce checkUserAccess, it wouldn’t be a problem — after all, wrapping five components isn’t the same amount of work as rewriting five pieces of logic.

Higher-order components can not only simplify the process of introducing logic, but also effectively avoid the cumbersome modification steps caused by logical changes: If this section of checkUserAccess logic is scattered in A, B, C, D, E these five components, then once the checkUserAccess decision rules need to modify, will need to modify the five code; But now checkUserAccess is pulled into a separate higher-order component, and a change in the higher-order component will take effect in all components it processes.

It can be seen that higher-order components can fundamentally reduce the repetitive writing and modification work, which is not only the benefit of higher-order components as a model, but also the meaning of “logic reuse”.

2, Render Props: Another way of thinking about logic reuse

  • The term “Render Prop” refers to a simple technique for sharing code between React components using a prop with a value of function. – the React official

1) What are render props?

Render props is another way of using component logic in React. It is implementation-wise similar to higher-order components — both of which extract common logic into one place. The difference is mainly in the use layer. The use posture of higher-order components is “function” wrapped around “component”, whereas the render props are the opposite, emphasizing “component” wrapped around “function”.

A simple render props could look like this, as shown in the following code:

import React from 'react'  
const RenderChildren = (props) => {
  return(
     <React.Fragment>
        {props.children(props)}
     </React.Fragment>
  );
};
Copy the code

RenderChildren will render all of its children. From this code, there are two important points to take away:

  • The carrier of render props should be a React component, as opposed to higher-order components (which are essentially functions);

  • The Render props component works only if its child components need to exist as functions.

The first point is relatively straightforward, and if you’re confused by the second point, it’s ok to get straight to how RenderChildren is used. See the code below:

<RenderChildren> {() => <p> </p> </RenderChildren>Copy the code

RenderChildren is itself a React component that wraps around other React components. In general, we are used to seeing “tag wrapped tag” packages, which are shown in the following code:

</p> </RenderChildren> </RenderChildren>Copy the code

In the render props mode, however, it requires that the function wrapped in the render props component tag be a function, so that the function is passed in as a child component. Thus, the Render props component can communicate with the target component by calling this function and passing the props.

2) How to implement logic reuse for render props?

Again, the checkUserAccess scenario is used as an example. To do this, use the render props to reuse the checkUserAccess logic:

Import checkUserAccess from './utils // define render props component const CheckAccess = (props) => {// This is generic logic: Const isAccessible = checkUserAccess() // Pass isAccessible to the target component return <React.Fragment> {props.children({ ... props, isAccessible })} </React.Fragment> };Copy the code

The CheckAccess child then gets the isAccessible value as follows:

<CheckAccess> { (props) => { const { isAccessible } = props; return <ChildComponent {... props} isAccessible={isAccessible} /> } } </CheckAccess>Copy the code

So far, we have seen the context of the “function passed in as a child component.” However, for the render props model, the function does not have to be passed as a child component. It can be passed as any property name, as long as the Render props component feels it.

For example, if a function could be passed to the Render props component with a property called checkTaget, the CheckAccess component could simply rewrite its receiver function as follows:

Import checkUserAccess from './utils // define render props component const CheckAccess = (props) => {// This is generic logic: Const isAccessible = checkUserAccess() // Pass isAccessible to the target component return <React.Fragment> {props.checkTaget({ ... props, isAccessible })} </React.Fragment> };Copy the code

To use the CheckAccess component, pass the function to the component in checkTaget, as shown in the following code:

<CheckAccess checkTaget={(props) => { const { isAccessible } = props; return <ChildComponent {... props} isAccessible={isAccessible} /> }} />Copy the code

It is also perfectly ok to use render props like this.

3) Understand the flexibility of render props

There is a bit of a confusion here: both higher-order components and render props can reuse logic, so which one is better?

Render props is a better choice for you because it’s more flexible. Where to begin with “more flexibility”?

A very important difference between render props and higher-order components lies in the processing of data: in higher-order components, the target component has no initiative to obtain data, and the data distribution logic converges inside the higher-order components. In the render props, in addition to the parent component distributing data, the child component can optionally receive data.

For example, if you have an F component, it also needs the checkUserAccess logic. However, F is an old component that does not recognize props. IsAccessible. It only recognizes props. With this requirement in mind, let’s take a look at how higher-order components solve the problem. The original higher-order component logic looks like this:

Import checkUserAccess from './utils // Wrap the target component const with a higher-order component WithCheckAccess = (WrappedComponent) => {// This part is generic logic: Const isAccessible = checkUserAccess() // pass isAccessible to the targetComponent const targetComponent = (props) =>  ( <div className="wrapper-container"> <WrappedComponent {... props} isAccessible={isAccessible} /> </div> ); return targetComponent; };Copy the code

It automatically installs the isAccessible variable on all components. In order to adapt it to the logic of F component, the most straightforward idea is to add a component type judgment in withCheckAccess. Once it is determined that the current input parameter is F component, name isAccessible specifically to isValidated.

This is a temporary solution, but it’s not a flexible solution: If more and more components need to change their property names, withCheckAccess will inevitably become bloated inside and difficult to maintain over time.

In fact, there is a very important principle in software design patterns called the “open closed principle”. A good model should be as open as possible to expansion and closed to modification.

When you find that the internal logic of withCheckAccess needs to change frequently in response to changing requirements, you should be on alert because this violates the “close to change” principle.

With the same requirements, render props were able to help us with the “open and closed” principle.

As mentioned earlier, in addition to the parent component distributing data, the child component can optionally receive data in the render props. This means that data adaptation can be implemented in the new F component-related logic (as shown in the code below) without affecting the logic in the old CheckAccess component.

<CheckAccess> { (props) => { const { isAccessible } = props; return <ChildComponent {... props} isValidated={isAccessible} /> } } </CheckAccess>Copy the code

This way, no matter how many new components are added or how many property names need to be changed, the impact surface remains firmly in the category of “new logic.” The Render props model, which conforms to the “open and closed” principle, is obviously much more flexible than higher-order components.

Stateful versus stateless components: The practice of the single responsibility principle in component design patterns

1) What is the “single responsibility” principle?

The single responsibility principle, also known as the “single function principle,” states that a class or module should have only one reason for change. In plain English, this means aggregating component functionality as much as possible and not trying to make one component do too many things.

2) What is stateful component? What is a stateless component?

The concept of stateless components is presented in lecture 07 (juejin.cn/post/691873…). Has been introduced, here is a simple review:

The React component is, as the name suggests, a function. In the early days, react-hooks were not implemented. Function components could not define and maintain state internally, hence the name “stateless component”.

A typical stateless component, as shown in the following code:

function DemoFunction(props) { const { text } = props return ( <div className="demoFunction"> <p>{`function [${text}] '}</p> </div>); }Copy the code

Stateless components are not necessarily functional components, and class components that do not maintain internal state can also be considered stateless components. By contrast, a component that maintains state and manages data within the component is a “stateful component.”

3) Why do stateful and stateless components need to be stripped?

Stateful and stateless components have many nicknames. Some books refer to them as “container components” and “presentation components,” or even “smart components” and “dumb components.” Whatever it’s called, the core purpose is to separate data processing from interface rendering.

Why do you do that? Remember, React’s core feature is a “data-driven view”, and we often use the following formula to express how it works:

So for a React component, what it does comes down to these two things:

  • Data processing (including data acquisition, formatting, distribution, etc.)

  • Rendering interface

It’s possible to do both in one component, but that’s not elegant.

According to the principle of “single responsibility”, the logic of data processing and interface rendering should be separated into different components, so that the combination of functional modules will be more flexible and more conducive to the reuse of logic. In addition, a single responsibility can limit the scope of change as much as possible, reducing the maintenance cost of the code: when the data-related logic changes, only the stateful components need to be modified, and the stateless components are completely unaffected.

Design patterns do not solve all problems

Design patterns are good, but they are not a panacea.

React, both high-order components and render props are designed to make up for the inflexibility of class components in the “logic reuse” aspect. Each of them has its own shortcomings, including but not limited to the following:

  • The nesting hell problem, when there are too many levels of nesting, data source traceability becomes very difficult

  • Higher learning costs

  • Name conflict of props properties

.

Overall, the development model of “HOC/ Render props+ class components” is still inadequate. When design patterns don’t solve problems, we instinctively look to programming patterns for answers. So there was “functional programming” as a complement to (and potentially a replacement for) “object orientation” as you see in React, and the general trend that we see today is “Hooks to everything.”

Now, when we want to reuse a piece of logic, the first thing to think of is not “higher-order functions” or “render props,” but “custom hooks.” Hooks do a great job of avoiding the pitfalls of the various design patterns in old-time class components, such as no nested hell, allowing property renaming, allowing us to import and access target state wherever we need it, etc. As you can see, a good programming pattern can save a lot of time “patching” the various component design patterns. The better the framework, the easier the developer’s job.

5, summary

Learn how to design the React component. While understanding the two classical design modes of high-order components and Render props, I also formed a preliminary understanding of the two important software design principles of “single responsibility” and “open and closed”.

There are no silver bullets in software, and if there were, they couldn’t be design patterns. Through the study of this lecture, we recognize the advantages of design pattern, but also realize its limitations. On this basis, we also have a stronger positive view of react-hooks and the “functional programming” philosophy behind them.

That concludes the React tutorial. After studying this column, we will be more or less interested in the React operating mechanism, and even the front-end framework. In the next lecture, we’ll share our understanding of frameworks and use the opportunity to talk about React 17, which has just launched.

Learning the source (the article reprinted from) : kaiwu.lagou.com/course/cour…