## 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
页面高度:{windowSize.innerWidth}
;
```
实现:和标题思路基本一致,这次从 `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 Text.
;
```
实现:可以看到,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}
>
);
```
实现:和 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 ;
```
可以看到,这样不仅没有占用组件自己的 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 setTarget(100)}>{value}
;
```
实现:为了实现动画效果,需要依赖 `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: any, params: any[]) {
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 asyncFetchUser;
}
```
### 填表单
React Hooks 特别适合做表单,尤其是 [antd form](https://ant.design/components/form-cn/) 如果支持 Hooks 版,那用起来会方便许多:
```tsx
function App() {
const { getFieldDecorator } = useAntdForm();
return (
);
}
```
不过虽然如此,`getFieldDecorator` 还是基于 RenderProps 思路的,彻底的 Hooks 思路是利用之前说的 **组件辅助方式,提供一个组件方法集,用解构方式传给组件**。
#### Hooks 思维的表单组件
效果:通过 `useFormState` 拿到表单值,并且提供一系列 **组件辅助** 方法控制组件状态。
```tsx
const [formState, { text, password }] = useFormState();
return (
);
```
上面可以通过 `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 的项是用来更新数据的,但数据必须有变化才会触发 render,因此我们可以这样设计:
```tsx
const useUpdate = () => {
const [, setState] = useState(0);
return () => setState(cnt => cnt + 1);
};
```
或者利用 `useReducer` 做一个简单的 Action 来支持:
```tsx
const [, forceRender] = useReducer(s => s + 1, 0);
```
> 感谢:感谢用户 [cike8899](https://github.com/cike8899) 对此处的勘误,并提供示例代码。
> 对于 `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 = () => (
);
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 }) => (
{children}
);
```
剩下就是 `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 (
{({ on, toggle }) => (
)}
)
}
↓ ↓ ↓ ↓ ↓ ↓
import { useToggle } from 'react-powerhooks'
function App() {
const [on, toggle] = useToggle()
return
}
```
效果:假如我是 `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 副作用修改 / 监听。
- 组件辅助。
- 做动画。
- 发请求。
- 填表单。
- 模拟生命周期。
- 存数据。
- 封装原有库。
欢迎大家的持续补充。
## 4 更多讨论
> 讨论地址是:[精读《怎么用 React Hooks 造轮子》 · Issue #112 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/112)
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**