Let’s talk about designing a more elegant React component from a design-thinking perspective.
The basic principle of
Single responsibility
The principle of single responsibility is to have each module focus on one function, that is, to have as few responsibilities as possible for each module. If a module has too many functions, it should be split into multiple modules for better maintenance of the code.
Just as a person had better focus on one thing and do the best in everything he is responsible for. Components, too, are limited to a suitable granularity that can be reused. If the functionality of a component is too complex and the code volume becomes too large, consider breaking it down into single-responsibility components. Each widget is concerned with its own functionality and can be combined to meet complex requirements.
A single component is easier to maintain and test, but don’t abuse it. Only break it up when necessary. Granularity minimization is an extreme and can lead to a large number of modules.
Divide the boundary
How to split components. If two components are too closely related to logically define their responsibilities, they should not be split. Otherwise their duties are not clear, boundaries are not divided, will produce logical confusion. Then the key to split components is to determine the boundary, through the abstract parameter communication, let each component play its own unique capabilities.
High cohesion/low coupling
High-quality components meet the principles of high cohesion and low coupling.
High cohesion means bringing things together that are logically closely related. In the era of jQuery, we put a functional resource in js, HTML, CSS and other directories. When developing, we need to find relevant logical resources in different directories. Another example is Redux’s suggestion to split actions, reducers, and Store into different places, which disperses a very simple functional logic. This is not very cohesive. Redux aside, the React componentization thinking itself satisfies the principle of high cohesion, namely that a component is a self-contained unit that contains logic/style/structure, and even dependent static resources.
Low coupling refers to reducing dependencies between different components so that each component is as independent as possible. This means that code is written for low coupling. Decouple complex operations by separating responsibilities and dividing boundaries.
Benefits of following basic principles:
- Reduce the complexity of individual components and improve readability
- Reduce coupling, will not affect the whole body
- Improve reusability
- The border is transparent and easy to test
- Clear process, reduce error rate, and easy debugging
Advanced design
Controlled/uncontrolled state
There are two terms that are often used in React form management: controlled input and uncontrolled input. In simple terms, controlled means that the state of the current component becomes the only data source for the form. Indicates that the value of this form is in the control of the current component and can only be updated by setState.
The concept of controlled/uncontrolled is very common in component design. Controlled components are usually paired with value and onChange. Passed to the child component, the child cannot modify the value directly, but can only tell the parent to update it through the onChange callback. Uncontrolled components can pass in the defaultValue property to provide an initial value.
Controlled/Uncontrolled for Modal component Visible:
/ / controlled
<Modal visible={visible} onVisibleChange={handleVisibleChange} />
/ / not controlled
<Modal defaultVisible={visible} />
Copy the code
If this state is the core logic of a component, it should support controlled or compatible uncontrolled modes. If the status is secondary logic, you can optionally support the controlled mode according to the actual situation.
For example, the Select component handles controlled and uncontrolled logic:
function Select(props: SelectProps) {
// Value and onChange are core logic and can be controlled. Compatibility passing defaultValue becomes uncontrolled
// defaultOpen is secondary logic and can be uncontrolled
const { value: controlledValue, onChange: onControlledChange, defaultValue, defaultOpen } = props;
// Uncontrolled mode uses internal state
const [innerValue, onInnerValueChange] = React.useState(defaultValue);
// Secondary logic, select the box to expand the state
const [visible, setVisible] = React.useState(defaultOpen);
// Check whether the parameter is controlled by checking whether it contains a value attribute, even though value is undefined
const shouldControlled = Reflect.has(props, 'value');
// Support controlled and uncontrolled processing
const value = shouldControlled ? controlledValue : innerValue;
const onChange = shouldControlled ? onControlledChange : onInnerValueChange;
// ...
}
Copy the code
Cooperate withhooks
controlled
Whether a component is controlled, usually for its own support, can now be overcome by custom hooks. Complex components that are more handy with hooks.
Encapsulate such a component, put the logic in hooks, and the component itself is hollowed out for rendering primarily with custom hooks.
function Demo() {
// The main logic is in the custom hook
const sheet = useSheetTable();
The component itself receives only one argument, the return value of hook
<SheetTable sheet={sheet} />;
}
Copy the code
The advantage of this is a complete separation of logic and components, which is more conducive to state promotion. You can directly access all states of the sheet, and this mode can be controlled more thoroughly. Simple components may not be suitable for this pattern because they do not have such large controlled requirements, and encapsulation can add some complexity.
Single data source
The single-source principle, in which a state of a component is passed to its children as props and has continuity in the process. This means that states are passed to the child components without useState receiving them, which makes the passed state unresponsive.
The following code violates the principle of a single data source because the state searchResult is defined in the child component to cache the search results, which causes the options parameter to become unresponsive to the child component after onFilter.
function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) {
// Cache search results
const [searchResult, setSearchResult] = React.useState<Option[] | undefined> (undefined);
return (
<div>
<Input.Search
onSearch={(keyword)= >{ setSearchResult(keyword ? onFilter(keyword) : undefined); }} / ><OptionList options={searchResult ?? options} / >
</div>
);
}
Copy the code
The principle of a single data source should be followed. Save the keyword as state and generate new options in response to the keyword change:
function SelectDropdown({ options = [], onFilter }: SelectDropdownProps) {
// Search for keywords
const [keyword, setKeyword] = React.useState<string | undefined> (undefined);
// Use filter criteria to filter data
const currentOptions = React.useMemo(() = > {
return keyword && onFilter ? options.filter((n) = > onFilter(keyword, n)) : options;
}, [options, onFilter, keyword]);
return (
<div>
<Input.Search
onSearch={(text)= >{ setKeyword(text); }} / ><OptionList options={currentOptions} />
</div>
);
}
Copy the code
To reduceuseEffect
UseEffect is a side effect. UseEffect should be minimized if not necessary. React officially lists the use scenarios of the API as changing the DOM, adding subscriptions, asynchronous tasks, logging, and so on. Let’s start with some code:
function Demo({ value, onChange }) {
const [labelList, setLabelList] = React.useState(() = > value.map(customFn));
// Update the internal status after the value changes
React.useEffect(() = > {
setLabelList(value.map(customFn));
}, [value]);
}
Copy the code
The above code uses useEffect to keep labelList and Value responsive. Maybe you can see that the code itself works. If you now had a requirement that labelList change also sync to value, you might write something like this on a literal level:
React.useEffect(() = > {
onChange(labelList.map(customFn));
}, [labelList]);
Copy the code
You will find that the application goes into a permanent loop and the browser loses control. This is the unnecessary useEffect. It can be understood that useEffect should not be used for scenarios such as changing DOM, adding subscriptions, asynchronous tasks, logging, etc., such as listening for state and changing other states. The end result is that the application gets to a point where either the browser crashes first or the developer crashes.
Is there a good way? We can think of logic as action + state. Where a change of state can only be triggered by an action. This can well solve the problem in the above code, improve the state of labelList, find out the action to change the value, and encapsulate a method to change labelList linkage to each action. The more complex the scene, the more efficient this mode is.
Principle of universality
In a sense, universal design relinquishes control over the DOM and transfers control over the DOM structure to the developer, such as reserving custom rendering.
For example, Table in ANTD uses the render function to give the user the right to render each cell, which greatly improves the extensibility of the component:
const columns = [
{
title: 'name'.dataIndex: 'name'.width: 200.render(text) {
return <em>{text}</em>; }},];<Table columns={columns} />;
Copy the code
Excellent components that preset default rendering behavior with parameters and support custom rendering.
A unified API
When the number of components increases, there may be a certain relationship between components directly. We can unify the consistency of certain behavior apis, which can reduce the mental burden of users on the API names of each component. Otherwise, component parameter passing is as painful as counting noodles one by one.
For example, the classic Value and onChange apis can be used in different form fields. More high-level components can be exported in a wrapper, which in turn can be accommodated by a form management component.
We can convention apis on components such as Visible, onVisibleChange, bordered, Size, allowClear to be consistent across components.
Immutable state
Immutable state and one-way data flow are the core concepts of React, a functional programming paradigm. Immer is recommended for immutable data management if a complex component is left in immutable state manually. If the internal properties of an object change, then the whole object is new, and the invariant parts are kept referenced, which is natural to fit the React. Memo comparison, reduce the performance cost of shouldComponentUpdate comparison.
Pay attention to the trap
React is a state machine in the sense that variables defined by render are redeclared each time.
Context
trap
export function ThemeProvider(props) {
const [theme, switchTheme] = useState(redTheme);
// Each time the ThemeProvider is rendered, a new value is created, forcing all components that use the Context to be rendered
return <Context.Provider value={{ theme.switchTheme}} >{props.children}</Context.Provider>;
}
Copy the code
So the value passed to the Context is cached:
export function ThemeProvider(props) {
const [theme, switchTheme] = useState(redTheme);
const value = React.useMemo(() = > ({ theme, switchTheme }), [theme]);
return <Context.Provider value={value}>{props.children}</Context.Provider>;
}
Copy the code
render props
trap
When creating functions in the Render method, using render props offsets the advantages of using React. Memo. Because shallow comparisons of props always give false, and in this case each render will generate a new value for render props.
<CustomComponent renderFooter={() = > <em>Footer</em>} / >Copy the code
You can use useMethods instead of: github.com/MinJieLiu/heo/blob/main/src/useMethods.tsx
Communities of practice
Higher-order component/decorator pattern
const HOC = (Component) = > EnhancedComponent;
Copy the code
The decorator pattern is to wrap and extend the original object (adding attributes or methods) without changing the original object, so that the original object can meet the user’s more complex needs, meet the open and close principle, and do not break the existing operations. Components convert props to UI, whereas higher-order components convert one component to another.
For example, Iron Man in Marvel movies is an ordinary man himself who can walk and jump. They can run faster and have the ability to fly.
Wrapping a withRouter(React-Router) in a normal component gives you the ability to manipulate routes. Wrapping a Connect (React-Redux) gives you the ability to manipulate global data.
Render Props
<Component render={(props) = > <EnhancedComponent {. props} / >} / >Copy the code
Render Props for code sharing between React components using a prop with a value of function. Render Props, like higher-order components, are designed to give pure function components state in response to the React lifecycle. It passes a function to a child component to call, in a callback fashion, to get the state to interact with the parent component.
The chainHooks
In the age of React Hooks, high-order components and render props were used much less frequently and in many cases were replaced by Hooks.
Let’s look at the hooks rule:
- Use only at the top level
Hook
- Not called in loops, conditions, or nested functions
Hook
- Only in the
React
Call from a functionHook
Hooks are called in some order because they are implemented internally using linked lists. Each hook can be presented as a module through the concept of a single responsibility, and incremental enhancements can be achieved by combining custom hooks. Like RXJS, you can make chained calls while manipulating their state and lifecycle.
Example:
function Component() {
const value = useSelectStore();
const keyboardEvents = useInteractive(value);
const label = useSelectPresent(keyboardEvents);
// ...
}
Copy the code
After using semantic composition, you can choose to use hooks as needed to create custom components that fit each requirement. In the sense that a minimum unit is not just a component, it can also be custom hooks.
conclusion
Hopefully, everyone can write high-quality components.
Elegant chapter
I just started writing in the last two months. I hope I can help you, or I can join groups to learn and progress together.
- How to write a more elegant React component – code structure
- How to write more elegant code – JavaScript text