v16.8 版本之前,组件的标准写法是类(class component)。v16.8 版本引入了全新的 API(hooks component),叫做 React Hooks,颠覆了以前的用法。
函数组件(function component)
- React 早期就支持函数组件,但是有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。
- React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。
// 早期函数式组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
React Hooks
useState 状态钩子
- 通过在函数组件里调用它来给组件添加一些内部
state。React 会在重复渲染时保留这个state。useState会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似class组件的this.setState,但是它不会把新的state和旧的state进行合并。 - 可在组件中多次使用
// hooks component
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
// 这里的 setCount 就类似 this.setState({count: any})
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 副作用钩子
- 你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过
DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。 useEffect就是一个Effect Hook,给函数组件增加了操作副作用的能力。它跟class组件中的componentDidMount、componentDidUpdate和componentWillUnmount具有相同的用途,只不过被合并成了一个 API。- 副作用函数还可以通过返回一个函数来指定如何“清除”副作用。
- 可在组件中多次使用
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// useEffect()接受两个参数。
// 第一个参数是一个函数,异步操作的代码放在里面。
// 第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。
// 第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
// 执行副作用
document.title = `You clicked ${count} times`;
// 消除副作用
return () => {
document.title = "";
}
}, [dependencies]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useContext 共享状态钩子
- 如果需要在组件之间共享状态,可以使用
useContext()。
import React, {createContext} from 'react';
// 建立 context
const AppContext = createContext({});
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
Navbar中使用共享状态
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
Messages中使用共享状态
const Messages = () => {
const { username } = useContext(AppContext);
return (
<div className="messages">
<h1>Messages</h1>
<p>1 message for {username}</p>
<p className="message">useContext is awesome!</p>
</div>
)
}
useReducer action 钩子
React本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是Redux。Redux的核心概念是,组件发出action与状态管理器通信。状态管理器收到action以后,使用Reducer函数算出新的状态,Reducer函数的形式是(state, action) => newState。useReducers()钩子用来引入Reducer功能。
import React, {useReducer} from 'react';
function App(){
// 接受 Reducer 函数和状态的初始值作为参数,返回一个数组。
// 数组的第一个成员是状态的当前值,
// 第二个成员是发送 action 的 dispatch 函数。
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
};
default:
return state;
}
}, {count: 0});
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}
- 由于
Hooks可以提供共享状态和Reducer函数,所以它在这些方面可以取代Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用Redux。 - 惰性初始化,需要将
init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initialArg);这么做可以将用于计算state的逻辑提取到reducer外部,这也为将来对重置state的action做处理提供了便利
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>);
}
- 如果
Reducer Hook的返回值与当前state相同,React将跳过子组件的渲染及副作用的执行。(React使用 Object.is 比较算法 来比较state。)需要注意的是,React可能仍需要在跳过渲染前再次渲染该组件。不过由于React不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用useMemo来进行优化。
useCallback 回调钩子
- 把内联回调函数及依赖项数组作为参数传入
useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如shouldComponentUpdate)的子组件时,它将非常有用。
// useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
// 依赖项数组不会作为参数传给回调函数。
// 虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
useMemo 记忆钩子
- 传入
useMemo的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect的适用范畴,而不是useMemo。 - 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
// 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useRef
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>
</>
);
}
- 应该熟悉
ref这一种访问DOM的主要方式。如果你将ref对象以<div ref={myRef} />形式传入组件,则无论该节点如何改变,React都会将ref对象的.current属性设置为相应的DOM节点。 - 当
ref对象内容发生变化时,useRef并不会通知你。变更.current属性不会引发组件重新渲染。如果想要在React绑定或解绑DOM节点的ref时运行某些代码,则需要使用回调 ref来实现。
useImperativeHandle
useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用
// useImperativeHandle(ref, createHandle, [deps])
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} {...props} />;
}
FancyInput = forwardRef(FancyInput);
// 使用 FancyInput
// 父组件可以调用 inputRef.current.focus()。
<FancyInput ref={inputRef} />
useLayoutEffect
- 其函数签名与
useEffect相同,但它会在所有的DOM变更之后同步调用effect。可以使用它来读取DOM布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。 - 如果你正在将代码从
class组件迁移到使用Hook的函数组件,则需要注意useLayoutEffect与componentDidMount、componentDidUpdate的调用阶段是一样的。但是,我们推荐你一开始先用useEffect,只有当它出问题的时候再尝试使用useLayoutEffect。