Preface
"Design pattern" is a common topic, but it is more concentrated in the field of object-oriented languages, such as C++, Java, etc. The front-end field is not very popular about design patterns. Many people feel that it is difficult to reflect the value of design patterns for a typical process-oriented language like JavaScript. I had a similar view before, and my understanding of design patterns only stayed at the conceptual level, without in-depth understanding of their practice in front-end engineering. I recently read the book "JavaScript Design Patterns and Development Practices". The book introduces 15 common design patterns and basic design principles, as well as how to use JavaScript to elegantly implement and apply them in actual projects. It happened that the team held a debate about Hooks not long ago, and the core idea of Hooks lies in functional programming, so I decided to explore the topic of "whether design patterns help us write more elegant Hooks".
Why is a design pattern
In a counter-attack martial arts drama, when the protagonist asks the first master for martial arts, at first the master only asks the protagonist to pick water, stab the horse, and other basic skills. At this time, the protagonist will always complain in all manners, but because of some objectiveness The reason had to be persisted. Later, when he began to learn real martial arts, he realized the painstaking efforts of his former teacher and father. After laying a solid foundation, he learned martial arts by leaps and bounds, and eventually became a generation of heroes. For us developers, "data structure" and "design pattern" are the basic skills taught by our teacher and father. They may not make us go faster, but they can certainly make us go more stable and farther, which is helpful. Because we write highly reliable and easy-to-maintain code to avoid being "dumb" in the future.
Since the release of Hooks, one thing that has been criticized is the surge in maintenance costs, especially for teams with large gaps in the ability of members. Even if the entire project framework is built by experienced classmates at the beginning, once it is handed over to a newcomer for maintenance for a period of time, it will most likely become unrecognizable, let alone let the newcomer use Hooks to develop a zero-to-one project. I understand this is due to the high flexibility of Hooks. Class Component still has a series of life cycle methods to constrain. In addition to the constraints on API parameters, Hooks only have " at the top level" " only" in the React function "Two mandatory rules. On the other hand, custom Hooks improve the reuse rate of component logic, but also lead to inexperienced developers lacking design in abstraction. The abstraction of logic in Class Component is usually abstracted as a pure function, while the encapsulation of Hooks may carry various side effects ( useEffect
), and it is more expensive to troubleshoot when a bug occurs.
So since "design mode" is a basic skill, and "Hooks" is a new trick, then we try to start from the design mode and conquer the new trick.
What are the classic design patterns
Before we formally enter the topic, let's briefly review those classic design patterns and design principles that are about to be forgotten by us. In daily life, we refer to design principles that will simplify them to " SOLID ", which corresponds to the Single Responsibility Principle, Open Closed Principle, Liskov Substitution Principle, The Law of Demeter, Interface Segregation Principle and Dependence Inversion Principle. The design pattern also includes singleton pattern, strategy pattern, agent pattern, iterator pattern, publish-subscribe pattern, command pattern, combination pattern, template method pattern, hengyuan pattern, responsibility chain pattern, intermediary pattern, decorator pattern, State mode, adapter mode, etc.
There are many excellent articles about design principles and design patterns in the community, so I won't repeat them here, just to arouse everyone's memory.
1 + 1 > 2
Do you have to use useContext?
In the React Hook project, once it comes to global state management, our intuition is to use useContext
. For example, suppose that the project needs to determine whether certain components should be rendered based on the information returned by the grayscale interface. Since the entire project shares a gray configuration, we can easily think of it as a global state, call the asynchronous interface to obtain and initialize when the project is initialized, and then use useContext
to obtain it inside the component.
// context.js
const GrayContext = React.createContext();
export default GrayContext;
// App.js
import GrayContext from './context';
function App() {
console.log('App rerender');
const [globalStatus, setGlobalStatus] = useState({});
useEffect(() => {
console.log('Get GrayState');
setTimeout(() => {
setGlobalStatus({
gray: true
});
}, 1000);
}, []);
return (
<GrayContext.Provider value={globalStatus}>
<GrayComponent />
<OtherChild />
</GrayContext.Provider>
);
}
// GrayComponent/index.js
function GrayComponent() {
console.log('GrayComponent rerender');
const grayState = useContext(GrayContext);
return (
<div>
子节点
{grayState.gray && <div>灰度字段</div>}
</div>
);
}
// OtherChild/index.js
function OtherChild() {
console.log('OtherChild rerender');
return (
<div>其它子节点</div>
);
}
However createContext
Provider
to be re-rendered once the global state changes, even if it does not consume any information under the context.
Think about it carefully, this scenario has the same effect as the "publish-subscribe model" in the design pattern. We can define a global state instance GrayState
App
, initialize the value in the 0611a615dc5ed5 component, and subscribe to the changes of the instance in the sub-components, The same effect can be achieved, and only GrayState
change will be re-rendered.
// GrayState.js
class GrayState {
constructor() {
this.observers = [];
this.status = {};
}
attach(func) {
if (!this.observers.includes(func)) {
this.observers.push(func);
}
}
detach(func) {
this.observers = this.observers.filter(observer => observer !== func);
}
updateStatus(val) {
this.status = val;
this.trigger();
}
trigger() {
for (let i = 0; i < this.observers.length; i++) {
this.observers[i](this.status);
}
}
}
export default new GrayState();
// App.js
import GrayState from './GrayState.js';
function App() {
console.log('App rerender');
useEffect(() => {
console.log('Get GrayState');
setTimeout(() => {
const nextStatus = {
gray: true,
};
GrayState.updateStatus(nextStatus);
}, 200);
}, []);
return (
<div>
<GrayComponent />
<OtherChild />
</div>
);
}
// GrayComponent/index.js
import GrayState from './GrayState.js'
function GrayComponent() {
console.log('GrayComponent rerender');
const [visible, setVisible] = useState(false);
useEffect(() => {
const changeVisible = (status) => {
setVisible(status.gray);
};
GrayState.attach(changeVisible);
return () => {
GrayState.detach(changeVisible);
};
}, []);
return (
<div>
子节点
{visible && <div>灰度字段</div>}
</div>
);
}
The final effect is the same. The difference is that after obtaining the grayscale state, the GrayComponent
, which only relies on the grayscale configuration information, is re-rendered.
Considering better reuse, we can also Status
into a custom Hook:
// useStatus.js
import { useState, useEffect } from 'react';
import GrayState from './GrayState';
function useGray(key) {
const [hit, setHit] = useState(false);
useEffect(() => {
const changeLocalStatus = (status) => {
setHit(status[key]);
};
GrayState.attach(changeLocalStatus);
return () => {
GrayState.detach(changeLocalStatus);
};
}, []);
return hit;
}
export default useGray;
// GrayComponent/index.js
import useStatus from './useGray.js'
function GrayComponent() {
console.log('GrayComponent rerender');
const [visible, setVisible] = useGray('gray');
return (
<div>
子节点
{visible && <div>灰度字段</div>}
</div>
);
}
Of course, with the help of redux, you can also re-render on demand, but if the project does not have a large amount of global state, using redux is a bit of a sledgehammer.
useState or useReducer
Hooks beginners often feel that "I only use useState
useEffect
in my development, and other hooks don't seem to be needed." This feeling stems from the lack of thorough understanding of Hooks. useCallback
useMemo
is a performance optimization hook that is used only when necessary useReducer
is worthy of our attention. In the official explanation, useReducer
is useState
. Under what circumstances is it worth replacing? Here is also an example for analysis.
Take the most common example in the state mode-the sequence switcher of a music player.
function Mode() {
/* 普通书写模式 */
const [mode, setMode] = useState('order'); // 定义模式状态
const changeHandle = useCallback((mode) => { // 模式切换行为
if (mode === 'order') {
console.log('切换到随机模式');
setMode('random');
} else if (mode === 'random') {
console.log('切换到循环模式');
setMode('loop');
} else if (mode === 'loop') {
console.log('切换到顺序模式');
setMode('order');
}
}, []);
return (
<div>
<Button onClick={() => changeHandle(mode)}>切换模式</Button>
<div>{mode.text}</div>
</div>
);
}
In the above implementation, it can be seen that the mode switching depends on the previous state, which is switched in sequence among the three modes of "sequential play-random play-loop play". There are only three modes at present, which can be implemented using the simple if...else
method, but once there are more modes, it will be very difficult to maintain and expand. Therefore, for scenarios where this behavior depends on state, when the branch grows to a certain extent, it needs to be considered. Redesign using "state mode".
function Mode() {
/* 普通的状态模式实现 */
const [mode, setMode] = useState({});
useEffect(() => {
const MODE_MAP = {
order: {
text: 'order',
press: () => {
console.log('切换到随机模式');
setMode(MODE_MAP.random);
},
},
random: {
text: 'random',
press: () => {
console.log('切换到循环模式');
setMode(MODE_MAP.loop);
},
},
loop: {
text: 'loop',
press: () => {
console.log('切换到顺序模式');
setMode(MODE_MAP.order);
},
}
};
setMode(MODE_MAP.order);
}, []);
return (
<div>
<Button onClick={() => mode.press()}>切换模式</Button>
<div>{mode.text}</div>
</div>
);
}
React official website of useReducer
mentioned explanation of "in some scenarios, useReducer
than useState
more suitable, for example, more complex logic state and comprising a plurality of sub-values, or next state dependent on the previous state the like." Let's focus on the latter scenario. The "state mode" of "the behavior of a class is based on its state change" is a typical scenario that depends on the previous state. This makes useReducer
naturally suitable for multi-state switching business scenarios.
/* 借助 reducer 更便捷实现状态模式 */
const reducer = (state) => {
switch(state) {
case 'order':
console.log('切换到随机模式');
return 'random';
case 'random':
console.log('切换到循环模式');
return 'loop';
case 'loop':
console.log('切换到顺序模式');
return 'order';
}
};
function Mode() {
const [mode, dispatch] = useReducer(reducer, 'order');
return (
<div>
<Button onClick={dispatch}>切换模式</Button>
<div>{mode.text}</div>
</div>
);
}
Custom Hook packaging principle
Custom Hooks are an important reason for the popularity of React Hooks. However, a custom Hook with poor abstraction may greatly increase maintenance costs. In the "The Ugly Side of React Hooks", the cost of anomalies caused by nested side effects is listed. We often say that design principles and design patterns help improve the maintainability and extensibility of the code. So what principles/patterns can help us elegantly encapsulate custom Hooks?
OS: "How to encapsulate custom Hooks elegantly" is a big topic. I will just cite a few points here.
The zeroth point: there is data-driven
When comparing Hooks and class component development models, one thing that is often mentioned is that Hooks help us achieve a wider range of functional reuse between components. Therefore, when I first started learning Hooks, I often over-corrected any functional logic that might have reusable value and encapsulated them into weird Hooks. For example, when the user closes the notification and opens it for the first time in the day, I will give a second reminder. "Open" is a function, I abstracted a useTodayFirstOpen
:
// 🔴 Bad case
function useTodayFirstOpen() {
const [status, setStatus] = useState();
const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);
useEffect(() => {
// 获取用户状态
const fetchStatus = async () => {
const res = await getUserStatus();
setStatus(res);
};
fetchStatus();
// 判断今天是否首次打开
const value = window.localStorage.getItem('isTodayFirstOpen');
if (!value) {
setIsTodayFirstOpen(true);
} else {
const curr = getNowDate();
setIsTodayFirstOpen(curr !== value);
}
}, []);
useEffect(() => {
if (status <= 0) {
// 未打开时进行二次提醒
setTimeout(() => {
tryToPopConfirm({
onConfirm: () => {
setStatus(1);
updateUserStatus(1);
},
});
}, 300);
window.localStorage.setItem('isTodayFirstOpen', Date.now())
}
}, [status, isTodayFirstOpen]);
}
In fact, it didn't return anything, and it was only useTodayFirstOpen()
when called in the component. Looking back, this function does not have any external data inflow, and no data outflow. It can be abstracted as a higher-order function instead of a custom Hooks. Therefore reuse value and have a data-driven relationship with the outside need to be abstracted into custom Hooks.
The first principle: the single responsibility principle
The Single Responsibility Principle (SRP) suggests that a method has only one cause of change. Custom Hooks is essentially an abstract method to facilitate the reuse of logic functions in the component. However, if one Hooks assumes too many responsibilities, it may cause the change of one responsibilities to affect the execution of another responsibilities, causing unexpected consequences and increasing the probability of errors during subsequent function iterations. As for when it should be split, refer to the principle of separation of duties recommended in the SRP, which is more suitable for interpretation in Hooks as "If the cause of two data changes is not the same, then the two should be separated."
Taking useTodayFirstOpen
as an example, suppose there are still Switch
controls outside the world that need to be displayed and interacted status
function useTodayFirstOpen() {
const [status, setStatus] = useState();
const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);
// ...
const updateStatus = async (val) => {
const res = await updateUserStatus(val);
// dosomething...
}
return [status, updateStatus];
}
Assuming that getUserStatus
has changed, the Hook needs to be modified.
function useTodayFirstOpen() {
const [status, setStatus] = useState();
const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);
useEffect(() => {
// 获取用户状态
const fetchStatus = async () => {
const res = await getUserStatus();
setStatus(res.notice);
};
fetchStatus();
// ...
}, []);
// ...
}
Suppose one day, the frequency of the supervisory feedback being reminded twice a day is too high, and the request is changed to "twice reminding" every week, and the Hook needs to be refactored again.
function useThisWeekFirstOpen() {
const [status, setStatus] = useState();
const [isThisWeekFirstOpen, setIsThisWeekFirstOpen] = useState(false);
useEffect(() => {
// 获取用户状态
// ...
// 判断今天是否首次打开
const value = window.locaStorage.getItem('isThisWeekFirstOpen');
if (!value) {
setIsTodayFirstOpen(true);
} else {
const curr = getNowDate();
setIsThisWeekFirstOpen(diffDay(curr, value) >= 7);
}
}, []);
// ...
}
This obviously violates the single responsibility principle. At this time, we need to consider separating the status
and ...FirstOpen
to make it more versatile, and then abstract as a business hook in the form of combined with
// 用户状态管理
function useUserStatus() {
const [status, setStatus] = useState();
const fetchStatus = async () => {
const res = await getUserStatus();
setStatus(res);
};
useEffect(() => {
fetchStatus();
}, []);
const updateStatus = useCallback(async (type, val) => {
const res = await updateUserStatus(type, val);
if (res) {
console.log('设置成功');
fetchStatus();
} else {
console.log('设置失败');
}
}, []);
return [status, updateStatus];
}
// 二次提醒
function useSecondConfirm(key, gapDay, confirmOptions = {}) {
const [isConfirm, setIsConfirm] = useState(false);
const showConfirm = useCallback(() => {
const curr = Date.now();
const lastDay = window.localStorage.getItem(`${key}_lastCheckDay`);
if (!lastDay || diffDay(curr, lastDay) > gapDay) {
setTimeout(async () => {
tryToPopConfirm({
title: confirmOptions.title,
content: confirmOptions.content,
onConfirm: () => setIsConfirm(true),
});
}, 300);
window.localStorage.setItem(`${key}_lastCheckDay`, curr);
}
}, [gapDay]);
return [isConfirm, showConfirm];
}
function useStatusWithSecondConfirm(type, gapDay, confirmOptions) {
const [userStatus, setUserStatus] = useUserStatus();
const [isConfirm, showConfirm] = useSecondConfirm(type, gapDay, confirmOptions);
// 关闭状态二次提醒用户是否打开
useEffect(() => {
console.log(userStatus);
if (userStatus && userStatus[type] <= 0) {
showConfirm();
}
}, [userStatus]);
// 确认后修改用户状态
useEffect(() => {
if (isConfirm) {
setUserStatus(type, 1);
}
}, [isConfirm]);
return [userStatus ? userStatus[type] : null, setUserStatus];
}
// 使用时
function Component() {
const [status, setStatus] = useStatusWithSecondConfirm(
'notice',
1,
{
title: '是否打开提醒',
content: '打开通知以避免错过重要信息'
}
);
return (
<>
<label>打开消息提醒</label>
<Switch
checked={status}
onChange={setStatus}
/>
</>
);
}
After the transformation, if the interface for obtaining/setting the user status changes, modify useUserStatus
; if the effect of the secondary reminder needs to be changed (such as reporting the log), modify useSecondConfirm
; if the logic of the secondary reminder is adjusted in business (members will not be twice) Reminder), you only need to modify useStatusWithSecondConfirm
and define Hooks to perform their duties.
The n + 1 main point is trying hard to explore..., leave a hole, and continue to share new ideas in the future
Summarize
To be honest, this article is indeed suspected of "React Hooks" hotspots (manual dog head), but I have to say that the data structure and design pattern is yyds, which can guide us to find a clear path in the development of complex systems. Hooks are difficult to maintain, so try to let "God" save this chaotic situation. Regarding the question "whether design patterns help us write more elegant Hooks", after reading the previous chapters, I believe you also have your own answers. Of course, this article is not for the purpose of debating "whether Hooks are better than class development". Topic, if you are interested, welcome to join ES2049, maybe you can catch up with the next debate (~ ̄▽ ̄)~
Author: ES2049 / Lin Mumu
The article can be reprinted at will, but please keep the link to the original text.
You are very welcome to join ES2049 Studio if you are passionate. Please send your resume to caijun.hcj@alibaba-inc.com
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。