forked from elecrabbit/front-end-interview
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreactHook.md
774 lines (560 loc) · 27.1 KB
/
reactHook.md
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# React-hooks 抽象组件
点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**.
本文主要内容来源于前端精读系列的[怎么用 React Hooks 造轮子](https://github.com/dt-fe/weekly/blob/v2/080.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md)
## 1 引言
上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md) 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(就是数组),但理解实现原理就可以用好了吗?学的是知识,而用的是技能,看别人的用法就像刷抖音一样(哇,饭还可以这样吃?),你总会有新的收获。
这篇文章将这些知识实践起来,看看广大程序劳动人民是如何发掘 React Hooks 的潜力的(造什么轮子)。
首先,站在使用角度,要理解 React Hooks 的特点是 “非常方便的 Connect 一切”,所以无论是数据流、Network,或者是定时器都可以监听,有一点 RXJS 的意味,也就是你可以利用 React Hooks,将 React 组件打造成:任何事物的变化都是输入源,当这些源变化时会重新触发 React 组件的 render,你只需要挑选组件绑定哪些数据源(use 哪些 Hooks),然后只管写 render 函数就行了!
## 2 精读
参考了部分 React Hooks 组件后,笔者按照功能进行了一些分类。
> 由于 React Hooks 并不是非常复杂,所以就不按照技术实现方式去分类了,毕竟技术总有一天会熟练,而且按照功能分类才有持久的参考价值。
### DOM 副作用修改 / 监听
做一个网页,总有一些看上去和组件关系不大的麻烦事,比如修改页面标题(切换页面记得改成默认标题)、监听页面大小变化(组件销毁记得取消监听)、断网时提示(一层层装饰器要堆成小山了)。而 React Hooks 特别擅长做这些事,造这种轮子,大小皆宜。
> 由于 React Hooks 降低了高阶组件使用成本,那么一套生命周期才能完成的 “杂耍” 将变得非常简单。
下面举几个例子:
#### 修改页面 title
效果:在组件里调用 `useDocumentTitle` 函数即可设置页面标题,且切换页面时,页面标题重置为默认标题 “前端精读”。
```tsx
useDocumentTitle("个人中心");
```
实现:直接用 `document.title` 赋值,不能再简单。在销毁时再次给一个默认标题即可,这个简单的函数可以抽象在项目工具函数里,每个页面组件都需要调用。
```tsx
function useDocumentTitle(title) {
useEffect(
() => {
document.title = title;
return () => (document.title = "前端精读");
},
[title]
);
}
```
[在线 Demo](https://codesandbox.io/s/lrnvnx866l)
#### 监听页面大小变化,网络是否断开
效果:在组件调用 `useWindowSize` 时,可以拿到页面大小,并且在浏览器缩放时自动触发组件更新。
```tsx
const windowSize = useWindowSize();
return <div>页面高度:{windowSize.innerWidth}</div>;
```
实现:和标题思路基本一致,这次从 `window.innerHeight` 等 API 直接拿到页面宽高即可,注意此时可以用 `window.addEventListener('resize')` 监听页面大小变化,此时调用 `setValue` 将会触发调用自身的 UI 组件 rerender,就是这么简单!
最后注意在销毁时,`removeEventListener` 注销监听。
```tsx
function getSize() {
return {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerHeight: window.outerHeight,
outerWidth: window.outerWidth
};
}
function useWindowSize() {
let [windowSize, setWindowSize] = useState(getSize());
function handleResize() {
setWindowSize(getSize());
}
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return windowSize;
}
```
[在线 Demo](https://codesandbox.io/s/j2rz2mj83)
#### 动态注入 css
效果:在页面注入一段 class,并且当组件销毁时,移除这个 class。
```tsx
const className = useCss({
color: "red"
});
return <div className={className}>Text.</div>;
```
实现:可以看到,Hooks 方便的地方是在组件销毁时移除副作用,所以我们可以安心的利用 Hooks 做一些副作用。注入 css 自然不必说了,而销毁 css 只要找到注入的那段引用进行销毁即可,具体可以看这个 [代码片段](https://github.com/streamich/nano-css/blob/c21413ddbed233777886f7c9aa1375af8a221f7b/addon/pipe.js#L51)。
> DOM 副作用修改 / 监听场景有一些现成的库了,从名字上就能看出来用法:[document-visibility](https://github.com/rehooks/document-visibility)、[network-status](https://github.com/rehooks/network-status)、[online-status](https://github.com/rehooks/online-status)、[window-scroll-position](https://github.com/rehooks/window-scroll-position)、[window-size](https://github.com/rehooks/window-size)、[document-title](https://github.com/rehooks/document-title)。
### 组件辅助
Hooks 还可以增强组件能力,比如拿到并监听组件运行时宽高等。
#### 获取组件宽高
效果:通过调用 `useComponentSize` 拿到某个组件 ref 实例的宽高,并且在宽高变化时,rerender 并拿到最新的宽高。
```tsx
const ref = useRef(null);
let componentSize = useComponentSize(ref);
return (
<>
{componentSize.width}
<textArea ref={ref} />
</>
);
```
实现:和 DOM 监听类似,这次换成了利用 `ResizeObserver` 对组件 ref 进行监听,同时在组件销毁时,销毁监听。
其本质还是监听一些副作用,但通过 ref 的传递,我们可以对组件粒度进行监听和操作了。
```tsx
useLayoutEffect(() => {
handleResize();
let resizeObserver = new ResizeObserver(() => handleResize());
resizeObserver.observe(ref.current);
return () => {
resizeObserver.disconnect(ref.current);
resizeObserver = null;
};
}, []);
```
[在线 Demo](https://codesandbox.io/s/zqxp3l9yrm),对应组件 [component-size](https://github.com/rehooks/component-size)。
#### 拿到组件 onChange 抛出的值
效果:通过 `useInputValue()` 拿到 Input 框当前用户输入的值,而不是手动监听 onChange 再腾一个 `otherInputValue` 和一个回调函数把这一堆逻辑写在无关的地方。
```tsx
let name = useInputValue("Jamie");
// name = { value: 'Jamie', onChange: [Function] }
return <input {...name} />;
```
可以看到,这样不仅没有占用组件自己的 state,也不需要手写 onChange 回调函数进行处理,这些处理都压缩成了一行 use hook。
实现:读到这里应该大致可以猜到了,利用 `useState` 存储组件的值,并抛出 `value` 与 `onChange`,监听 `onChange` 并通过 `setValue` 修改 `value`, 就可以在每次 `onChange` 时触发调用组件的 rerender 了。
```tsx
function useInputValue(initialValue) {
let [value, setValue] = useState(initialValue);
let onChange = useCallback(function(event) {
setValue(event.currentTarget.value);
}, []);
return {
value,
onChange
};
}
```
这里要注意的是,我们对组件增强时,**组件的回调一般不需要销毁监听,而且仅需监听一次,这与 DOM 监听不同**,因此大部分场景,我们需要利用 `useCallback` 包裹,并传一个空数组,来保证永远只监听一次,而且不需要在组件销毁时注销这个 callback。
[在线 Demo](https://codesandbox.io/s/0xlk250l5l),对应组件 [input-value](https://github.com/rehooks/input-value)。
### 做动画
利用 React Hooks 做动画,一般是拿到一些具有弹性变化的值,我们可以将值赋给进度条之类的组件,这样其进度变化就符合某种动画曲线。
#### 在某个时间段内获取 0-1 之间的值
这个是动画最基本的概念,某个时间内拿到一个线性增长的值。
效果:通过 `useRaf(t)` 拿到 t 毫秒内不断刷新的 0-1 之间的数字,期间组件会不断刷新,但刷新频率由 requestAnimationFrame 控制(不会卡顿 UI)。
```tsx
const value = useRaf(1000);
```
实现:写起来比较冗长,这里简单描述一下。利用 `requestAnimationFrame` 在给定时间内给出 0-1 之间的值,那每次刷新时,只要判断当前刷新的时间点占总时间的比例是多少,然后做分母,分子是 1 即可。
[在线 Demo](https://codesandbox.io/s/n745x9pyy4),对应组件 [use-raf](https://github.com/streamich/react-use/blob/master/docs/useRaf.md)。
#### 弹性动画
效果:通过 `useSpring` 拿到动画值,组件以固定频率刷新,而这个动画值以弹性函数进行增减。
实际调用方式一般是,先通过 `useState` 拿到一个值,再通过动画函数包住这个值,这样组件就会从原本的刷新一次,变成刷新 N 次,拿到的值也随着动画函数的规则变化,最后这个值会稳定到最终的输入值(如例子中的 `50`)。
```tsx
const [target, setTarget] = useState(50);
const value = useSpring(target);
return <div onClick={() => setTarget(100)}>{value}</div>;
```
实现:为了实现动画效果,需要依赖 `rebound` 库,它可以实现将一个目标值拆解为符合弹性动画函数过程的功能,那我们需要利用 React Hooks 做的就是在第一次接收到目标值是,调用 `spring.setEndValue` 来触发动画事件,并在 `useEffect` 里做一次性监听,再值变时重新 `setValue` 即可。
最神奇的 `setTarget` 联动 `useSpring` 重新计算弹性动画部分,是通过 `useEffect` 第二个参数实现的:
```tsx
useEffect(
() => {
if (spring) {
spring.setEndValue(targetValue);
}
},
[targetValue]
);
```
也就是当目标值变化后,才会进行新的一轮 rerender,所以 `useSpring` 并不需要监听调用处的 `setTarget`,它只需要监听 `target` 的变化即可,而巧妙利用 `useEffect` 的第二个参数可以事半功倍。
[在线 Demo](https://codesandbox.io/s/yq0moqo8mv)
#### Tween 动画
明白了弹性动画原理,Tween 动画就更简单了。
效果:通过 `useTween` 拿到一个从 0 变化到 1 的值,这个值的动画曲线是 `tween`。可以看到,由于取值范围是固定的,所以我们不需要给初始值了。
```tsx
const value = useTween();
```
实现:通过 `useRaf` 拿到一个线性增长的值(区间也是 0 ~ 1),再通过 `easing` 库将其映射到 0 ~ 1 到值即可。这里用到了 hook 调用 hook 的联动(通过 `useRaf` 驱动 `useTween`),还可以在其他地方举一反三。
```tsx
const fn: Easing = easing[easingName];
const t = useRaf(ms, delay);
return fn(t);
```
### 发请求
利用 Hooks,可以将任意请求 Promise 封装为带有标准状态的对象:loading、error、result。
#### 通用 Http 封装
效果:通过 `useAsync` 将一个 Promise 拆解为 loading、error、result 三个对象。
```tsx
const { loading, error, result } = useAsync(fetchUser, [id]);
```
实现:在 Promise 的初期设置 loading,结束后设置 result,如果出错则设置 error,这里可以将请求对象包装成 `useAsyncState` 来处理,这里就不放出来了。
```tsx
export function useAsync(asyncFunction) {
const asyncState = useAsyncState(options);
useEffect(() => {
const promise = asyncFunction();
asyncState.setLoading();
promise.then(
result => asyncState.setResult(result);,
error => asyncState.setError(error);
);
}, params);
}
```
具体代码可以参考 [react-async-hook](https://github.com/slorber/react-async-hook/blob/master/src/index.js),这个功能建议仅了解原理,具体实现因为有一些边界情况需要考虑,比如组件 isMounted 后才能相应请求结果。
#### Request Service
业务层一般会抽象一个 `request service` 做统一取数的抽象(比如统一 url,或者可以统一换 socket 实现等等)。假如以前比较 low 的做法是:
```tsx
async componentDidMount() {
// setState: 改 isLoading state
try {
const data = await fetchUser()
// setState: 改 isLoading、error、data
} catch (error) {
// setState: 改 isLoading、error
}
}
```
后来把请求放在 redux 里,通过 connect 注入的方式会稍微有些改观:
```tsx
@Connect(...)
class App extends React.PureComponent {
public componentDidMount() {
this.props.fetchUser()
}
public render() {
// this.props.userData.isLoading | error | data
}
}
```
最后会发现还是 Hooks 简洁明了:
```tsx
function App() {
const { isLoading, error, data } = useFetchUser();
}
```
而 `useFetchUser` 利用上面封装的 `useAsync` 可以很容易编写:
```tsx
const fetchUser = id =>
fetch(`xxx`).then(result => {
if (result.status !== 200) {
throw new Error("bad status = " + result.status);
}
return result.json();
});
function useFetchUser(id) {
const asyncFetchUser = useAsync(fetchUser, id);
return asyncUser;
}
```
### 填表单
React Hooks 特别适合做表单,尤其是 [antd form](https://ant.design/components/form-cn/) 如果支持 Hooks 版,那用起来会方便许多:
```tsx
function App() {
const { getFieldDecorator } = useAntdForm();
return (
<Form onSubmit={this.handleSubmit} className="login-form">
<FormItem>
{getFieldDecorator("userName", {
rules: [{ required: true, message: "Please input your username!" }]
})(
<Input
prefix={<Icon type="user" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="Username"
/>
)}
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
Or <a href="">register now!</a>
</FormItem>
</Form>
);
}
```
不过虽然如此,`getFieldDecorator` 还是基于 RenderProps 思路的,彻底的 Hooks 思路是利用之前说的 **组件辅助方式,提供一个组件方法集,用解构方式传给组件**。
#### Hooks 思维的表单组件
效果:通过 `useFormState` 拿到表单值,并且提供一系列 **组件辅助** 方法控制组件状态。
```tsx
const [formState, { text, password }] = useFormState();
return (
<form>
<input {...text("username")} required />
<input {...password("password")} required minLength={8} />
</form>
);
```
上面可以通过 `formState` 随时拿到表单值,和一些校验信息,通过 `password("pwd")` 传给 `input` 组件,让这个组件达到受控状态,且输入类型是 `password` 类型,表单 key 是 `pwd`。而且可以看到使用的 `form` 是原生标签,这种表单增强是相当解耦的。
实现:仔细观察一下结构,不难发现,我们只要结合 **组件辅助 小节说的 “拿到组件 onChange 抛出的值” 一节的思路,就能轻松理解 `text`、`password` 是如何作用于 `input` 组件,并拿到其输入状态**。
往简单的来说,只要把这些状态 Merge 起来,通过 `useReducer` 聚合到 `formState` 就可以实现了。
为了简化,我们只考虑对 `input` 的增强,源码仅需 30 几行:
```tsx
export function useFormState(initialState) {
const [state, setState] = useReducer(stateReducer, initialState || {});
const createPropsGetter = type => (name, ownValue) => {
const hasOwnValue = !!ownValue;
const hasValueInState = state[name] !== undefined;
function setInitialValue() {
let value = "";
setState({ [name]: value });
}
const inputProps = {
name, // 给 input 添加 type: text or password
get value() {
if (!hasValueInState) {
setInitialValue(); // 给初始化值
}
return hasValueInState ? state[name] : ""; // 赋值
},
onChange(e) {
let { value } = e.target;
setState({ [name]: value }); // 修改对应 Key 的值
}
};
return inputProps;
};
const inputPropsCreators = ["text", "password"].reduce(
(methods, type) => ({ ...methods, [type]: createPropsGetter(type) }),
{}
);
return [
{ values: state }, // formState
inputPropsCreators
];
}
```
上面 30 行代码实现了对 `input` 标签类型的设置,监听 `value` `onChange`,最终聚合到大的 `values` 作为 `formState` 返回。读到这里应该发现对 React Hooks 的应用都是万变不离其宗的,特别是对组件信息的获取,通过解构方式来做,Hooks 内部再做一下聚合,就完成表单组件基本功能了。
实际上一个完整的轮子还需要考虑 `checkbox` `radio` 的兼容,以及校验问题,这些思路大同小异,具体源码可以看 [react-use-form-state](https://github.com/wsmd/react-use-form-state)。
### 模拟生命周期
有的时候 React15 的 API 还是挺有用的,利用 React Hooks 几乎可以模拟出全套。
#### componentDidMount
效果:通过 `useMount` 拿到 mount 周期才执行的回调函数。
```tsx
useMount(() => {
// quite similar to `componentDidMount`
});
```
实现:`componentDidMount` 等价于 `useEffect` 的回调(仅执行一次时),因此直接把回调函数抛出来即可。
```tsx
useEffect(() => void fn(), []);
```
#### componentWillUnmount
效果:通过 `useUnmount` 拿到 unmount 周期才执行的回调函数。
```tsx
useUnmount(() => {
// quite similar to `componentWillUnmount`
});
```
实现:`componentWillUnmount` 等价于 `useEffect` 的回调函数返回值(仅执行一次时),因此直接把回调函数返回值抛出来即可。
```tsx
useEffect(() => fn, []);
```
#### componentDidUpdate
效果:通过 `useUpdate` 拿到 didUpdate 周期才执行的回调函数。
```tsx
useUpdate(() => {
// quite similar to `componentDidUpdate`
});
```
实现:`componentDidUpdate` 等价于 `useMount` 的逻辑每次执行,除了初始化第一次。因此采用 mouting flag(判断初始状态)+ 不加限制参数确保每次 rerender 都会执行即可。
```tsx
const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});
```
#### Force Update
效果:这个最有意思了,我希望拿到一个函数 `update`,每次调用就强制刷新当前组件。
```tsx
const update = useUpdate();
```
实现:我们知道 `useState` 下标为 1 的项是用来更新数据的,而且就算数据没有变化,调用了也会刷新组件,所以我们可以把返回一个没有修改数值的 `setValue`,这样它的功能就仅剩下刷新组件了。
```tsx
const useUpdate = () => useState(0)[1];
```
> 对于 `getSnapshotBeforeUpdate`, `getDerivedStateFromError`, `componentDidCatch` 目前 Hooks 是无法模拟的。
#### isMounted
很久以前 React 是提供过这个 API 的,后来移除了,原因是可以通过 `componentWillMount` 和 `componentWillUnmount` 推导。自从有了 React Hooks,支持 isMount 简直是分分钟的事。
效果:通过 `useIsMounted` 拿到 `isMounted` 状态。
```tsx
const isMounted = useIsMounted();
```
实现:看到这里的话,应该已经很熟悉这个套路了,`useEffect` 第一次调用时赋值为 true,组件销毁时返回 false,注意这里可以加第二个参数为空数组来优化性能。
```tsx
const [isMount, setIsMount] = useState(false);
useEffect(() => {
if (!isMount) {
setIsMount(true);
}
return () => setIsMount(false);
}, []);
return isMount;
```
[在线 Demo](https://codesandbox.io/s/5zwr1l1o1n)
### 存数据
上一篇提到过 React Hooks 内置的 `useReducer` 可以模拟 Redux 的 reducer 行为,那唯一需要补充的就是将数据持久化。我们考虑最小实现,也就是全局 Store + Provider 部分。
#### 全局 Store
效果:通过 `createStore` 创建一个全局 Store,再通过 `StoreProvider` 将 `store` 注入到子组件的 `context` 中,最终通过两个 Hooks 进行获取与操作:`useStore` 与 `useAction`:
```tsx
const store = createStore({
user: {
name: "小明",
setName: (state, payload) => {
state.name = payload;
}
}
});
const App = () => (
<StoreProvider store={store}>
<YourApp />
</StoreProvider>
);
function YourApp() {
const userName = useStore(state => state.user.name);
const setName = userAction(dispatch => dispatch.user.setName);
}
```
实现:这个例子的实现可以单独拎出一篇文章了,所以笔者从存数据的角度剖析一下 `StoreProvider` 的实现。
对,Hooks 并不解决 Provider 的问题,所以全局状态必须有 Provider,但这个 Provider 可以利用 React 内置的 `createContext` 简单搞定:
```tsx
const StoreContext = createContext();
const StoreProvider = ({ children, store }) => (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
```
剩下就是 `useStore` 怎么取到持久化 Store 的问题了,这里利用 `useContext` 和刚才创建的 Context 对象:
```tsx
const store = useContext(StoreContext);
return store;
```
更多源码可以参考 [easy-peasy](https://github.com/ctrlplusb/easy-peasy),这个库基于 redux 编写,提供了一套 Hooks API。
### 封装原有库
是不是 React Hooks 出现后,所有的库都要重写一次?当然不是,我们看看其他库如何做改造。
#### RenderProps to Hooks
这里拿 [react-powerplug](https://github.com/renatorib/react-powerplug) 举例。
比如有一个 renderProps 库,希望改造成 Hooks 的用法:
```tsx
import { Toggle } from 'react-powerplug'
function App() {
return (
<Toggle initial={true}>
{({ on, toggle }) => (
<Checkbox checked={on} onChange={toggle} />
)}
</Toggle>
)
}
↓ ↓ ↓ ↓ ↓ ↓
import { useToggle } from 'react-powerhooks'
function App() {
const [on, toggle] = useToggle()
return <Checkbox checked={on} onChange={toggle} />
}
```
效果:假如我是 `react-powerplug` 的维护者,怎么样最小成本支持 React Hook? 说实话这个没办法一步做到,但可以通过两步实现。
```tsx
export function Toggle() {
// 这是 Toggle 的源码
// balabalabala..
}
const App = wrap(() => {
// 第一步:包 wrap
const [on, toggle] = useRenderProps(Toggle); // 第二步:包 useRenderProps
});
```
实现:首先解释一下为什么要包两层,首先 Hooks 必须遵循 React 的规范,我们必须写一个 `useRenderProps` 函数以符合 Hooks 的格式,**那问题是如何拿到 Toggle 给 render 的 `on` 与 `toggle`?**正常方式应该拿不到,所以退而求其次,将 `useRenderProps` 拿到的 Toggle 传给 `wrap`,**让 `wrap` 构造 RenderProps 执行环境拿到 `on` 与 `toggle` 后,调用 `useRenderProps` 内部的 `setArgs` 函数,让 `const [on, toggle] = useRenderProps(Toggle)` 实现曲线救国。**
```tsx
const wrappers = []; // 全局存储 wrappers
export const useRenderProps = (WrapperComponent, wrapperProps) => {
const [args, setArgs] = useState([]);
const ref = useRef({});
if (!ref.current.initialized) {
wrappers.push({
WrapperComponent,
wrapperProps,
setArgs
});
}
useEffect(() => {
ref.current.initialized = true;
}, []);
return args; // 通过下面 wrap 调用 setArgs 获取值。
};
```
由于 `useRenderProps` 会先于 `wrap` 执行,所以 wrappers 会先拿到 Toggle,`wrap` 执行时直接调用 `wrappers.pop()` 即可拿到 Toggle 对象。然后构造出 RenderProps 的执行环境即可:
```tsx
export const wrap = FunctionComponent => props => {
const element = FunctionComponent(props);
const ref = useRef({ wrapper: wrappers.pop() }); // 拿到 useRenderProps 提供的 Toggle
const { WrapperComponent, wrapperProps } = ref.current.wrapper;
return createElement(WrapperComponent, wrapperProps, (...args) => {
// WrapperComponent => Toggle,这一步是在构造 RenderProps 执行环境
if (!ref.current.processed) {
ref.current.wrapper.setArgs(args); // 拿到 on、toggle 后,通过 setArgs 传给上面的 args。
ref.current.processed = true;
} else {
ref.current.processed = false;
}
return element;
});
};
```
以上实现方案参考 [react-hooks-render-props](https://github.com/dai-shi/react-hooks-render-props),有需求要可以拿过来直接用,不过实现思路可以参考,作者的脑洞挺大。
#### Hooks to RenderProps
好吧,如果希望 Hooks 支持 RenderProps,那一定是希望同时支持这两套语法。
效果:一套代码同时支持 Hooks 和 RenderProps。
实现:其实 Hooks 封装为 RenderProps 最方便,因此我们使用 Hooks 写核心的代码,假设我们写一个最简单的 `Toggle`:
```tsx
const useToggle = initialValue => {
const [on, setOn] = useState(initialValue);
return {
on,
toggle: () => setOn(!on)
};
};
```
[在线 Demo](https://codesandbox.io/s/ppvrpnz80m)
然后通过 `render-props` 这个库可以轻松封装出 RenderProps 组件:
```tsx
const Toggle = ({ initialValue, children, render = children }) =>
renderProps(render, useToggle(initialValue));
```
[在线 Demo](https://codesandbox.io/s/249n3n4r30)
其实 `renderProps` 这个组件的第二个参数,在 Class 形式 React 组件时,接收的是 `this.state`,现在我们改成 `useToggle` 返回的对象,也可以理解为 `state`,利用 Hooks 机制驱动 Toggle 组件 rerender,从而让子组件 rerender。
#### 封装原本对 setState 增强的库
Hooks 也特别适合封装原本就作用于 setState 的库,比如 [immer](https://github.com/mweststrate/immer)。
`useState` 虽然不是 `setState`,但却可以理解为控制高阶组件的 `setState`,我们完全可以封装一个自定义的 `useState`,然后内置对 `setState` 的优化。
比如 immer 的语法是通过 `produce` 包装,将 mutable 代码通过 Proxy 代理为 immutable:
```tsx
const nextState = produce(baseState, draftState => {
draftState.push({ todo: "Tweet about it" });
draftState[1].done = true;
});
```
那这个 `produce` 就可以通过封装一个 `useImmer` 来隐藏掉:
```tsx
function useImmer(initialValue) {
const [val, updateValue] = React.useState(initialValue);
return [
val,
updater => {
updateValue(produce(updater));
}
];
}
```
使用方式:
```tsx
const [value, setValue] = useImmer({ a: 1 });
value(obj => (obj.a = 2)); // immutable
```
## 3 总结
本文列出了 React Hooks 的以下几种使用方式以及实现思路:
- DOM 副作用修改 / 监听。
- 组件辅助。
- 做动画。
- 发请求。
- 填表单。
- 模拟生命周期。
- 存数据。
- 封装原有库。
欢迎大家的持续补充。
---
## 公众号
想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新.
**简历模板:** 关注公众号回复「模板」获取
**《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取
![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png)