SRP of 7 Criteria for Reliable React Component Design

  html5, javascript, node.js

Translation: Liu Xiaoxi

Original link:https://dmitripavlutin.com/7- …

The length of the original textVeryLong, but the content is too attractive to me, still can’t help but to translate it. This article is very helpful for writing reusable and maintainable React components. But because of the length is too long, I had to split, this article focuses onSRP, that is, the principle of single responsibility.

I am a dividing line.

More articles to stamp: https://github.com/YvetteLau/ …

I like React component development. You can divide a complex user interface into components and take advantage of the reusability of components and abstract DOM operations.

Component-based development is efficient: a complex system is built by specialized and easy-to-manage components. However, only well-designed components can ensure the benefits of combination and reuse.

Despite the complexity of the application, in order to meet deadlines and unexpected changes, you must keep walking on the fine line of architectural correctness. You must separate the components into individual tasks and have them tested well.

Unfortunately, it is always easier to follow the wrong path: writing large components with many responsibilities, tightly coupled components, forgetting unit tests. These increase the technical debt, making it more and more difficult to modify existing functions or create new ones.

When writing React applications, I often ask myself:

  • How to construct components correctly?
  • When should a large component be split into smaller components?
  • How do you design to prevent communication between tightly coupled components?

Fortunately, reliable components have common characteristics. Let’s study these 7 useful criteria (this article only elaboratesSRP, the remaining criteria are on the way), and will be detailed into the case study.

Single responsibility

When a component has only one reason for change, it has a single responsibility.

The basic principle to consider when writing React components is the principle of single responsibility. The principle of single responsibility (abbreviation:SRP) requires a component to have one and only one reason to change.

The responsibility of a component can be to present a list, or to display a date selector, or to issue aHTTPRequest, or draw a chart, or delay loading images, etc. Your component should select only one responsibility and implement it. When you modify the way a component implements its responsibilities (for example, change the limit on the number of rendered lists), it has a reason to change.

Why is it important that only one reason can change? Because modification of such components is isolated and controlled. The principle of single responsibility regulates the size of components to focus on one thing. Components that focus on one thing are easy to code, modify, reuse, and test.

Let’s give a few examples

Example 1: A component acquires remote data. Accordingly, when acquiring logical changes, it has a reason for the changes.

The reason for the change is:

  • Modify server URL
  • Modify response format
  • To use a different HTTP request library
  • Or any modification related only to the acquisition logic.

Example 2: The table component maps the data array to the row component list, so there is one reason to change when the mapping logic changes.

The reason for the change is:

  • You need to limit the number of render line components (for example, display up to 25 lines)
  • When there are no items to be displayed, the prompt message “list is empty” is required to be displayed.
  • Or any modification related only to the mapping of array to row components.

Do your components have many responsibilities? If the answer is “yes”, the component is divided into several blocks according to each individual responsibility.

If you find SRP a little fuzzy, please readThis article.
Modules written in the early stages of the project will change frequently until they reach the release stage. These changes usually require components to be easily modified in isolation: this is also the goal of SRP.

1.1 Trap of Multiple Responsibilities

A common problem occurs when a component has multiple responsibilities. At first glance, this seems harmless and requires less work:

  • You start coding immediately: there is no need to identify responsibilities and plan the structure accordingly
  • One large component can do all this: there is no need to create components for each responsibility.
  • No split-no overhead: no need to create communication between split componentspropsAndcallbacks

This childish structure is easy to code at the beginning. However, with the increase and complexity of applications, difficulties will arise in future modifications. There are many reasons for changes in components that implement multiple responsibilities at the same time. The main problem now is that changing a component for some reason will inadvertently affect other responsibilities implemented by the same component.

Don’t turn off the light switch, because it also acts on the elevator.

This design is veryFragile. Unexpected side effects are difficult to predict and control.

For example,<ChartAndForm>At the same time, they have two responsibilities: to draw a chart and to process the form that provides data for the chart.<ChartandForm>There are two reasons for the change: drawing a chart and processing a form.

When you change a form field (e.g.<input>Revise to<select>When, you accidentally interrupt the rendering of the chart. In addition, the diagram implementation is not reusable because it is coupled with the form details.

To solve the problem of multiple responsibilities, it is necessary to<ChartAndForm>Split into two components:<Chart>And<Form>. Each component has only one responsibility: drawing charts or processing forms. Communication between components is throughpropsImplementation.

The worst case of the problem of multiple responsibilities is the so-called God component (analogy of God’s object). God’s component tends to understand and do everything. You may see it is called<Application><Manager><Bigcontainer>Or<Page>The code exceeds 500 lines.

With the help of the combination, it conforms to SRP, thus decomposing God’s components. (composition) is a way to create larger components by joining components together. Combination is the core of React. )

1.2 Case Study: Make Components Have Only One Responsibility

Imagine a component issuing to a specialized serverHTTPRequest to get the current weather. When the data is successfully obtained, the component uses the response to display weather information:

import axios from 'axios';
// 问题: 一个组件有多个职责
class Weather extends Component {
    constructor(props) {
        super(props);
        this.state = { temperature: 'N/A', windSpeed: 'N/A' };
    }

    render() {
        const { temperature, windSpeed } = this.state;
        return (
            <div className="weather">
                <div>Temperature: {temperature}°C</div>
                <div>Wind: {windSpeed}km/h</div>
            </div>
        );
    }

    componentDidMount() {
        axios.get('http://weather.com/api').then(function (response) {
            const { current } = response.data;
            this.setState({
                temperature: current.temperature,
                windSpeed: current.windSpeed
            })
        });
    }
}

When dealing with similar situations, ask yourself: Do you have to split components into smaller components? You can best answer this question by determining how components may change according to their responsibilities.

There are two reasons for this weather component change:

  1. componentDidMount()hit the targetfetchLogic: Server URL or response format may change.
  2. render()Weather Display in: The way components display weather can be changed many times.

The solution is to<Weather>It is divided into two components: each component has only one responsibility. Named<WeatherFetch>And<WeatherInfo>.

<WeatherFetch>The component is responsible for acquiring weather, extracting response data and saving it tostateChina. There is only one reason for its change: the logical change of acquiring data.

import axios from 'axios';
// 解决措施: 组件只有一个职责就是请求数据
class WeatherFetch extends Component {
    constructor(props) {
        super(props);
        this.state = { temperature: 'N/A', windSpeed: 'N/A' };
    }

    render() {
        const { temperature, windSpeed } = this.state;
        return (
            <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
        );
    }

    componentDidMount() {
        axios.get('http://weather.com/api').then(function (response) {
            const { current } = response.data;
            this.setState({
                temperature: current.temperature,
                windSpeed: current.windSpeed
            });
        });
    }
}

What are the benefits of this structure?

For example, you want to useasync/awaitGrammar to replacepromiseGo to the server to get a response. Reason for Change: Modify Acquisition Logic

// 改变原因: 使用 async/await 语法
class WeatherFetch extends Component {
    // ..... //
    async componentDidMount() {
        const response = await axios.get('http://weather.com/api');
        const { current } = response.data;
        this.setState({
            temperature: current.temperature,
            windSpeed: current.windSpeed
        });
    }
}

Because ..<WeatherFetch>There is only one reason for change: modificationfetchLogic, so any modifications to this component are isolated. Useasync/awaitIt will not directly affect the weather display.

<WeatherFetch>Render<WeatherInfo>. The latter is only responsible for displaying the weather, and the reason for the change may only be the visual display change.

// 解决方案: 组件只有一个职责,就是显示天气
function WeatherInfo({ temperature, windSpeed }) {
    return (
        <div className="weather">
            <div>Temperature: {temperature}°C</div>
            <div>Wind: {windSpeed} km/h</div>
        </div>
    );
}

Let’s change it<WeatherInfo>If not displayed“wind:0 km/h”It shows“wind:calm”. This is why the visual display of the weather has changed:

// 改变原因: 无风时的显示
function WeatherInfo({ temperature, windSpeed }) {
    const windInfo = windSpeed === 0 ? 'calm' : `${windSpeed} km/h`;
    return (
        <div className="weather">
            <div>Temperature: {temperature}°C</div>
            <div>Wind: {windInfo}</div>
        </div>
    );
}

Similarly, yes<WeatherInfo>The modification of is isolated and will not affect<WeatherFetch>Components.

<WeatherFetch>And<WeatherInfo>Have their own responsibilities. Changes in one component have little effect on the other. This is the role of the principle of single responsibility: modifying isolation will have a slight and predictable impact on other components of the system.

1.3 Case Study: HOC Prefers Single Responsibility Principle

Using a combination of segmented components by responsibility does not always help to follow the principle of single responsibility. Another effective practice is efficient components (abbreviated asHOC)

A high-order component is a function that accepts a component and returns a new component.

HOCA common use of is to add new attributes to encapsulated components or modify existing attribute values. This technique is called attribute proxy:

function withNewFunctionality(WrappedComponent) {
    return class NewFunctionality extends Component {
        render() {
            const newProp = 'Value';
            const propsProxy = {
                ...this.props,
                // 修改现有属性:
                ownProp: this.props.ownProp + ' was modified',
                // 增加新属性:
                newProp
            };
            return <WrappedComponent {...propsProxy} />;
        }
    }
}
const MyNewComponent = withNewFunctionality(MyComponent);

You can also control the rendering results by controlling the rendering process of the input components. suchHOCTechnology is called Rendering Hijacking:

function withModifiedChildren(WrappedComponent) {
    return class ModifiedChildren extends WrappedComponent {
        render() {
            const rootElement = super.render();
            const newChildren = [
                ...rootElement.props.children,
                // 插入一个元素
                <div>New child</div>
            ];
            return cloneElement(
                rootElement,
                rootElement.props,
                newChildren
            );
        }
    }
}
const MyNewComponent = withModifiedChildren(MyComponent);

If you want to learn more about HOCS practices, I suggest you read “Deep Response Advanced Components”.

Let’s take an example to see how HOC’s attribute proxy technology helps to separate responsibilities.

Component<PersistentForm>ByinputInput boxes and buttonssave to storageComposition. After changing the input value, clicksave to storageButton to write it tolocalStorageChina.

inputThe state of is inhandlechange(event)Method. Click on the button, the value will be saved to local storage, inhandleclick()Medium processing:

class PersistentForm extends Component {
    constructor(props) {
        super(props);
        this.state = { inputValue: localStorage.getItem('inputValue') };
        this.handleChange = this.handleChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
    }

    render() {
        const { inputValue } = this.state;
        return (
            <div className="persistent-form">
                <input type="text" value={inputValue}
                    onChange={this.handleChange} />
                <button onClick={this.handleClick}>Save to storage</button>
            </div>
        );
    }

    handleChange(event) {
        this.setState({
            inputValue: event.target.value
        });
    }

    handleClick() {
        localStorage.setItem('inputValue', this.state.inputValue);
    }
}

Unfortunately:<PersistentForm>There are 2 responsibilities: manage form fields; Will lose if only savedlocalStorage.

Let’s reconstruct it<PersistentForm>Component to have only one responsibility: to show form fields and additional event handlers. It should not know how to use storage directly:

class PersistentForm extends Component {
    constructor(props) {
        super(props);
        this.state = { inputValue: props.initialValue };
        this.handleChange = this.handleChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
    }

    render() {
        const { inputValue } = this.state;
        return (
            <div className="persistent-form">
                <input type="text" value={inputValue}
                    onChange={this.handleChange} />
                <button onClick={this.handleClick}>Save to storage</button>
            </div>
        );
    }

    handleChange(event) {
        this.setState({
            inputValue: event.target.value
        });
    }

    handleClick() {
        this.props.saveValue(this.state.inputValue);
    }
}

The component receives the stored input value from the initial value of the attribute and uses the attribute functionsaveValue(newValue)To save the input value. ThesepropsBy using attribute proxy technologywithpersistence()Provided by HOC.

Now<PersistentForm>accord withSRP. The only reason for the change is to modify the form fields.

The responsibility for querying and saving to local storage consists ofwithPersistence()Ad HOC commitment:

function withPersistence(storageKey, storage) {
    return function (WrappedComponent) {
        return class PersistentComponent extends Component {
            constructor(props) {
                super(props);
                this.state = { initialValue: storage.getItem(storageKey) };
            }

            render() {
                return (
                    <WrappedComponent
                        initialValue={this.state.initialValue}
                        saveValue={this.saveValue}
                        {...this.props}
                    />
                );
            }

            saveValue(value) {
                storage.setItem(storageKey, value);
            }
        }
    }
}

withPersistence()Is aHOC, its responsibility is lasting. It does not know any details about the form field. It focuses on only one task: providing for incoming componentsinitialValueString sumsaveValue()Function.

will<PersistentForm>Andwithpersistence()Use together to create a new component<LocalStoragePersistentForm>. It is connected to local storage and can be used in applications:

const LocalStoragePersistentForm
    = withPersistence('key', localStorage)(PersistentForm);

const instance = <LocalStoragePersistentForm />;

As long as<PersistentForm>Correct useinitialValueAndsaveValue()Property, any modification to this component cannot be destroyed.withPersistence()Logic saved to storage.

And vice versa: as long aswithPersistence()Provide correctinitialValueAndsaveValue()YesHOCAny modification of cannot destroy the way form fields are processed.

The efficiency of SRP is shown again: the isolation is modified to reduce the impact on other parts of the system.

In addition, code reusability will also increase. You can send any other form<MyOtherForm>Connect to local storage:

const LocalStorageMyOtherForm
    = withPersistence('key', localStorage)(MyOtherForm);

const instance = <LocalStorageMyOtherForm />;

You can easily change the storage type tosession storage

const SessionStoragePersistentForm
    = withPersistence('key', sessionStorage)(PersistentForm);

const instance = <SessionStoragePersistentForm />;

Initial version<PersistentForm>There is no benefit of isolating modifications and reusability because it mistakenly has multiple responsibilities.

In the case of bad block combination, attribute proxy and rendering hijackHOCTechnology can make components have only one responsibility.

Thank you for your friends’ willingness to spend precious time reading this article. If this article gives you some help or inspiration, please don’t be stingy with your praise and Star. Your affirmation is my greatest motivation to move forward. https://github.com/YvetteLau/ …

I recommend paying attention to my public number.

clipboard.png