前言
这篇文章主要介绍了React Hooks的一些实践用法和场景,遵循我个人一贯的思(tao)路(是什么-为什么-怎么做)
是什么
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
简单来说,上面这段官腔大概翻(xia)译(shuo)就是告诉我们class能够做到的老子用hooks基本可以做到,放弃抵抗吧,少年!
其实按照我自己的看法:React Hooks是在函数式组件中的一类以use为开头命名的函数。 这类函数在React内部会被特殊对待,所以也称为钩子函数。
- 函数式组件
Hooks只能用于Function Component, 其实这么说不严谨,我更喜欢的说法是建议只在于Function Component使用Hooks
- use开头
React 约定,钩子一律使用use前缀命名,便于识别,这没什么可说的,要被特殊对待,就要服从一定的规则
- 特殊对待
Hooks作为钩子,存在与每个组件相关联的“存储器单元”的内部列表。 它们只是我们可以放置一些数据的JavaScript对象。 当你像使用useState()一样调用Hook时,它会读取当前单元格(或在第一次渲染时初始化它),然后将指针移动到下一个单元格。 这是多个useState()调用每个get独立本地状态的方式
为什么
解决为什么要使用hooks的问题,我决定从hooks解决了class组件的哪些痛点和hooks更符合react的组件模型两个方面讲述。
1. class组件不香吗?
class组件它香,但是暴露的问题也不少。Redux 的作者 Dan Abramov总结了几个痛点:
- Huge components that are hard to refactor and test.
- Duplicated logic between different components and lifecycle methods.
- Complex patterns like render props and higher-order components.
第一点:难以重构和测试的巨大组件。 如果让你在一个代码行数300+的组件里加一个新功能,你不慌吗?你尝试过注释一行代码,结果就跑不了或者逻辑错乱吗?如果需要引入redux或者定时器等那就更慌了~~
第二点:不同组件和生命周期方法之间的逻辑重复。 这个难度不亚于蜀道难——难于上青天!当然对于简单的逻辑可能通过HOC和render props来解决。但是这两种解决办法有两个比较致命的缺点,就是模式复杂和嵌套。
第三点:复杂的模式,比如render props和 HOC。 不得不说我在学习render props的时候不禁发问只有在render属性传入函数才是render props吗?好像我再任意属性(如children)传入函数也能实现一样的效果; 一开始使用HOC的时候打开React Develops Tools一看,Unknown是什么玩意~看着一层层的嵌套,我也是无能为力。
以上这三点都可以通过Hooks来解决(疯狂吹捧~)
2. hooks更符合React的编程模型?
我们知道,react强调单向数据流和数据驱动视图,说白了就是组件和自上而下的数据流可以帮助我们将UI分割,像搭积木一样实现页面UI。这里更加强调组合而不是嵌套,class并不能很完美地诠释这个模型,但是hooks配合函数式组件却可以!函数式组件的纯UI性配合Hooks提供的状态和副作用可以将组件隔离成逻辑可复用的独立单元,逻辑分明的积木他不香吗!
怎么做
别问,问就是文档,如果不行的话,请熟读并背诵文档...
但是(万事万物最怕But), 既然是实践,就得假装实践过,下面就说说本人的简单实践和想法吧。
1. 转变心智模型
- state一把梭
// in class component
class Demo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'Hello',
age: '18',
rest: {},
}
}
...
}
// in function component
function Demo(props) {
const initialState = {
name: 'Hello',
age: '18',
rest: {},
}
const [state, setState] = React.useState(initialState)
...
}
- 尝试模拟生命周期
// 这么实现很粗糙,可以配合useRef和useCallback,但即使这样也不完全等价于componentDidMount
function useDidMount(handler){
React.useEffect(()=>{
handler && handler()
}, [])
}
- 在useEffect使用setInterval有时会事与愿违
// count更新到1就不动了
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
...
}
其实,在class component环境下思考问题更像是在特定的时间点做特定的事情,例如我们会在constructor中初始化state,会在组件挂载后(DidMount)请求数据等,会在组件更新后(DidUpdate)处理状态变化的逻辑,会在组件卸载前(willUnmount)清除一些副作用
然而在hooks+function component环境下思考问题应该更趋向于特定的功能逻辑,以功能为一个单元去思考问题会有一种豁然开朗的感觉。例如改变document的title、网络请求、定时器... 对于hooks,只是为了实现特定功能的工具而已
你会发现大部分你想实现的特定功能都是有副作用(effect)的,可以负责任的说useEffect是最干扰你心智模型的Hooks, 他的心智模型更接近于实现状态同步,而不是响应生命周期事件。还有一个可能会影响你的就是每一次渲染都有它自己的资源,具体表现为以下几点
- 每一次渲染都有它自己的Props 和 State:当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的状态值,这个状态值是函数中的一个常量(也就是会说,在任意一次渲染中,props和state是始终保持不变的)
- 每一次渲染都有它自己的事件处理函数:和props和state一样,它们都属于一次特定的渲染,即便是异步处理函数也只能拿到那一次特定渲染的状态值
- 每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state(建议在分析问题时,将每次的渲染的props和state都常量化)
2. 所谓Hooks实践
useState —— 相关的状态放一起
- 不要所有state一把梭,可以写多个useState,基本原则是相关的状态放一起
- setXX的时候建议使用回调的形式setXXX(xxx => xxx...)
- 管理复杂的状态可以考虑使用useReducer(如状态更新依赖于另一个状态的值)
// 实现计数功能
const [count, setCount] = React.useState(0);
setCount(count => count + 1)
// 展示用户信息
const initialUser = {
name: 'Hello',
age: '18',
}
const [user, setUser] = React.useState(initialUser)
useEffect —— 不接受欺骗的副作用
- 不要对依赖数组撒谎,effect中用到的所有组件内的值都要包含在依赖中。这包括props,state,函数等组件内的任何东西
- 不要滥用依赖数组项, 让Effect自给自足
- 通过返回一个函数来清除副作用,在重新渲染后才会清除上一次的effects
// 修改上面count更新到1就不动了,方法1
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
...
}
// 修改上面count更新到1就不动了,方法2( 与方法1的区别在哪里 )
function Counter() {
const [count, setCount] = React.useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
...
}
关于useEffect, 墙裂推荐Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!
useReducer —— 强大的状态管理机制
- 把组件内发生了什么(actions)和状态如何响应并更新分开表述,是Hooks的作弊模式
/** 修改需求:每秒不是加多少可以由用户决定,可以看作不是+1,而是+step*/
// 方法1
function Counter() {
const [count, setCount] = React.useState(0);
const [step, setStep] = React.useState(1);
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
...
}
// 方法2( 与方法1的区别在哪里 )
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { ...state, count: count + step };
} else if (action.type === 'step') {
return { ...state, step: action.step };
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
...
}
useCallback —— FP里使用函数的好搭档
说这个之前,先说一说如果你要在FP里面使用函数,你要先要思考有替代方案吗?
方案1: 如果这个函数没有使用组件内的任何值,把它提到组件外面去定义
方案2:如果这个函数只是在某个effect里面用到,把它定义到effect里面
如果没有替代方案,就是useCallback出场的时候了。
- 返回一个 memoized 回调, 不要对依赖数组撒谎
// 场景1:依赖组件的query
function Search() {
const [query, setQuery] = React.useState('hello');
const getFetchUrl = React.useCallback(() => {
return `xxxx?query=${query}`;
}, [query]);
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]);
...
}
// 场景2:作为props
function Search() {
const [query, setQuery] = React.useState('hello');
const getFetchUrl = React.useCallback(() => {
return `xxxx?query=${query}`;
}, [query]);
return <MySearch getFetchUrl={getFetchUrl} />
}
function MySearch({ getFetchUrl }) {
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]);
...
}
useRef —— 有记忆功能的可变容器
- 返回一个可变的 ref 容器对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变,也就是说会在每次渲染时返回同一个 ref 对象
- 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染
- 可以在ref.current 属性中保存一个可变值的“盒子“。常见使用场景:存储指向真实DOM / 存储事件监听的句柄 / 记录Function Component在某次渲染的值( eg:上一次state/props,定时器id.... )
// 存储不变的引用类型
const { current: stableArray } = React.useRef( [1, 2, 3] )
<Comp arr={stableArray} />
// 存储dom引用
const inputEl = useRef(null);
<input ref={inputEl} type="text" />
// 存储函数回调
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}
useMemo —— 记录开销大的值
- 返回一个 memoized 值,不要对依赖数组撒谎
- 大多数时候可以优先考虑使用useRef,useMemo常来处理开销较大的计算
- 可以依赖useMemo作为性能优化,但不能是语义保证(未来可能会忘记记忆值,比如为了释放内存)
// 此栗子来自文档
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useContext —— 功能强大的上下文
- 接收一个 context (React.createContext 的返回值)并返回该 context 的当前值,当前的 context 值由上层组件中最先渲染的 <MyContext.Provider value={value}> 的 value决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值,如果重新呈现组件非常昂贵,那么可以通过使用useMemo来优化它
// 此栗子来自文档
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
彩蛋
说是彩蛋,其实是补充说明~~
1. 一条重要的规则(代码不规范,亲人两行泪)
hooks除了要以use开头,还有一条很很很很重要的规则,就是hooks只允许在react函数的顶层被调用(这里墙裂推荐Hooks必备神器eslint-plugin-react-hooks)
考虑到出于研(gang)究(jing)精神的你可能会问,为什么不能这么用,我偏要的话呢?如果我是hooks开发者,我会毫不犹豫地说出门右转,有请下一位开发者!当然如果你想知道为什么这么约定地话,还是值得探讨一下的。其实这个规则就是保证了组件内的所有hooks可以按照顺序被调用。那么为什么顺序这么重要呢,不可以给每一个hooks加一个唯一的标识,这样不就可以为所欲为了吗?我以前一直都这么想过直到Dan给了我答案,简单点说就是为了hooks最大的闪光点——custom-hooks
2. custom-hooks
给我的感觉就是custom-hooks是一个真正诠释了React的编程模型的组合的魅力。你可以不看好它,但它确实有过人之处,至少它呈现出思想让我越想越上头~~以至于vue3.0也借鉴了他的经验,推出了Vue Hooks。反手推荐一下react conf 2018的custom-hooks。
// 修改页面标题
function useDocumentTitle(title) {
useEffect (() => {
document.title = title;
}, [title]);
}
// 使用表单的input
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
写在最后
最后抛出两个讨论的小问题。
-
React Hooks没有缺点吗?
- 肯定是有的,给我最直观的感受就是令人又爱又恨的闭包
- 不断地重复渲染会带来一定的性能问题,需要人为的优化
-
上面说了写了很多的setInterval的代码,可以考虑封装成一个custom-hooks?
- 可以考虑封装成useInterva,关于封装还是墙裂推荐Dan的 Making setInterval Declarative with React Hooks
- 如果有一堆特定的功能hooks,是不是完全可以通过组装各种hooks完成业务逻辑的开发,例如网络请求、绑定事件监听等
本人能力有限,如果有哪里说得不对的地方,欢迎批评指正!
真的真的最后,怕你错过,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!