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
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
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
<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,
It seems that
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().
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
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
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