Translation: Liu Xiaoxi

Original link: dmitripavlutin.com/7-architect…

The original text is very long, but the content is so fascinating that I cannot resist translating it. This article is very helpful for writing reusable and maintainable React components. However, because of the length of the article, I had to break it down and focus on SRP, the single responsibility principle.

More articles can be read: github.com/YvetteLau/B…

———————————— I am a dividing line ————————————

I like the React component development approach. You can split a complex user interface into components, taking advantage of component reusability and abstract DOM manipulation.

Component-based development is efficient: a complex system is built from specialized, easily managed components. However, only well-designed components can ensure the benefits of composition and reuse.

Despite the complexity of your application, you must constantly walk the fine line of architectural correctness in order to meet deadlines and unexpected changes in requirements. You have to separate components to focus on a single task and be well tested.

Unfortunately, it’s always easier to follow the wrong path: write large components with lots of responsibilities, tightly coupled components, and forget unit tests. These add to the technical debt, making it increasingly difficult to modify existing features or create new ones.

When writing the React application, I often ask myself:

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

Fortunately, reliable components have common characteristics. Let’s take a look at these seven useful standards (this article covers SRP only; the rest of the criteria are on their way) and break them down into case studies.

Single responsibility

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

The basic rule to consider when writing the React component is the single responsibility principle. The single responsibility principle (SRP) requires that components have one and only one reason for change.

A component’s responsibility could be to render a list, or display a date picker, or make an HTTP request, or draw a chart, or lazy-load an image, and so on. Your component should only choose one responsibility and implement it. When you modify the way a component implements its responsibilities (for example, changing the number of lists that are rendered), it has a reason to change.

Why is it important to have only one reason to change? This is because changes to components are isolated and controlled. The single responsibility principle limits the size of components so that they focus on one thing. Components that focus on one thing are easy to code, modify, reuse, and test.

Let’s take a few examples

Example 1: A component retrieves remote data, and accordingly, when it retrieves a logical change, it has a reason to change.

The reasons for the change are:

  • Changing the SERVER URL
  • Modifying the response Format
  • Use other HTTP request libraries
  • Or just any changes related to the fetch logic.

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

The reasons for the change are:

  • You need to limit the number of render line components (e.g., maximum 25 lines)
  • Ask to display “List is empty” message when there is no item to display
  • Or just any changes related to the mapping of array to row components.

Does your component have many responsibilities? If the answer is yes, divide the component into chunks for each individual responsibility.

If you find SRP a little vague, read this article. Units written early in the project will change frequently until the release phase is reached. These changes typically require components to be easily modified in isolation: this is also the goal of SRP.

1.1 The Multiple Responsibilities Trap

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

  • You start coding right away: no need to identify responsibilities and plan the structure accordingly
  • A large component can do all this: there is no need to create components for each responsibility
  • No split – No overhead: No need to create for communication between split componentspropscallbacks

This naive structure is easy to code in the beginning. However, as the application grows and becomes more complex, difficulties arise in subsequent modifications. Components that implement multiple responsibilities at the same time have many reasons to change. The main problem now is that changing a component for whatever reason can inadvertently affect other responsibilities implemented by the same component.

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

The design is fragile. Unexpected side effects are hard to predict and control.

For example,

has two responsibilities at the same time, drawing a chart and processing the form that feeds that chart.

is changed for two reasons: to draw diagrams and to process forms.

When you change a form field (for example, changing to

Solving the multiple responsibilities problem requires that

be split into two components:

and

. Each component has only one responsibility: to draw diagrams or process forms. Communication between components is implemented through props.


The worst case of the multiple liability problem is the so-called God component (god object analogy). The God component tends to know and do everything. You might see it named

,

,

, or with more than 500 lines of code.


The God component is decomposed by making it conform to SRP with the help of composition. Composition is a way to create larger components by joining components together. Composition is at the heart of React.)

1.2 Case study: Make the component have only one responsibility

Imagine a component making an HTTP request to a dedicated server to get the current weather. When the data is successfully retrieved, the component uses the response to display the weather information:

import axios from 'axios';
// Problem: a component has multiple responsibilities
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 }) }); }}Copy the code

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

This weather component changes for two reasons:

  1. Fetch logic in componentDidMount() : The server URL or response format may change.

  2. Weather display in Render () : The way the component displays weather can be changed multiple times.

The solution is to split

into two components: each component has only one responsibility. Name it

and

.


The

component is responsible for getting the weather, extracting the response data, and saving it to state. The only reason it changes is because the logic of getting the data changes.

import axios from 'axios';
// Solution: The component has only one responsibility: to request data
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 }); }); }}Copy the code

What are the benefits of this structure?

For example, you want to use async/await syntax instead of promise to get a response from the server. Reason for change: The acquisition logic was modified

// Reason for change: use async/await syntax
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 }); }}Copy the code

Because

has only one reason to change: to modify the FETCH logic, any changes to this component are isolated. Using async/await does not directly affect the weather display.


Render

. The latter is only responsible for displaying the weather, which can only be changed because of a visual display change.

// Solution: The component has only one responsibility, which is to display the weather
function WeatherInfo({ temperature, windSpeed }) {
    return (
        <div className="weather">
            <div>Temperature: {Temperature} ° C</div>
            <div>Wind: {windSpeed} km/h</div>
        </div>
    );
}
Copy the code

Let’s change

to show “wind:0 km/h” instead of “wind:0 km/h” instead of “wind:calm”. This is where the weather visual changes:

// Reason for change: display when there is no wind
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>
    );
}
Copy the code

Again, changes to

are isolated and do not affect the

component.


and

have their respective responsibilities. A change in one component has little effect on another. This is where the single responsibility principle comes in: modify isolation, and the impact on other components of the system is slight and predictable.

1.3 Case study: HOC prefers the single liability principle

Using a combination of partitioned components by responsibility does not always help to follow the single responsibility principle. Another useful practice is higher-order components (HOC for short)

A higher-order component is a function that takes a component and returns a new component.

A common use of HOC is to add new attributes to encapsulated components or modify existing attribute values. This technique is called property brokering:

function withNewFunctionality(WrappedComponent) {
    return class NewFunctionality extends Component {
        render() {
            const newProp = 'Value';
            constpropsProxy = { ... this.props,// Modify existing attributes:
                ownProp: this.props.ownProp + ' was modified'.// Add new attributes:
                newProp
            };
            return <WrappedComponent {. propsProxy} / >;
        }
    }
}
const MyNewComponent = withNewFunctionality(MyComponent);
Copy the code

You can also control the rendering results by controlling the rendering process of the input components. This HOC technology is called rendering hijacking:

function withModifiedChildren(WrappedComponent) { return class ModifiedChildren extends WrappedComponent { render() { const rootElement = super.render(); Const newChildren = [... rootElement. Props. Children, / / insert an element < div > New child < / div >]; return cloneElement( rootElement, rootElement.props, newChildren ); } } } const MyNewComponent = withModifiedChildren(MyComponent);Copy the code

If you want to learn more about HOCS practices, I recommend you read “Response to Advanced Components in Depth.”

Let’s look at an example of how HOC’s attribute broker technology helps separate responsibilities.

The component consists of the Input box and the save to storage button. After changing the input value, click the Save to Storage button to write it to localStorage.


Copy the code

The state of the input is updated in the handlechange(Event) method. Click the button and the value is saved to local storage, handled in Handleclick () :

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); }}Copy the code

Unfortunately, has two responsibilities: managing form fields; Save the input only in localStorage.

Let’s refactor the component so that it has only one responsibility: to display form fields and an additional event handler. 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); }}Copy the code

The component receives the stored input value from the property initial value and uses the property function saveValue(newValue) to hold the input value. These props are provided by WithPersistence () HOC using property broker technology.

now complies with SRP. The only reason for the change is to modify the form field.

The responsibility for querying and saving to local storage is taken up by withPersistence() HOC:

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); }}}}Copy the code

WithPersistence () is a HOC whose responsibility is to persist. It does not know any details about the form fields. It focuses on just one job: providing the initialValue string and saveValue() function to the component that is passed in.

The < PersistentForm > and withpersistence () are used 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 />;
Copy the code

As long as uses the initialValue and saveValue() properties correctly, any changes to this component cannot break the withPersistence() logic to store.

The reverse is also true: as long as withPersistence() provides the correct initialValue and saveValue(), any modifications to HOC cannot break the way form fields are handled.

The efficiency of SRP is again apparent: modified isolation to reduce impact on the rest of the system.

In addition, the code becomes more reusable. You can connect any other form

to local storage:

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

const instance = <LocalStorageMyOtherForm />;
Copy the code

You can easily change the storage type to Session Storage:

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

const instance = <SessionStoragePersistentForm />;
Copy the code

The original version does not isolate changes and reusability benefits because it mistakenly has multiple responsibilities.

In the case of bad chunking, attribute brokering and render hijacking HOC techniques can make components have only one responsibility.

Thank you for your precious time to read this article. If this article gives you some help or inspiration, please do not spare your praise and Star. Your praise is definitely the biggest motivation for me to move forward. Github.com/YvetteLau/B…

Pay attention to the public number, join the technical exchange group