引入
Hook 是 React 16.8 的新增特性,由于目前前端的 React 项目版本都高于此版本,因此可以直接在项目中使用。
你也可以基于 React 提供的 hook 开发和封装更加通用和高级的自定义 hook。
自定义 Hook
模拟类组件生命周期
useMount
import { useEffect } from 'react';
const useMount = fn => {
useEffect(() => {
if (typeof fn === 'function') {
fn()
}
}, []);
}
useUnmount
import { useEffect } from 'react';
const useUnmount = fn => {
useEffect(() => () => {
fn();
}, []);
}
useUpdate
import { useEffect, useRef } from 'react';
const useUpdate = (fn, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return fn();
}
}, deps);
};
从Redux批量获取model
useConnect
import { useSelector } from 'umi';
export default function useConnect(models = []) {
return useSelector(state => {
const modelMap = {};
models.forEach(model => {
if (model in state) {
modelMap[model] = state[model];
}
});
return modelMap;
});
}
管理定时器
import { useEffect, useRef } from "react";
const useInterval = (fn: Function, timestamp = 300) => {
const fnRef = useRef<Function>();
fnRef.current = fn;
useEffect(() => {
const timer = setInterval(() => {
fnRef.current?.();
}, timestamp);
return () => {
clearInterval(timer);
};
}, [timestamp]);
};
export default useInterval;
第三方Hook库
按照第三方库的官方文档下载使用。
例如 ahooks
// 安装依赖
npm i ahooks --save
// 使用 Hooks
import { useRequest } from 'ahooks';
规范
[强制] 仅从函数组件调用 Hooks 。
不要从常规 JavaScript 函数中调用 Hooks。仅从函数组件或自定义 Hooks 中调用 Hooks。
[强制] 仅在顶级调用 Hooks 。
不要在循环、条件和嵌套函数内调用 Hooks。当你想有条件地使用某些 Hooks 时,请在这些 Hooks 中写入条件。
// bad
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
// good
useEffect(function persistForm() {
if (name !== '') {
localStorage.setItem('formData', name);
}
});
[强制] 自定义 Hook 的函数名必须以 use 开头。
// bad
const customBoolean = () => {}
// good
const useBoolean = () => {}
[强制] 函数式组件使用函数表达式配合箭头函数进行声明。
// bad
function FunctionalComponent(props) {
return <h1>This is a functional component.</h1>;
}
const FunctionalComponent = function(props) {
return <h1>This is a functional component.</h1>;
}
// good
const FunctionalComponent = (props) => {
return <h1>This is a functional component.</h1>;
};
[建议] 先使用 useState Hook 声明状态变量,然后使用 useEffect Hook 编写订阅,接着编写与组件作业相关的其他函数。最后,你得返回要由浏览器渲染的元素
[建议] 复杂的状态保存到 Redux 中。
function App() {
const [user, setUser] = useState(null);
const [name, setName] = useState('');
useEffect(() => {
console.log("component is mounted");
}, []);
return <div>React component order</div>;
}
[建议] 局部状态不推荐使用 useReducer 。
会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。
[建议] 使用 useContext 避免 prop drilling。
prop-drilling 是 React 应用程序中的常见问题,指的是将数据从一个父组件向下传递,经过各层组,直到到达指定的子组件,而其他嵌套组件实际上并不需要它们。
[建议] 减少在 函数组件内部声明函数。
函数式组件在每一次渲染的过程中都会生成独立的所用域,因此,在组件内部的子函数和变量等在每次渲染的时候都会重新生成。
// bad
function App() {
const [counter, setCounter] = useState(0);
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={() => setCounter(prevState => ++prevState)}>
Increment
</button>
</div>
);
}
// good
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={()=>onClick(setCounter)}>
Increment
</button>
</div>
);
[建议] 使用 useCallback 包裹很少变化的函数,使用 useMemo 包裹昂贵的计算。
// good
function App(props) {
const [counter, setCounter] = useState(0);
const result = useMemo(() => {
// 一些昂贵的计算操作
// ...
return ...
}, []);
const onClick = useCallback(()=>{
// 依赖少或者依赖项很少改变的函数
// ...
},[props.count]);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={onClick}>
Increment
</button>
</div>
);
}
[建议] 对于不需要重复初始化的对象使用 useRef 存储。
为什么有时候会出现无限重复请求的问题?
你可能使用了错误的依赖。
这个通常发生于你在effect里做数据请求并且没有设置effect依赖参数的情况。没有设置依赖,effect会在每次渲染后执行一次,然后在effect中更新了状态引起渲染并再次触发effect。无限循环的发生也可能是因为你设置的依赖总是会改变。你可以通过一个一个移除的方式排查出哪个依赖导致了问题。但是,移除你使用的依赖(或者盲目地使用[])通常是一种错误的解决方式。你应该做的是解决问题的根源。举个例子,函数可能会导致这个问题,你可以把它们放到effect里,或者提到组件外面,或者用useCallback包一层。useMemo 可以做类似的事情以避免重复生成对象。
为什么有时候在effect里拿到的是旧的state或prop呢?
可能的原因:
你的依赖列表可能缺失了某些依赖。
这种情况,只需要检查 effect 并添加正确的依赖。
你在异步操作中获取了state或prop。
通过 useRef 保存需要在异步操作中保存的变量。
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。
当state为数组或者对象时,如何更新?
Hook 更新状态与类组件的 setState 的方式不同,后者是合并,而前者是替换,因此,你需要手动合并需要更新的对象或者数组,然后更新它。
const [user, setUser] = useState({id: 1, name: 'Bob'});
useEffect(() => {
getUser.then((res) => {
setUser({...user, name: res.name}); // 合并新值到旧对象中
})
}, [user]);
然而,React 推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。
如何正确地在useEffect里请求数据?[]又是什么?
[]表示effect没有使用任何React数据流里的值,因此该effect仅被调用一次是安全的。[]同样也是一类常见问题的来源,也即你以为没使用数据流里的值但其实使用了。你需要学习一些策略(主要是useReducer 和 useCallback)来移除这些effect依赖,而不是错误地忽略它们。
如何使用React Hooks获取数据?
我应该把函数当做effect的依赖吗?
尽量将函数和 effect 解耦。
在 effect 内部去声明它所需要的函数。
尝试把那个函数移动到你的组件之外。
在 effect 之外调用它。
把函数加入 effect 的依赖但把它的定义包裹进 useCallback。
依赖数组是如何比较?
Object.is 。