在 React 16 中,除去 Fiber 架构外,Hooks 是最使人激动的一个特性,相比于 class component,Hooks 加持后的 function component 在写法与思路上都大有不一样,不少时候显得更为简洁与清爽(熵更低,弱化生命周期的概念),同时解决了使人烦恼的 this 指针指向问题,仍是很香的。react
但理性来讲,到目前为止,hooks 仍是一个坑不少的阶段,而且也缺少一个成体系的最佳实践,如下谈谈我对 hooks 的一些浅薄的认识。git
那么,是时候发车了。github
咱们能够把这里的 state 看作咱们在 class component 中使用的 this.state。ajax
咱们的每一次 setState 操做,在改变了值以后,都会引起 rerender 操做,从而触发页面的更新(可是若是没有改变的话,则不会触发 rerender,咱们在后面将会利用这一特性作一件有趣的事情)。编程
同时,setState 能够以函数做为参数,这个时候咱们能够获取到最新的 state 值(在第一个回调参数)。redux
import React, { useState } from 'react';
function App() {
const [ state, setState ] = useState(0);
return (
<span>{state}</span>
)
}
复制代码
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 与 useEffect 的不一样在于,useLayoutEffect 会在 DOM 渲染以前执行,而 useEffect 会在 DOM 渲染以后执行,因此咱们能够利用这个特性,避免一些因为 DOM 渲染以后进行操做致使的白屏问题。性能优化
useCallback 能够帮助咱们缓存函数(useMemo一样能够作到,写法不一样),经过手动控制依赖,作到减小由于函数的更新致使子组件的更新(带来的性能问题很是明显)antd
import React, { useCallback } from 'react';
function App() {
const cb = useCallback(() => { console.log('callback') }, []);
return (
<button onClick={cb}></button>
)
}
复制代码
useMemo 能够为咱们的 function component 提供缓存的能力,在一些重计算的场景下,能够减小重复计算的次数,起到明显的性能提高。 固然,useMemo一样能够用来缓存组件,起到相似与 class component 中 shouldComponentUpdate 的做用,让咱们手动经过管理依赖的方式作到控制子组件的更新(固然这个手动管理的成本是很是高的)
由于在 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 事实上是对 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];
}
复制代码
假定咱们已经有了一个 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> ) } 复制代码
咱们应该树立一个理念,在 function component 中,全部的状态,都是隶属于它的闭包的,因此致使了 咱们每一轮的 Render 都会有本身的一个闭包,全部的 useEffect 与 useLayoutEffect 都在其最后一次更新的闭包中 Hooks处理请求
hooks 编程有些相似于响应式编程,同时,为了能够老是拿到最正确的值,正确的去书写 hooks依赖 是很是重要的,也就是所谓的对依赖诚实,这样才能保证咱们最终发送请求之时,能够取到正确的 state 和 props。
为了可以正确的处理请求,有一种想法是——将请求的函数放置于 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';
复制代码
利用 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>
)
}
复制代码
咱们能够作相似于 class component 中的 PureComponent 这样的操做,咱们能够用 React.memo 包裹大部分的组件(会带来额外的比较,性能不必定是最佳的)
利用 React.memo,咱们能够作到让 React 对咱们的组件进行浅比较,
const Child = function({ a, b, c }) {
return <div>{a}{b}{c}</div>
}
export default React.memo(Child);
复制代码
function App({ a, b, c }) {
const RenderComponent = useMemo(() => {
return <div>{c}</div>
}, [c]);
return (
<RenderComponent /> ) } 复制代码
在这里,可使用我上面所介绍的 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 };
}
复制代码
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];
};
复制代码
不少时候,咱们都会依赖于 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 是否能够像是高阶组件那样,为咱们返回一个组件呢,答案是能够的,而且利用这样的能力,咱们还能够简化不少状况下咱们的编程。
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 进行的表单的注册和提交拦截(我的认为也是一种很是清奇的思路)
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 + 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。
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。
多是目前社区中得到 star 和关注最多的自定义 hooks 项目,提供了很是多的自定义 hooks(不少是香的) react-use
在 React hooks 雨后春笋般的请求库中,最为亮眼的当属于 swr。详情请见官方 github 仓库 swr
一个很好用的 react-hook-form 表单库。详情请见官方 github 仓库 react-hook-form