Almost every developer needs to think about logic reuse, otherwise your project will end up with a lot of duplicate code. So how does React reuse component logic? In this article, you’ll learn how to reuse component logic in React. If you already know this, skip this article.

I have tried my best to verify the code and content in the article, but because of the limitations of my knowledge level, there are inevitable errors, welcome to correct in the comment area.

1. Mixins

Mixins are actually a product of React. CreateClass. Of course, if you’ve ever used Mixins in lower versions of React, such as react-timer-mixin, react-addons-pure-render-mixin, then you probably know that In the new version of React, mixins are still available. Although react. createClass has been removed, mixins can still be used using the create-react-class library. Even components written in ES6 have ways to use mixins. Of course, this is not the focus of this article, so I won’t go into more details. If you are maintaining an old project and encounter these problems during the upgrade process, please feel free to discuss them with me.

New projects rarely have Mixins, but if your company has older projects to maintain, they probably have Mixins in them, so it’s worth taking a little time to understand how they work and how they work. If you don’t need this at all, you can skip this section.

The use of Mixins

React 15.3.0 adds PureComponent. Before that, or if you used React. CreateClass, to get the same functionality, use react-addons-pure-render-mixin, for example:

// The following code works in new versions of React. Since 'React. CreateClass' is no longer available, I will not use 'React.

const createReactClass = require('create-react-class');
const PureRenderMixin = require('react-addons-pure-render-mixin');

const MyDialog = createReactClass({
    displayName: 'MyDialog'.mixins: [PureRenderMixin],
    //other code
    render() {
        return (
            <div>
                {/* other code */}
            </div>)}});Copy the code

First, note that the value of mixins is an array. If there are multiple mixins, just put them in the array one by one, for example: Mixins: [PureRenderMixin, TimerMixin].

The principle of Mixins

Mixins work simply by adding methods on a mixin object to a component. Similar to the $.extend method, React does a few other things. For example, mixins are not allowed to have the same attributes except for lifecycle functions, and they are not allowed to have the same attributes or methods as components, otherwise exceptions will be thrown. Also, even for lifecycle functions, constructor, Render, and shouldComponentUpdate are not allowed to be repeated.

The life cycle of compoentDidMount, for example, calls Mixins in turn, and then compoentDidMount defined in the component.

For example, the object provided by PureRenderMixin above has a shouldComponentUpdate method that is added to MyDialog, ShouldComponentUpdate can no longer be defined in MyDialog, otherwise it will throw an exception.

/ / the react - addons - pure - render - a mixin source code
var shallowEqual = require('fbjs/lib/shallowEqual');

module.exports = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return (
      !shallowEqual(this.props, nextProps) || ! shallowEqual(this.state, nextState) ); }};Copy the code

The disadvantage of Mixins

  1. Mixins introduce implicit dependencies.

    For example, each mixin depends on other mixins, so modifying one might break the other.

  2. Mixins can cause name conflicts

    If a method with the same name exists in two mixins, an exception is thrown. In addition, suppose you introduce a third-party mixin whose method name conflicts with your component’s method name, and you have to rename the method.

  3. Mixins lead to increasing complexity

    Mixins start out simple, but become more and more complex over time. For example, a component needs some state to track mouse hovering. To keep logic reusable, handleMouseEnter(), handleMouseLeave() and isHovering() might be distilled into HoverMixin().

    Then someone else might need to implement a prompt box, they don’t want to copy the logic of HoverMixin(), so they create a TooltipMixin that uses HoverMixin, TooltipMixin reads isHovering() provided by HoverMixin() in its componentDidUpdate and either shows or hides the tooltip.

    A few months later, someone wanted to make the orientation of the prompt configurable. To avoid code duplication, they added the getTooltipOptions() method to TooltipMixin. As a result, after a while, you need to display multiple tooltips in the same component, no longer hover, or some other functionality, and you need to decouple HoverMixin() from TooltipMixin. In addition, if many components use a mixin, the functionality added to the mixin will be added to all components, even though many components don’t need the functionality at all.

    Gradually, the boundaries of encapsulation erode, and since it is difficult to change or remove existing mixins, they become more and more abstract until no one understands how they work.

React officially considers mixins unnecessary and problematic in the React codebase. It is recommended that developers use higher-order components for reuse of component logic.

2. HOC

The React documentation defines HOC as follows: 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.

In short, a higher-order component is a function that takes a component as an argument and returns a new component.

A higher-order component definition looks like this:

// Take a component WrappedComponent as an argument and return a new component Proxy
function withXXX(WrappedComponent) {
    return class Proxy extends React.Component {
        render() {
            return <WrappedComponent {. this.props} >}}}Copy the code

When developing a project, you find that different components have similar logic, or you find yourself writing duplicate code, this is the time to consider component reuse.

Here I take an example of actual development to illustrate that all major apps are adapting to the dark mode recently, and the background color and font color in the dark mode are definitely different from the normal mode. Then you need to listen for dark mode on/off events, and each UI component needs to be styled according to the current mode.

It is definitely not possible for each component to listen for event changes to setState, as it would result in multiple renders.

So we’re going to use the Context API to do that, so I’m going to use the new context API as an example. If you use the old context API to implement this function, you need to use the publish and subscribe mode. Finally, use unstable_batchedUpdates provided by react-native/react-dom to update the function. Avoid multiple renders (the old Context API if the component’s shouldComponentUpdate returned false when the value changed, its descendants would not be rerendered).

By the way, don’t rush to use many of the new apis in your projects, such as the new Context API. If your React version is 16.3.1 and your React dom version is 16.3.3, you’ll find that when your child components are function components, So if you use context.consumer, you can get the value of the Context, whereas if your component is a class component, you can’t get the value of the Context at all.

The React. ForwardRef also has a multiple rendering bug when used in this version. Blood and tears, no more talking, continue the dark mode this need.

My idea is to mount the current mode (assuming the value is light/dark) to the context. Other components can be retrieved directly from the context. However, we do know that the new ContextAPI function component and the class component, the method of obtaining the context is inconsistent. And there are so many components in a project that doing this once for each component is a repetitive effort. This is where higher-order components come in (PS: React16.8 provides the useContext Hook, which is handy)

Of course, the other reason I use higher-order components here is that our project contains the old Context API (don’t ask me why I don’t just refactor it, there are too many people involved to change it), the old and new context APIS can coexist in a project, However, we cannot use both in the same component. So if a component already uses an old context API, in order to get values from the new context API, you also need to use higher-order components to process it.

So I wrote a prototype of a higher-order component withColorTheme (here you can also think of withColorTheme as a higher-order function that returns higher-order components) :

import ThemeContext from './context';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        return class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {. this.props} colortheme={this.context}/>)}}}}Copy the code

Package display name

There are a few problems with this prototype. First, we didn’t show the name of the ProxyComponent wrapper, so we added:

import ThemeContext from './context';

function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {. this.props} colortheme={this.context}/>)
            }
        }
    }
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    return ProxyComponent;
}
Copy the code

Let’s take a look at the difference between unwrapped display name and wrapped display name:

React Developer Tools for debugging

ReactNative’s red screen reported an error

Copy static methods

As we all know, to use HOC to wrap components, you need to copy static methods. If your HOC is only used by a few components, no static methods need to be copied, or the static methods need to be copied are determined, then you can do it manually.

Since the withColorTheme is a higher-order component that will eventually be used by many businesses, there is no way to limit how others write components, so we have to write it generically here.

Hoist-non-react-statics is a dependency that automatically copies non-React static methods. It only copies non-React static methods, not all static methods of the wrapped component. The first time I used this dependency, I didn’t look carefully and thought I was copying all static methods from WrappedComponent to ProxyComponent. PropsTypes. Style undefined is not an object Because I didn’t copy the propTypes manually, I mistakenly assumed that the hoist non-react-statics would handle it for me.

The source code for the hoist non-react-Statics is very short, so if you’re interested, check out the 3.3.2 version I’m currently using.

As a result, Such as ChildContextTypes, contextType, contextTypes, defaultProps, displayName, getDefaultProps, getDerivedStateFromError, getDerivedS TateFromProps mixins, propTypes, types, etc. are not copied, which is easy to understand because proxyComponents may also need to set these things and cannot be overridden.

import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {. this.props} colortheme={this.context}/>) } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; // contextTypes and childContextTypes return ProxyComponent; }Copy the code

That seems close enough now, but HOC still has a problem with ref delivery. If nothing is done, we get the ProxyComponent instance from ref, not the WrappedComponent instance we would have wanted.

Ref to pass

Although we have pass-through with unrelated props, keys and refs are not ordinary prop and React will handle them specially.

So here we need to do something special for ref. If your reac-DOM is 16.4.2 or your React-native version is 0.59.9 or higher, then the react. forwardRef is the easiest way to use the forwardRef.

Use the React. ForwardRef forwarder

import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                const{ forwardRef, ... wrapperProps } =this.props;
                return<WrappedComponent {... wrapperProps} ref={forwardRef} colorTheme={ this.context } /> } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; //contextType contextTypes and childContextTypes because I don't need them here, ForwardRef (options. ForwardRef) {let forwardRef = forwardRef((props, ref)) => (<ProxyComponent {... props} forwardRef={ref} /> )); forwarded.displayName = displayName; forwarded.WrappedComponent = WrappedComponent; forwarded.propTypes = WrappedComponent.propTypes; return hoistNonReactStatics(forwarded, WrappedComponent); } else { return hoistNonReactStatics(ProxyComponent, WrappedComponent); }}Copy the code

Let’s say we decorate TextInput like export Default withColorTheme({forwardRef: true})(TextInput).


this. TextInput = v}>

To get an instance of WrappedComponent, just call this.textInput as before using the withColorTheme.

Call getWrappedInstance with the method

import ThemeContext from './context';
import hoistNonReactStatics from 'hoist-non-react-statics';
function withColorTheme(options={}) {
    return function(WrappedComponent) {
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;

            getWrappedInstance = (a)= > {
                if (options.forwardRef) {
                    return this.wrappedInstance;
                }
            }

            setWrappedInstance = (ref) = > {
                this.wrappedInstance = ref;
            }

            render() {
                const{ forwardRef, ... wrapperProps } =this.props;
                letprops = { ... this.props };if (options.forwardRef) {
                    props.ref = this.setWrappedInstance;
                }
                return<WrappedComponent {... props} colorTheme={ this.context } /> } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; //contextType contextTypes and childContextTypes because I don't need them here, ForwardRef (options. ForwardRef) {let forwardRef = forwardRef((props, ref)) => (<ProxyComponent {... props} forwardRef={ref} /> )); forwarded.displayName = displayName; forwarded.WrappedComponent = WrappedComponent; forwarded.propTypes = WrappedComponent.propTypes; return hoistNonReactStatics(forwarded, WrappedComponent); } else { return hoistNonReactStatics(ProxyComponent, WrappedComponent); }}Copy the code

Similarly, we decorate TextInput with export Default withColorTheme({forwardRef: true})(TextInput).


this. TextInput = v}>

If you want to get WrappedComponent instance, you need to through this. (see the getWrappedInstance () packaged components (for example.

Maximized composability

Let me start by saying why I designed it like this:

function withColorTheme(options={}) {
    function(WrappedComponent) {}}Copy the code

Instead of something like this:

function withColorTheme(WrappedComponent, options={}) {}Copy the code

React-redux is also used in many services:

@connect(mapStateToProps, mapDispatchToProps)
@withColorTheme()
export default class TextInput extends Component {
    render() {}
}
Copy the code

In this way, the original code structure can be designed without breaking. Otherwise, businesses that use decorator syntax are a bit cumbersome to change.

Go back to maximum composability and see what the official documentation says:

Single-argument HOC returned by functions like connect(provided by react-redux) has a signature Component => Component. Functions whose output type is the same as their input type can be easily combined.

/ /... You can write composite utility functions
// compose(f, g, h) is equal to (... args) => f(g(h(... args)))
const enhance = compose(
  // These are single-parameter HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
Copy the code

The source code for Compose can be seen in the implementation of Redux, which is very short.

To complicate matters further:

withRouter(connect(commentSelector)(withColorTheme(options)(WrappedComponent)));
Copy the code

Our “enhance” can be written as:

const enhance = compose(
  withRouter,
  connect(commentSelector),
  withColorTheme(options)
)
const EnhancedComponent = enhance(WrappedComponent)
Copy the code

If we were to write XXX(WrappedComponent, options), the above code would become:

const EnhancedComponent = withRouter(connect(withColorTheme(WrappedComponent, options), commentSelector))
Copy the code

Imagine what this code would look like if there were more HOC to use.

HOC conventions and considerations

convention

  • Will not be relevantpropsPass to the wrapped component (HOC should be transparent and independent of itselfprops)
  • Maximize composability
  • The wrapper displays the name for easy debugging

Matters needing attention

  • Don’t inrenderMethod.HOC

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.

This isn’t 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.

  • Be sure to copy static methods
  • RefsWill not be passed (extra processing required)

3. Reverse inheritance

The React documentation states that HOC does not modify incoming components, nor does it use inheritance to replicate their behavior. Instead, HOC makes up new components by wrapping them in container components. HOC is a pure function with no side effects.

So, I don’t think React favors reverse inheritance. Here’s how it works, and it might work in some situations.

Reverse inheritance
function withColor(WrappedComponent) {
    class ProxyComponent extends WrappedComponent {
        // Notice that ProxyComponent overrides functions of the same name as WrappedComponent, including state and props
        render() {
            //React.cloneElement(super.render(), { style: { color:'red' }})
            return super.render(); }}return ProxyComponent;
}
Copy the code

Unlike the previous section, reverse inheritance does not increase the hierarchy of components, and there are no static property copies and refs lost. I can use it for render hijacking, but I don’t currently have any scenarios where I have to use reverse inheritance.

It doesn’t have static attributes and refs, and it doesn’t add hierarchy, but it’s also not very useful and overrides properties and methods with the same name, which is frustrating. In addition, although you can modify the render result, it is not easy to inject props.

4. render props

First, render props is a simple technique for sharing code between React components using a prop with a value of function.

Components with Render Prop accept a function that returns a React element and calls it instead of implementing its own render logic.

<Route {... rest} render={routeProps => (<FadeIn>
            <Component {. routeProps} / >
        </FadeIn>)} / >Copy the code

ReactNative developers, in fact, use a lot of the render props technology, such as the FlatList component:

import React, {Component} from 'react';
import {
    FlatList,
    View,
    Text,
    TouchableHighlight
} from 'react-native';

class MyList extends Component {
    data = [{ key: 1.title: 'Hello' }, { key: 2.title: 'World' }]
    render() {
        return (
            <FlatList
                style={{marginTop: 60}}
                data={this.data}
                renderItem={({ item.index}) = > {
                    return (
                        <TouchableHighlight
                            onPress={()= > { alert(item.title) }}
                        >
                            <Text>{item.title}</Text>
                        </TouchableHighlight>
                    )
                }}
                ListHeaderComponent={() => {
                    return (<Text>Here is a List</Text>)
                }}
                ListFooterComponent={() => {
                   return <Text>No more data</Text>} />)}}Copy the code

For example, the renderItem and ListHeaderComponent of the FlatList are render prop.

Note that Render Prop is called Render Prop because of the mode, and you don’t have to use a prop named Render to use this mode. Render Prop is a function prop that tells components what to render.

In fact, we often apply this technique when encapsulating components. For example, we encapsulate a wheel map component, but the style of each page is not consistent. We can provide a basic style, but also allow customization, otherwise there is no general value:

// Provide a renderPage prop
class Swiper extends React.PureComponent {
    getPages() {
        if(typeof renderPage === 'function') {
            return this.props.renderPage(XX,XXX)
        }
    }
    render() {
        const pages = typeof renderPage === 'function' ? this.props.renderPage(XX,XXX) : XXXX;
        return (
            <View>
                <Animated.View>
                    {pages}
                </Animated.View>
            </View>)}}Copy the code

Matters needing attention

Be careful when using Render Props with react. PureComponent

If you create functions in the Render method, the render props will cancel out the advantages of using the react. PureComponent. Because shallow comparisons of props always give false, and in this case each render will generate a new value for render prop.

import React from 'react';
import { View } from 'react-native';
import Swiper from 'XXX';
class MySwiper extends React.Component {
    render() {
        return (
            <Swiper 
                renderPage={(pageDate, pageIndex) = > {
                    return (
                        <View></View>}} />)}}Copy the code

RenderPage generates a new value every time, and many React optimizations mention this as well. We can define renderPage functions as instance methods, as follows:

import React from 'react';
import { View } from 'react-native';
import Swiper from 'XXX';
class MySwiper extends React.Component {
    renderPage(pageDate, pageIndex) {
        return (
            <View></View>
        )
    }
    render() {
        return (
            <Swiper 
                renderPage={this.renderPage}
            />)}}Copy the code

If you can’t define prop statically,

should extend react.component. there’s no need for shallow comparisons, so don’t waste your time.

5. Hooks

Hook is a new feature in React 16.8 that lets you use state and other React features without writing classes. HOC and Render props work, though

React already has some Hooks built in, such as: UseState, useEffect, useContext, useReducer, useCallback, useMemo, useRef and other hooks. If you are not clear about these hooks, then you can first read the official documentation.

We are focusing on how to reuse component logic using Hooks. Suppose we have a requirement that, in a development environment, we print out the props for the component every time we render.

import React, {useEffect} from 'react';

export default function useLogger(componentName,... params) {
    useEffect((a)= > {
        if(process.env.NODE_ENV === 'development') {
            console.log(componentName, ...params);
        }
    });
}
Copy the code

When using:

import React, { useState } from 'react';
import useLogger from './useLogger';

export default function Counter(props) {
    let [count, setCount] = useState(0);
    useLogger('Counter', props);
    return (
        <div>
            <button onClick={()= > setCount(count + 1)}>+</button>
            <p>{`${props.title}, ${count}`}</p>
        </div>)}Copy the code

In addition, the official document custom Hook section also demonstrates step by step how to use hooks for logic reuse. I haven’t applied hooks in my project due to version restrictions, although the documentation has been reviewed several times. Will Hook be used to replace render props and HOC?

Typically, the render props and higher-order components render only one child node. We thought it would be easier to have hooks serve this usage scenario. These two patterns can still be used, for example, for properties such as renderItem of a FlatList component, or for a visible container component that might have its own DOM structure. But in most scenarios, hooks are sufficient and can help reduce nesting.

The most annoying aspect of HOC is hierarchy nesting. If the project is developed based on the new version, Hook should be given priority when logic reuse is needed. If the requirements cannot be realized, then render props and HOC should be used to solve the problem.

Refer to the link

  • Mixins Considered Harmful
  • High order component
  • Customize the Hook
  • Hooks FAQ

Finally, if it’s convenient, click on the Star to encourage: github.com/YvetteLau/B…