Compound pattern
这个模式的它能解决怎样的问题? 其实没有解决什么问题,只是把需要共用相同state的组件用一种合理的方式组合在一起罢了。
我们先看一下使用了Compound模式的组件代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</FlyOut>
);
}
|
这个模式出现在很多地方,比如Shadcn中的组件,也大多是基于多个更小的组件组合起来的。 不一定非得使用Flyout.xxxx来表示子组件, 但是这种命名方式更加清晰
Toggle,List, Item组件都复用了Flyout的状态value。
Flyout的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const FlyOutContext = createContext();
function Flyout({children}) {
const [value, setValue] = useState(false)
<FlyOutContext.Provider value={{value, setValue}}>
{chirdren}
</FlyOutContext.Provider>
}
|
然后以Toggle组件为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Flyout.Toggle = ({chidren}) => {
const {value, setValue} = useContext(FlyOutContext)
return (
<div onClick={() => setVale(!value)}>
<Icon />
</div>
)
}
|
其他的组件也是用一同样的方式获取到value, 这个不赘述了;这里需要追加的一点的是, 给多个子元素传递状态的方式除了上面这种使用Context, 也可以直接通过遍历React.Children结合React.cloneElement函数来实现, 比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| export function FlyOut(chilren) {
const [value, setValue] = React.useState(false);
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, { value, setValue })
)}
</div>
);
}
|
HOC (High-order component)
基础示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import React from 'react';
const withAsync = (WrappedComponent, LoadingComponent = () => <div>Loading...</div>) => {
return (props) => {
if (props.loading) {
return <LoadingComponent {...props} />;
}
return <WrappedComponent {...props} />;
};
};
export default withAsync;
|
上面展示了一个基础的HOC示例,withAsny函数会根据props的loading属性来决定是不是要添加一个等待的过渡效果, 好处在于逻辑复用(类似于装饰器函数)
带选项options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| import React from 'react';
const withAsync = (WrappedComponent, options = {}) => {
const {
LoadingComponent = () => <div>Loading...</div>,
ErrorComponent = ({ error }) => <div>Error: {error}</div>,
loadingPropName = 'loading',
errorPropName = 'error'
} = options;
return (props) => {
if (props[errorPropName]) {
return <ErrorComponent error={props[errorPropName]} {...props} />;
}
if (props[loadingPropName]) {
return <LoadingComponent {...props} />;
}
return <WrappedComponent {...props} />;
};
};
export default withAsync;
|
带options的HOC, 可以类比为带参数的装饰器函数, 可以更加灵活地处理不同的应用场景
使用场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| function DataDisplay({ data }) {
return <div>{JSON.stringify(data, null, 2)}</div>;
}
const EnhancedDataDisplay = withAsync(DataDisplay, {
LoadingComponent: Spinner,
ErrorComponent: ErrorMessage,
loadingPropName: 'isFetching',
errorPropName: 'fetchError'
});
...
return (
<div>
<h1>Data Display</h1>
<EnhancedDataDisplay
data={state.data}
isFetching={state.isFetching}
fetchError={state.fetchError}
/>
</div>
);
|
typescipt 版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| import React from 'react';
interface WithAsyncOptions<P = any> {
LoadingComponent?: React.ComponentType<P>;
ErrorComponent?: React.ComponentType<P & { error: any }>;
loadingPropName?: string;
errorPropName?: string;
}
function withAsync<P extends object>(
WrappedComponent: React.ComponentType<P>,
options: WithAsyncOptions<P> = {}
): React.FC<P> {
const {
LoadingComponent = () => <div>Loading...</div>,
ErrorComponent = ({ error }) => <div>Error: {error}</div>,
loadingPropName = 'loading',
errorPropName = 'error'
} = options;
return (props: P & { [key: string]: any }) => {
if (props[errorPropName]) {
return <ErrorComponent error={props[errorPropName]} {...props} />;
}
if (props[loadingPropName]) {
return <LoadingComponent {...props} />;
}
return <WrappedComponent {...props} />;
};
}
export default withAsync;
|
组合多个HOC情况
compose函数(用来结合多个HOC):
1
2
3
4
5
6
7
8
9
| const compose = (...hocs) => (Component) =>
hocs.reduceRight(
(WrappedComponent, hoc) => hoc(WrappedComponent),
Component
);
|
使用compose场景(结合context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| const withUserContext = (WrappedComponent) => {
return (props) => (
<UserContext.Consumer>
{(user) => <WrappedComponent {...props} user={user} />}
</UserContext.Consumer>
);
};
const EnhancedComponent = compose(
withTheme,
withLoading,
withUserContext
)(MyComponent);
|
Render props替代HOC场景
譬如实现基于登录状态的条件渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| function Auth({ children }) {
const [user, setUser] = useState(null);
useEffect(() => {
checkAuth().then(setUser);
}, []);
return children({
isAuthenticated: !!user,
user
});
}
// 使用
<Auth>
{({ isAuthenticated, user }) =>
isAuthenticated
? <Dashboard user={user} />
: <Login />
}
</Auth>
|
但是这里还是需要注意的一点在于: 和前面的HOC的条件渲染不同, 这里的条件渲染事实上是在子组件实现的(前面的HOC是在Wrapper函数实现的), 外部的Auth函数只是提供给Children必要的props而已; 但是可不可以把UI逻辑放到Auth组件里?其实也是可以的;但是render props模式一般只是提供给children共享的状态(比如上面的user);
另外假设有多个子组件要共用状态的话, 可以考虑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| <Auth>
{({ isAuthenticated, user }) => (
<>
{isAuthenticated ? (
<>
<Dashboard user={user} />
<Profile user={user} />
</>
) : (
<>
<Login />
<Signup />
</>
)}
</>
)}
</Auth>
|
或者直接使用Context(诸如前面的Compund模式), context适合更加复杂(比如深度嵌套)的状态共享
Props Collection 模式
基础用法
鼠标交互逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const mouseProps = {
onMouseMove: (e) => setPosition({ x: e.clientX, y: e.clientY }),
style: { cursor: 'pointer' },
};
return {
position,
mouseProps,
};
}
function MouseTracker() {
const { position, mouseProps } = useMouse();
return (
<div {...mouseProps}>
Mouse position: {position.x}, {position.y}
</div>
);
}
|
简单来讲Props collection就是把相关的props集合在了一起, 比如上面拿到useMouse, 把位置相关的, 以及和鼠标位置相关联的action组合在了一起
结合Render Props模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| function Tooltip({ children, content }) {
const [isVisible, setIsVisible] = useState(false);
const tooltipProps = {
onMouseEnter: () => setIsVisible(true),
onMouseLeave: () => setIsVisible(false),
};
return children({
isVisible,
tooltipProps,
});
}
function App() {
return (
<Tooltip content="This is a tooltip">
{({ isVisible, tooltipProps }) => (
<div>
<button {...tooltipProps}>Hover me</button>
{isVisible && <div className="tooltip">Tooltip content</div>}
</div>
)}
</Tooltip>
);
}
|
使用多种Props集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| function useHover() {
const [isHovered, setIsHovered] = useState(false);
const hoverProps = {
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
};
return {
isHovered,
hoverProps,
};
}
function useFocus() {
const [isFocused, setIsFocused] = useState(false);
const focusProps = {
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false),
};
return {
isFocused,
focusProps,
};
}
function Button() {
const { isHovered, hoverProps } = useHover();
const { isFocused, focusProps } = useFocus();
return (
<button
{...hoverProps}
{...focusProps}
style={{
backgroundColor: isHovered ? 'lightblue' : 'white',
borderColor: isFocused ? 'blue' : 'gray',
}}
>
Interactive Button
</button>
);
}
|
Props Getter 模式
不直接返回Props对象, 而是说利用闭包返回可以生成Props的函数, 这样在后续使用中可以更加灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| function useToggle(initialOn = false) {
const [on, setOn] = useState(initialOn);
const toggle = () => setOn(!on);
// Props Getter 函数
const getTogglerProps = ({ onClick, ...props } = {}) => ({
'aria-pressed': on,
onClick: () => {
toggle();
onClick?.();
},
...props,
});
return {
on,
toggle,
getTogglerProps,
};
}
function App() {
const { on, getTogglerProps } = useToggle();
return (
<div>
<button
{...getTogglerProps({
onClick: () => console.log('Button clicked'),
className: 'my-button',
})}
>
{on ? 'ON' : 'OFF'}
</button>
</div>
);
}
|
State Reducer模式
State Reducer 模式的核心思想是:组件仍然管理自己的状态,但将状态更新的决定权通过 reducer 函数暴露给使用者
传统Reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| function toggleReducer(state, action) {
switch (action.type) {
case 'TOGGLE':
return { on: !state.on };
default:
return state;
}
}
function useToggle() {
const [state, dispatch] = useReducer(toggleReducer, { on: false });
const toggle = () => dispatch({ type: 'TOGGLE' });
return {
on: state.on,
toggle,
};
}
|
添加State Reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| function useToggle({ reducer = toggleReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { on: false });
const toggle = () => dispatch({ type: 'TOGGLE' });
return {
on: state.on,
toggle,
};
}
// 使用默认行为
function App() {
const { on, toggle } = useToggle();
return <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>;
}
// 自定义 reducer
function App() {
const { on, toggle } = useToggle({
reducer(state, action) {
if (action.type === 'TOGGLE' && on) {
return state; // 阻止关闭
}
return toggleReducer(state, action);
}
});
return <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>;
}
|
使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| function useForm({ reducer = formReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { values: {} });
const setFieldValue = (name, value) =>
dispatch({ type: 'SET_FIELD', name, value });
return {
values: state.values,
setFieldValue,
};
}
// 自定义 reducer 添加日志
function LoggingForm() {
const { values, setFieldValue } = useForm({
reducer(state, action) {
console.log('Action:', action);
const newState = formReducer(state, action);
console.log('New state:', newState);
return newState;
}
});
// 渲染逻辑...
}
|
State Reducer的实现基础在于 reducer函数签名的一致性:(state, action) => void
也正因为多个reducer共享相同的函数签名, 所以State Reducer也可以和组合多个HOC那样实现composer模式(类似前面组合多个HOC一样)
多个reducer组合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| function composeReducers(...reducers) {
return (state, action) =>
reducers.reduce((currentState, reducer) =>
reducer(currentState, action), state);
}
function useAdvancedToggle() {
const { on, toggle } = useToggle({
reducer: composeReducers(
toggleReducer,
(state, action) => {
if (action.type === 'TOGGLE' && state.on) {
// 添加额外限制
return state;
}
return state;
},
(state, action) => {
// 添加日志
console.log(action);
return state;
}
)
});
}
|
总结
其实上面的设计模式, 核心就是围绕
上诉react设计模式之所以需要强调关注点分离的一个重要原因在于:React Hook本身在设计上, 将UI和逻辑紧密地糅在了一起(相对的Vue3将逻辑也就是script和UI进行了分离, 两种流派各有利弊),所以在设计复杂应用的时候又需要以某种方式实现关注点分离