参考: https://www.patterns.dev/react/
Compound pattern
这个模式的它能解决怎样的问题? 其实没有解决什么问题,只是把需要共用相同state的组件用一种合理的方式组合在一起罢了。
我们先看一下使用了Compound模式的组件代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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
| 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
| Flyout.Toggle = ({chidren}) => {
const {value, setValue} = useContext(FlyOutContext)
return (
<div onClick={() => setVale(!value)}>
<Icon />
</div>
)
}
|
其他的组件也是用一同样的方式获取到value, 这个不赘述了;这里需要追加的一点的是, 隐式传递状态的方式不止上面的这种, 还可以通过React.cloneElement来实现, 比如:
1
2
3
4
5
6
7
8
9
10
11
| export function FlyOut(chilren) {
const [value, setValue] = React.useState(false);
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, { value, setValue })
)}
</div>
);
}
|
简单来说就是直接把父组件的state作为属性附加到了FlyOut的直接子元素上,所以对于嵌套更深的组件, 这种方式就是不太行, 同时需要注意属性名冲突的问题
HOC
TL;DR, HOC高阶组件就是通过函数, 抽离共用逻辑来包装已有的组件生成的组件,例如, 我们需要给一些组件添加共用属性:
1
2
3
4
5
6
7
8
9
10
11
12
| function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' } // 共用style
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
|
看起来看很简单是吧?但是这里其实隐藏了一个有意思的点, withStyle是直接作用在被包装的组件上,以Button为例, style被添加在了Button上, 但是style并没有定义在Button中的button中, 但是它依然会生效!因为React会自动声明未处理的属性, 并传递到子组件
对被包装Hook使用固定的style有点笨, 假设我要传入自定义的style呢?可以把style作为withStyle的第二个参数
有时候我们会使用React hooks来替代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
| // 定义一个Hook useLocalStore
import { useState, useEffect } from 'react';
interface UseLocalStoreOptions<T> {
key: string;
initialValue: T;
}
function useLocalStore<T>(options: UseLocalStoreOptions<T>): [T, (value: T) => void] {
const { key, initialValue } = options;
// 从 localStorage 中获取初始值
const storedValue = localStorage.getItem(key);
const initialStoredValue = storedValue ? JSON.parse(storedValue) : initialValue;
// 使用 useState 管理状态
const [value, setValue] = useState<T>(initialStoredValue);
// 当 value 发生变化时,更新 localStorage
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
export default useLocalStore;
// 使用自定义useLocalStore
import React from 'react';
import useLocalStore from './useLocalStore';
const App: React.FC = () => {
const [count, setCount] = useLocalStore({ key: 'count', initialValue: 0 });
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
};
|
首先上面这个例子可不可以使用HOC, 答案是可行的; 就是把useLocalStorageh中的和localStorage交互的部分定义到HOC, 同时把第一个参数改成Component即可(options变成第二个参数), 只不过和之前withStyle不一样的地方在于, 我没有对子组件进行某种封装,简而言之:
- HOC的本质就是类型为 component => component 的函数
- 而Hooks通常只会代理组件的一部分通用功能或者副作用(side effects), 比如上面的从localStorage获取值的逻辑
Render props pattern
基础使用场景 - 无参render function
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 定义一个需要带有render props的组件
const Title = ({render}) => render()
// 使用Title
export const App = () => {
return <Title
render={()=>(
<h1>
This is a title
</h1>
)}
/>
}
|
简单来说就是子组件将部分渲染逻辑交给父组件去执行
带参数的render function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function Input({render}) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{render(value)}
</>
);
}
|
children
本质上render的功能就是props中chidren做的事情, 带参数的render函数本质是就是 (parms) => children
所以我们用分别改写上面的例子
1
2
3
4
| const Title = ({render}) => render()
// 等价于
const Title = ({children}) => children
|
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 Input({render}) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{render(value)}
</>
);
}
// 等价于
function Input({children}) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{children(value)}
</>
);
}
|
使用改写的Input组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input>
{value => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
</Input>
</div>
);
}
function Kelvin({ value }) {
return <div className="temp">{parseInt(value || 0) + 273.15}K</div>;
}
function Fahrenheit({ value }) {
return <div className="temp">{(parseInt(value || 0) * 9) / 5 + 32}°F</div>;
}
|
其实理论上也可以用状态提升(state lifting)来实现, 但是对于第三方组件, 如果我们想要拿到它的内部状态(假设Input是第三方的组件, input中的value就是它的内部状态), 使用render props pattern就可以轻松实现