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 components
props
和callbacks
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
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
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:
-
Fetch logic in componentDidMount() : The server URL or response format may change.
-
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
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,
Let’s refactor the
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.
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
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
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…