React 的有两套 API:类(class)API 和基于函数的钩子(hooks) API,官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较”轻”,而类比较”重”。而且,钩子是函数,更符合 React 函数式的本质。
组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。
一、类和函数的差异
-
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
-
函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。
二、副作用是什么
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
复制代码
这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 “纯函数”(pure function),那么问题来了:那些不涉及计算的操作(比如生成日志、储存数据、改变应用状态等等)应该写在哪里呢?
函数式编程将那些跟数据计算无关的操作,都称为 “副作用” (side effect) 。
三、钩子(hook)的作用
钩子(hook)就是 React 函数组件的副作用解决方案,用来为函数组件引入副作用。
函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副作用)都必须通过钩子引入。
React 为许多常见的操作(副作用),都提供了专用的钩子:
useState()
:保存状态useContext()
:保存上下文useRef()
:保存引用- ……
上面这些钩子,都是引入某种特定的副作用,而 useEffect()
是通用的副作用钩子 。找不到对应的钩子时,就可以用它。
四、useEffect
import React, { useEffect } from 'react';
function Welcome(props) {
useEffect(() => {
document.title = '加载完成';
});
return <h1>Hello, {props.name}</h1>;
}
复制代码
上面例子中,useEffect()
的参数是一个函数,它就是所要完成的副作用(改变网页标题)。组件加载以后,React 就会执行这个函数。
1. useEffect() 的第二个参数
function Welcome(props) {
useEffect(() => {
document.title = `Hello, ${props.name}`;
}, [props.name]);
return <h1>Hello, {props.name}</h1>;
}
复制代码
作用相当于**componentDidUpdate()**
方法。
如果第二个参数是一个空数组,就表明副作用参数没有任何依赖项。因此,副作用函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。此时作用相当于**componentDidMount()**
方法。
2. useEffect() 的返回值
副作用是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副作用。
useEffect()
允许返回一个函数,在组件卸载时,执行该函数,清理副作用。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);
复制代码
上例中,useEffect()
在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。作用相当于**componentWillUnmount()**
方法。
3. 注意点
如果有多个副作用,应该调用多个useEffect()
,而不应该合并写在一起。
五、useState():状态钩子
const [state, setState] = React.useState(initialState);
复制代码
返回一个 state,以及更新 state 的函数。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
1. 函数式更新
setState(i => i+1)
复制代码
对state
多次操作优先使用此形式
2. useState
不会自动合并更新对象
可以用函数式的 setState
结合展开运算符来达到合并更新对象的效果:
const [state, setState] = useState({});
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
复制代码
3. 惰性初始 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
复制代码
六、useReducer():action 钩子
React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState
。
useReducers()
钩子用来引入 Reducer 功能:
const [state, dispatch] = useReducer(reducer, initialState);
复制代码
它相当于useState
的复杂版本。它接收一个reducer,并返回当前的 state 以及与其配套的 dispatch
方法。
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数 。
下面是一个计数器的例子。用于计算状态的 Reducer 函数如下:
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
default:
return state;
}
}
复制代码
组件代码如下:
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}
复制代码
由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux,步骤:
- 将数据集中在一个
store
对象 - 将所有操作集中在
reducer
- 创建一个
Context
- 创建对数据的读写API
- 将第4步的内容放在第3步的
Context
- 用
Context.Provider
将Context
提供给所有组件 - 各个组件用
useContext
获取读写API
但是,useReducer
无法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux。
七、useContext():共享状态钩子
const value = useContext(MyContext);
复制代码
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
如果需要在组件之间共享状态,可以使用useContext()
。
现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态:
<div className="App">
<Navbar/>
<Messages/>
</div>
复制代码
第一步就是使用 React Context API,在组件外部建立一个 Context:
const AppContext = React.createContext({});
复制代码
组件封装代码如下:
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
复制代码
上面代码中,AppContext.Provider
提供了一个 Context 对象,这个对象可以被子组件共享。
Navbar 组件的代码如下:
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
复制代码
八、useRef
const refContainer = useRef(initialValue);
复制代码
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个常见的用例便是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
复制代码
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
九、useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值,相当于Vue 2中的computed
。这种优化有助于避免在每次渲染时都进行高开销的计算。
传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
**你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。**将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
十、自定义Hook
自定义 Hook 是一个函数,其名称以 “**use**
” 开头,函数内部可以调用其他的 Hook。
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use
开头,这样可以一眼看出其符合 Hook 的规则。