React Hooks使用姿势

遵循设计模式(SOLID)

单一功能(SRP)

  1. 每个类应该只有一个职责
  2. 如下一个展示活跃用户列表的组件,他分别做了获取数据,筛选数据,渲染数据这些功能。显然不符合单一功能的原则(虽然这看上去没有多少行代码),可以尝试对其进行拆分。
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
const ActiveUsersList = () => {
const [users, setUsers] = useState([])

useEffect(() => {
const loadUser = async () => {
const response = await fetch('/some-api')
const data = await response.json()
setUsers([...data])
}
loadUser()
}, [])

const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);

return (
<ul>
{users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo).map(user =>
<li key={user.id}>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
)}
</ul>
)
}
  1. 首先,可以将useState和useEffect抽离封装成一个独立的Hooks,useUsers只需要关心从api获取到数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const useUsers = () => {
const [users, setUsers] = useState([])

useEffect(() => {
const loadUser = async () => {
const response = await fetch('/some-api')
const data = await response.json()
setUsers([...data])
}
loadUser()
}, [])

return { users }
}
  1. 其次,对ui层渲染,筛选数据进行抽离。
1
2
3
4
5
6
7
8
9
10
11
12
13
const UserItem = ({ user }) => {
<li key={user.id}>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
}
const getActiveUser = (users) => {
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)

return users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo)
}
  1. 还可以将上面拆解的三个hook再封装成一个独立的hook,在ActiveUserList中直接调用。
1
2
3
4
5
6
7
8
9
const useActiveUsers = () => {
const { users } = useUsers()

const activeUsers = useMemo(() => {
return getOnlyActive(users)
}, [users])

return { activeUsers }
}
  1. 最后在ActiveUsersList只需要调用hooks进行渲染就好了**。**
1
2
3
4
5
6
7
8
9
10
const ActiveUsersList = () => {
const { activeUsers } = useActiveUsers()
return (
<ul>
{activeUsers.map(user =>
<UserItem key={user.id} user={user} />
)}
</ul>
)
}

开放封闭(OCP)

一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭 (以一种允许在不更改源代码的情况下扩展组件的方式来构造组件)

例如组件库中的表单组件,在FromItem中嵌套Input,Audio等组件,它不关心内部是什么组件,将责任委托给children。

ps:FormItem 会给自己的直接子节点(必须是唯一子节点)传递 onChangevalue 属性,自定义控件只有在调用这个onChange 之后,自己的值才能被 FormItem 收集到 。onChange事件的注入是调用了 cloneElement() 方法在原先子组件的基础上注入onChange方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<Form onSubmit={(v) => { console.log(v); }}>
<FormItem label='Username' field='name'>
<Input />
</FormItem>
<FormItem label='Age' field='age'>
<InputNumber placeholder='please enter your age' />
</FormItem>
<FormItem>
<Button type='primary' htmlType='submit'>
Submit
</Button>
</FormItem>
</Form>

里氏替换(LSP)

所有引用基类的地方必须能透明地使用其子类的对象

但是react团队不推荐使用继承,因此不做过多实践。

1
2
3
4
引用 https://zh-hans.reactjs.org/docs/composition-vs-inheritance.html#so-what-about-inheritance
在 Facebook,我们在成百上千个组件中使用 React。我们并没有发现需要使用继承来构建组件层次的情况。
Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。
如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

接口隔离(ISP)

**客户端****不应该依赖它不需要的接口 **不传递子组件不需要的属性

依赖倒置(DIP)

要依赖于抽象,不要依赖于具体

如下实现一个登陆的表单,它需要依赖另一个文件的api 方法。这样使用当修改api文件的某些参数或其他会影响api函数调用的东西,就需要修改LoginForm的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import api from '~/common/api'

const LoginForm = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')

const handleSubmit = async (evt) => {
evt.preventDefault()
await api.login(email, password)
}

return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Log in</button>
</form>
)
}

可以将api的调用作为一个props传给LoginForm组件,而调用的逻辑写在父组件(一般组件库的Form表单都是这样,把onSubmit交给用户)

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
// LoginForm
const LoginForm = ({ onSubmit }: Props) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')

const handleSubmit = async (evt) => {
evt.preventDefault()
await onSubmit(email, password)
}

return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Log in</button>
</form>
)
}
// ConnectedLoginForm
import api from '~/common/api'

const ConnectedLoginForm = () => {
const handleSubmit = async (email, password) => {
await api.login(email, password)
}

return (
<LoginForm onSubmit={handleSubmit} />
)
}

总结

上述思想可以让我们书写的React / Vue代码更为健壮,但是也不能固执的遵循这些设计原则,有些时候可能不需要很细粒度的解耦 / 拆分。

常见Hooks使用

useEffect

  1. 假设我们需要一个定时器在页面上每300ms自增,执行如下代码你会发现count一直是1。
  2. 首先我们需要知道useEffect第二个参数的含义: 数组内的依赖发生改变的时候才会重新渲染 。有点类似于Vue3中的watchEffect,但是它需要我们手动增加依赖而不能像watchEffect自动收集。
1
2
3
4
5
6
7
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log("running");
setCount(count + 1)
}, 300)
}, [])
  1. 因此其实use Effect只执行了一次,定时器中的count也固定成为0,相当于一直执行 setCount (0 + 1),因此页面上永远是1。( 而且他没有清除定时器
  2. 一个不优雅的方式是将count作为依赖传给useEffect的数组,因为count改变所以useEffect会一直执行,但是他需要不断创建和清除定时器。(在useEffect传入的函数 进行return会在函数组件销毁前执行 类似于 componentWillUnmount
1
2
3
4
5
6
7
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 300)
return ()=>{clearInterval(id)}
}, [count])
  1. 或者可以使用setState的函数形式。因为对于这个逻辑来说,我们只需要让React知道数据改变就可以了。
1
2
3
4
5
6
7
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count => count + 1)
}, 300)
return () => { clearInterval(id) }
}, [])

useCallback

  1. 把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。
  2. 但是注意,如果组件或者hook其实不关心你传入的函数是否改变,那么useCallback的引入只是徒增成本(因为你需要额外新增一个数组,每次新创建一个函数,原来的函数还不会被GC)这样反而降低了性能。
  3. 那么什么时候需要使用useCallback呢?
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
function Counter() {
const [count, setCount] = useState(0);
const handleCount = () => setCount(count + 1);

return (
<div>
<p>You clicked {count} times</p>
<Button onClick={handleCount}> Click me</Button>
<Other onClick={handleCount} />
</div>
);
}

function Buttprops) {
console.log('Button 组件渲染了');
return <button onClick={props.onClick}>{props.children}</button>;
}

// 假如 Other 中有很重的逻辑
function Other({ onClick }) {
console.log("Other组件渲染了");
return (
<div>
<button onClick={onClick}>其他组件</button>
</div>
);
}
  1. 如果不加优化,那么此时点击任意一个button都会触发两个组件的渲染( 挖坑 为什么react父组件更新 子组件也会随之更新 ),这显然是我们不希望看到的,这种情况下就可以使用React.memo包裹子组件,当子组件的props改变才更新。
  2. 但仅仅为两个子组件加上React.memo还是不够的,由于state更新父组件会重新渲染,相应的handleCount也会重新创建,因此每次传入的handleCount都是新的,这样的话React.memo就无法达到优化的目的。
  3. 因此还需要借助 useCallback, 将父组件的 handleCount进行改造。 全部代码如下:
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
import React, { useCallback, useState } from "react";
import { createRoot } from "react-dom/client";

function Counter() {
const [count, setCount] = useState(0);
const handleCount = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<p>You clicked {count} times</p>
<Button onClick={handleCount}> Click me</Button>
<Other onClick={handleCount} />
</div>
);
}

const Button = React.memo(function Button(props) {
console.log("Button 组件渲染了");
return <button onClick={props.onClick}>{props.children}</button>;
});

// 假如 Other 中有很重的逻辑
const Other = React.memo(function Other({ onClick }) {
console.log("Other 组件渲染了");
return (
<div>
<button onClick={onClick}>其他组件</button>
</div>
);
});

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(<Counter />);

useMemo

与useCallback类似,useMemo更多的用来缓存数据。

1
const baz = React.useMemo(() => [1, 2, 3], [])

useLayoutEffect

WIP。。。


React Hooks使用姿势
https://jing-jiu.github.io/jing-jiu/2022/09/05/Framework/React/Hooks/
作者
Jing-Jiu
发布于
2022年9月5日
许可协议