The original from: https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/#6testableandtested

A component is “single responsibility” when there is only one reason to change it

The SRP-Single responsibility principle is the foundation of writing the React component.

Duties might mean rendering a list, displaying a time picker, making an HTTP request, drawing a diagram, lazily loading an image, etc. Components should choose only one responsibility to implement. When you modify the unique responsibility that the component implements (such as limiting the number of items in the rendered list), the component changes accordingly.

Why is “only one reason to change” so important? This is because component changes are isolated and controlled.

A single responsibility limits the size of the component and allows it to focus on one thing. This facilitates coding as well as later modification, reuse, and testing.

Let me give you a couple of examples.

Example 1: A component that requests and processes remote data changes only because the request logic sends changes, including:

  • The server URL has been modified
  • The format of the response data has been modified
  • A different HTTP request library
  • Other changes are only related to the request logic

Example 2: A table component that maps an array of row components changes only because the mapping logic changes:

  • There is a requirement to limit the maximum number of rendered lines, say 25
  • A text prompt is required when there are no lines to render
  • Other changes are only related to the mapping between arrays and components

Does your component have multiple responsibilities? If the answer is yes, break it up into components with a single responsibility.

Units of code written in the early stages are frequently modified before a project is released. These components need to be easily isolated and modified — this is what SRP is all about.

1. The pitfalls of multiple responsibilities

Situations where a component has multiple responsibilities are often overlooked, which at first glance seems appropriate and understandable:

  • You can just roll up your sleeves and write code: you don’t have to differentiate responsibilities, you don’t have to map out the structure
  • It forms a hodgepodge of components
  • You do not create props and callback functions for communication between separated components

This naive structure is very simple in its coding. Trouble arises when applications grow and become more complex, requiring component changes.

There are many reasons to change a component that has multiple responsibilities; Then the main problem comes up: changing components for one reason can easily compromise other responsibilities.

Such a design is fragile. Unintended side effects are extremely difficult to predict and control.

For example,

is responsible for drawing the chart and also for processing the form that feeds the chart. Then

has two reasons to change: the drawing and the form.

When you change form fields (such as to

To solve the multi-responsibility problem, you need to split

into

and

components. Each has a single responsibility: drawing charts or processing forms accordingly. Communication between the two components is done through props.


The extreme case of the multi-responsibility problem is known as the “God component of the antipattern.” A God component that wants to know everything in your Application, you’ll usually see a component named

,

,

, or with more than 500 lines of code.


For god components, they should be split and combined to conform to SRP.

2. Case study: Make the component have a single responsibility

Imagine a component that sends an HTTP request to a specified server to query the current weather. When the request is successful, the same component displays weather conditions using the data in the response.

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

Whenever dealing with this type of problem, ask yourself: Do I have to break the component into smaller pieces? Determining how components change according to their responsibilities provides the best answer to these questions.

This weather component changes for two reasons:

  • Request logic in componentDidMount() : The server URL or response format may be modified
  • Weather visualization in Render () : The way a component displays weather can change many times

The solution is to split

into two components, each with its own unique responsibility. Name them

and

, respectively.


The first component,

, is responsible for getting the weather, extracting the response data, and storing it in state. Only the fetch logic causes it to change:

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

What good comes out of this?

For example, you might like to use async/await syntax instead of promises to handle server responses. This is one reason the fetch logic changes:

// Reason for change: Class WeatherFetch extends Component {//..... with async/await syntax // asynccomponentDidMount() {
     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

only changes because of the FETCH logic, any changes to it don’t affect anything else. Using async/await does not directly affect the way the weather is displayed.

While

renders

, which is only responsible for displaying the weather, for visual reasons only:

// Solution: The component's responsibility is simply to display the weatherfunction WeatherInfo({ temperature, windSpeed }) {  
   return (
     <div className="weather"> <div>Temperature: {Temperature}°C</div> <div>Wind: {windSpeed} km/h</div> </div>); }Copy the code

Change “Wind: 0 km/h” in

to display “Wind: calm” :

// Reason to change: handle calm wind  
function WeatherInfo({ temperature, windSpeed }) {  
   const windInfo = windSpeed === 0 ? 'calm' : `${windSpeed} km/h`;
   return (
     <div className="weather"Temperature: {Temperature}°C</div> <div>Wind: {windInfo}</div> </div>); }Copy the code

Again, this change to

is independent and does not affect

.


and

do their jobs. Changes to each component are minimal to the others. This is the power of the single responsibility principle: changes are insulated so that the impact on other components of the system is small and predictable.

3. Case study: HOC style single responsibility principle

Grouping divided components together by responsibility does not always meet the single responsibility principle. Another efficient approach, called hoc-higher Order Component, may be more appropriate:

HOC is a function that takes a component as an argument and returns a new component

A common use of HOC is to add additional props or modify existing props for wrapped components. This technique is called the props Proxy:

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

You can even form a new render mechanism by replacing elements rendered by wrapped components. This HOC technology is called Render highjacking:

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

If you want to learn more about HOC, read the article recommended at the end.

Here’s an example of how HOC’s attribute broker technology can help us achieve a single responsibility.

The component consists of an input box and a button that is responsible for saving to storage. The value of the input box is read and stored locally.

<div id="root"></div>
Copy the code
class PersistentForm extends React.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>
        <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);
  }
}

ReactDOM.render(<PersistentForm />, document.getElementById('root'));
Copy the code

When the input changes, the component state is updated in the handleChange(Event); When the button is clicked, the value is stored locally in handleClick().

Unfortunately, has two responsibilities: managing form data and storing input values locally.

It seems that should not have a second responsibility, namely that it should not be concerned with manipulating local storage directly. The idea is to optimize the component for a single responsibility: rendering the form field, with event handlers attached.

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 accepts the initial input value initialValue from the property and stores the value of the input via the saveValue(newValue) function also passed from the property; These two properties are provided by HOC, a property broker called withPersistence().

now complies with SRP. A change to the form is the only reason it changes.

The responsibility for querying and storing to local storage has been shifted to 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 responsible for persistence; It doesn’t know any of the details of the form and instead focuses on one job: providing the initialValue string and saveValue() function for the wrapped component.

The < PersistentForm > and withPersistence () together to create a new component < LocalStoragePersistentForm > :

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

const instance = <LocalStoragePersistentForm />;  
Copy the code

As long as uses the initialValue and saveValue() properties correctly, any modifications to itself cannot break the local storage-related logic held by withPersistence(), and vice versa.

This again demonstrates the power of SRP: it isolates changes from each other and has little impact on the rest of the system.

In addition, the code is more reusable. With other

components, you can also implement persistence logic:

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

const instance = <LocalStorageMyOtherForm />;  
Copy the code

You can also easily change the storage mode to sessionStorage:

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

const instance = <SessionStoragePersistentForm />;  
Copy the code

Isolation of changes and reusability traversal were not present in the original multi-responsibility component.

In situations where composition doesn’t work, HOC property proxies and rendering hijacks often help components achieve a single responsibility.

Read more:

  • “With the principle of SOLID React components development position” : mp.weixin.qq.com/s/jxdMzD3sm…
  • The in-depth React higher-order components: mp.weixin.qq.com/s/dtlrOGTjo…


(end)


Please indicate the source of reprint



Long press the QR code or search fewelife to follow us