在 React 16 中,除去 Fiber 架构外,Hooks 是最令人激动的一个特性,相比于 class component,Hooks 加持后的 function component 在写法与思路上都大有不同,很多时候显得更为简洁与清爽(熵更低,弱化生命周期的概念),同时解决了令人烦恼的 this 指针指向问题,还是很香的。
但理性来说,到目前为止,hooks 还是一个坑很多的阶段,并且也缺乏一个成体系的最佳实践,以下谈谈我对 hooks 的一些浅薄的认识。
那么,是时候发车了。
常用Hooks
useState
我们可以把这里的 state 看做我们在 class component 中使用的 this.state。
我们的每一次 setState 操作,在改变了值之后,都会引发 rerender 操作,从而触发页面的更新(但是如果没有改变的话,则不会触发 rerender,我们在后面将会利用这一特性做一件有趣的事情)。
同时,setState 可以以函数作为参数,这个时候我们可以获取到最新的 state 值(在第一个回调参数)。
import React, { useState } from 'react';
function App() {
const [ state, setState ] = useState(0);
return (
<span>{state}</span>
)
}
useEffect
useEffect可以说是所有 hooks API 中最像是声明周期的钩子了,很容易让人理解成为,如果依赖数组为空,那么它等价为 componentDidMount,但是真的这样吗?
我们可以这样去理解我们的函数组件,函数组件的每次运行都相当于 class component 中的一次 render,每轮都会保留它的闭包,所以,我们的 useEffect 实际保留了它运行轮次的 state 和 props 状态(如果依赖不更新,那么状态不更新),这也就是 useEffect 和 componentDidMount 生命周期的关系。
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('I am mount');
return () => {
console.log('before next run, I am cleaned');
}
}, []);
useLayoutEffect
useLayoutEffect 与 useEffect 的不同在于,useLayoutEffect 会在 DOM 渲染之前执行,而 useEffect 会在 DOM 渲染之后执行,所以我们可以利用这个特性,避免一些由于 DOM 渲染之后进行操作导致的白屏问题。
useCallback
useCallback 可以帮助我们缓存函数(useMemo同样可以做到,写法不同),通过手动控制依赖,做到减少因为函数的更新导致子组件的更新(带来的性能问题非常明显)
import React, { useCallback } from 'react';
function App() {
const cb = useCallback(() => { console.log('callback') }, []);
return (
<button onClick={cb}></button>
)
}
useMemo
useMemo 可以为我们的 function component 提供缓存的能力,在一些重计算的场景下,可以减少重复计算的次数,起到明显的性能提升。 当然,useMemo同样可以用来缓存组件,起到类似与 class component 中 shouldComponentUpdate 的作用,让我们手动通过管理依赖的方式做到控制子组件的更新(当然这个手动管理的成本是非常高的)
useRef
因为在 hooks 中,我们所声明的所有变量是只属于它的闭包的,所以,我们无法做到变量的一个共享。因为 immutable 的 state 并不适合我们存储一些不参与 UI 显示的变量。hooks 为我们提供了 useRef 去存储 mutable 的不参与 UI 显示的变量,并且可以在每一轮 render 中共享。
useRef 不仅可以用来存储对 Dom 元素的引用(它的本意),更可以用来存储我们需要在每轮 render 中共享的 mutable 的变量(可能非常常用)。
import React, { useRef } from 'react';
function App() {
const td = useRef(1);
console.log(td.current); // 1
...
useReducer
在当前版本中的 useReducer 事实上是对 useState 的一层封装,实现了 redux 的一套原理(之前的版本是 useState 是对 useReducer 的一层封装)
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
useContext
假定我们已经有了一个 Context ,并且我们的子组件已经在 Provider 包裹下,我们可以直接使用 useContext 去获取值,而非使用回调去获取值。 同时,我们也可以对某些 Context 进行 useContext 的封装,让我们可以在不同的组件中方便的使用 Context 中的数据。
// 假定我们已经有 Context
function Child(props) {
const { value } = useContext(Context);
return (
<div>
{value}
</div>
)
}
我们可以将 Context 、useReducer 与 useContext 结合起来,打造我们自己的 Redux
const CTX = React.createContext(null);
const reducer = (state, action) => {
switch(action.type) {
default:
reutrn state;
}
}
const Context = function({ children }) {
const [state, dispatch] = useReducer(reducer, {});
return (
<CTX.Provider value={ state, dispatch }>
{children}
</CTX.Provider>
)
}
怎么理解Hooks中的状态
我们应该树立一个理念,在 function component 中,所有的状态,都是隶属于它的闭包的,所以导致了 我们每一轮的 Render 都会有自己的一个闭包,所有的 useEffect 与 useLayoutEffect 都在其最后一次更新的闭包中 Hooks处理请求
正确处理依赖
hooks 编程有些类似于响应式编程,同时,为了可以总是拿到最正确的值,正确的去书写 hooks依赖 是非常重要的,也就是所谓的对依赖诚实,这样才能保证我们最终发送请求之时,可以取到正确的 state 和 props。
放置依赖的请求于 useEffect 中
为了能够正确的处理请求,有一种想法是——将请求的函数放置于 useEffect 中,这样子就可以确保我们每时每刻都会去正确的处理其中的依赖问题。 处理竞态 我们知道,在 hooks 里,每一次 Render 以及 每一次 useEffect 的执行都是在它自己所处轮次的闭包中,所以,我们处理竞态的一个思路就来源于这里。
我们的依赖变化会触发我们的 ajax 操作,所以当第二次请求发生时,实际上上一次 effect 已经到了清理副作用时期,所以执行了 return 中的函数,将我们的flag置为true,这样,当我们的请求返回之时,其effect 所在的闭包是可以感知到执行结束的状态的,从而抛弃旧值,达到对竞态的正确处理。
useEffect(() => {
let flag = false;
ajax().then(res => {
if (!flag) {
//...do something
}
})
return () => {
flag = true;
}
}, [deps])
请求与触发分离
请求
我们可以将请求函数用普通函数的方法,放置于整个 function 中,这样足以确保我们这个函数能够拿到当前 render 轮次所依赖的 state 和 props,如果有性能方面的顾虑,可以考虑使用 useCallback 去进行包装(但此时一定要对依赖诚实)
function App() {
const [flag, setFlag] = useState(0);
const ajax = () => {
_ajax(props)
};
useEffect(() => {
ajax();
}, [flag]);
return (
...
)
}
触发
这个时候一定要注意的一点是,我们的触发 flag,一定要在最后修改(先进行预操作——其它的 state 修改),确定我们的 effect 更新时,索引用的,是最新的 ajax 请求函数。
非渲染参数使用 ref 进行保存 因为我们在 effect 中,永远可以正确的获取到 ref 值,所以,当我们的参数不参与渲染时,我们可以用 useRef 生成的 ref 对其进行管理,这样我们就可以不用去担心由于 ref 所引用参数的变化问题(同时,也不会触发页面的 rerender)
const name = useRef('小明')
const ajax = useCallback(() => {
ajax({ name })
}, []);
// 修改 param 直接操作 ref
name.current = '123';
极限性能Trick
减少计算
依赖数组欺骗
利用 setState 的回调处理获取 state 的问题 因为
const [state, setState] = useState(0);
// 利用 setState 的回调拿到最新的 state,返回原值,可以不触发 rerender(极端情况下可以用于性能优化)
const update = useCallback(() => {
setState(state => {
// 做你想做的任何事情
return state;
})
}, []);
试想一个很骚的场景,如果我们使用 setter 嵌套(并且都返回原始值),那么我们是不是可以在无任何依赖情况下用 state 做任何想做的事情呢(代码可读性忽略)
const trigger = useCallback(() => {
setState1(state1 => {
setState2(state2 => {
console.log(state1 + state2);
return state2;
})
return state1;
})
});
利用 useReducer 和 setState 结合处理获取 state 和 props 的问题 因为上面的方法,我们只能确保我们可以无依赖的拿到 state ,但是我们却不能在无依赖的情况下拿到 props 那么我们可以怎么办呢。 我们可能把 useReducer 的 reducer 放在 function component 函数体内,利用 dispatch 最终触发的是最新的闭包中的 reducer 来确保我们可以拿到处于最新状态的 props
function App({ a, b, c }) {
const reducer = (state, action) => {
switch(action.type) {
case 'init':
// 这里永远可以拿到最新的 a
return Object.assign(state, { a: a });
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, {});
return (
<div>{ state.a }</div>
)
}
减少Render
我们可以做类似于 class component 中的 PureComponent 这样的操作,我们可以用 React.memo 包裹大部分的组件(会带来额外的比较,性能不一定是最佳的)
直接使用 React.memo
利用 React.memo,我们可以做到让 React 对我们的组件进行浅比较,
const Child = function({ a, b, c }) {
return <div>{a}{b}{c}</div>
}
export default React.memo(Child);
使用 useMemo 进行细粒度的控制
function App({ a, b, c }) {
const RenderComponent = useMemo(() => {
return <div>{c}</div>
}, [c]);
return (
<RenderComponent />
)
}
使用 useCallback 包裹函数,使函数变化减少
在这里,可以使用我上面所介绍的 trick ,减少依赖的数量,从而减少 rerender 的次数 Eg: trigger.jsx 开关
const useTrigger = () => {
const [state, setState] = useState(false);
const trigger = useCallback(() => {
setState(ste => !ste);
}, []);
return { state, trigger };
}
// vs
const useTrigger = () => {
const [state, setState] = useState(false);
const trigger = useCallback(() => {
setState(ste => !ste);
}, [state]);
return { state, trigger };
}
Hooks 实践
谈谈表单
像双向数据绑定那样编写表单
const useInput = () => {
const [value, setValue] = useState('');
const onChange = val => {
setValue(val.target.value);
};
return {
value,
onChange
};
};
表单提交
export const useSubmit = submitFunction => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [res, setRes] = useState(null);
const trigger = useCallback(() => {
try {
if (_.isFunction(submitFunction)) {
(async () => {
let res = await submitFunction();
if (res) {
setRes(res);
}
})();
}
} catch (e) {
setError(e);
} finally {
setLoading(true);
}
}, [submitFunction]);
return [loading, res, error];
};
利用 useMemo 操作 props 数据
很多时候,我们都会依赖于 props 去计算我们的 state,在 class component 中给我们提供了 getDerivedStateFromProps 生命周期供我们去做类似的操作,但是在 hooks 里,我们并没有这样的生命周期的概念,那我们应该如何去做呢?
我们可以利用 useMemo 去进行对 props 的计算操作,通过正确处理依赖,就可以籍由 useMemo 的记忆特性,让我们以最小的成本去正确的更新 state (高成本的方案是每一次去计算将值赋给闭包中的普通变量)。
import React, { useMemo } from 'react';
function App({ data }) {
// 只有 data 更新时重新计算
const info = useMemo(() => {
// 对 data 进行一系列的计算操作
return newData;
}, [data]);
}
利用 hooks 返回组件
之前所说的大都是利用 hooks 去处理逻辑问题,那么 hooks 是否可以像是高阶组件那样,为我们返回一个组件呢,答案是可以的,并且利用这样的能力,我们还可以简化很多情况下我们的编程。
import React, { useState, useCallback } from 'react';
import { Modal } from 'antd';
export default function useModal() {
const [show, setShow] = useState<boolean>(false);
const openModal = useCallback(() => {
setShow(true);
}, []);
const closeModal = useCallback(() => {
setShow(false);
}, []);
const CusModal: React.SFC = ({ children, ...props }) => {
return (
<Modal
visible={show}
{...props}>
{children}
</Modal>
)
}
return {
show,
setShow,
openModal,
closeModal,
CusModal
}
}
利用 ref hooks 进行一些无侵入操作(react 官方 不推荐) 因为 ref 可以拿到原始 dom,我们可以利用这个特性做一些操作,例如说侵入代码性的埋点迁移至 ref(减少对原始代码侵入)
eg:利用 ref 记录停留时间(可以做无侵入埋点)
export const useHoverTime = eventName => {
const EV = `${ eventName}`;
const ref = useRef(null);
useEffect(() => {
localStorage.setItem(EV, 0);
return () => {
const time = localStorage.getItem(EV);
// do something
localStorage.setItem(EV, null);
};
}, []);
useEffect(() => {
let startTime = null;
let endTime = null;
const overHandler = () => {
startTime = new Date();
};
const outHandler = () => {
endTime = new Date();
localStorage.setItem(
EV,
parseInt(localStorage.getItem(EV)) +
parseInt(endTime - startTime)
);
startTime = 0;
endTime = 0;
};
if (ref.current) {
ref.current.addEventListener('mouseover', overHandler);
ref.current.addEventListener('mouseout', outHandler);
}
return () => {
if (ref.current) {
ref.current.removeEventListener('mouseover', overHandler);
ref.current.removeEventListener('mouseout', outHandler);
}
};
}, [ref]);
return ref;
};
React-hook-form 利用 ref 进行的表单的注册和提交拦截(个人认为也是一种非常清奇的思路)
Hooks with Immer.js
immutable.js 的使用复杂度是非常高的,但是有时候我们又希望我们的 React App 性能更好,节省不必要的 rerender,那么 Immer.js 就是一个非常好的选择(事实上dva也使用了immer作为底层库)
我们可以在使用 useReducer 的时候,使用 Immer 进行状态的变更,从而使得我们最新的 state 是 immutable 的。
const reducer = (state, action) => {
switch (action.type) {
case 'initData':
return produce(state, draft => {
draft.data = action.data;
});
让 useReducer 用上 Redux 中间件生态
从何种角度看,useReducer + useContext + Context 的组合都在做传统 Redux 所在做的事情,那么,有没有可能让我们的原生 hooks 使用上 Redux 的中间件呢(本质上劫持了 action ,与 Redux 的 Api 无关)?! 是可以的,事实上,这里相当于把 Redux 中间件的实现迁移到了 hooks 上,我们当然可以自己实现,但是 react-use 这个库里帮我们做了集成,我们可以方便的直接使用它。
// 创建增强了中间件的 reducer , 这里的例子增加了 redux-logger 与 redux-thunk
const useLoggerReducer = createReducer(logger, thunk);
export default function App() {
const [state, dispatch] = useLoggerReducer(reducer, initState);
这样子,我们便可以利用 redux-thunk、redux-saga 等中间件进行异步任务的处理,使用 redux-logger 进行 action 的打印和前后 state 的 diff。
打造自己的 combindReducer (代码思路来源于 Medium)
const combineReducers = (reducers) => {
const keys = Object.keys(reducers);
const initObj = {};
keys.forEach(key => {
let draftState = reducers[key](undefined, { type: '' });
if (!draftState) {
draftState = {};
console.warn(
`[Error]: 在combineReducers 中 Reducer 需要初始化!`
);
}
initObj[key] = draftState;
})
return (state, action) => {
keys.forEach(key => {
const prevState = initObj[key];
initObj[key] = reducers[key](prevState, action);
});
return { ...initObj };
}
}
将它和我们的增强后的 useReducer 结合起来,我们便拥有了一个几乎可以媲美 redux 的 reducer。
Hooks 第三方工具集合 react-use
可能是目前社区中获得 star 和关注最多的自定义 hooks 项目,提供了非常多的自定义 hooks(很多是香的) react-use
Hooks 请求工具 swr
在 React hooks 雨后春笋般的请求库中,最为亮眼的当属于 swr。详情请见官方 github 仓库 swr
Hooks 第三方表单库 react-hook-form
一个很好用的 react-hook-form 表单库。详情请见官方 github 仓库 react-hook-form