1、为什么学习hooks,hooks是什么,hooks有什么作用?
Hook 是React16.8的新增特性,它是一个特殊的函数,可以让你“钩入” React 的特性,可以在不用编写class组件的情况下使用state以及其他的React特性。为什么要学习hook呢?因为我们在开发的时候,往往会遇到一些问题,例如:在组件之间复用状态逻辑困难、复杂组件变得难以理解、难以理解的class等等。Hook 在无需修改组件结构的情况下复用状态逻辑。Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。还可以使用 reducer 来管理组件的内部状态,使其更加可预测。Hook还可以在不使用class的情况下,使用更多的React特性。
2、HOOKS概览
1、 使用 useState
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p> {/*使用State时*/}
<button onClick={() => setCount(count + 1)}> {/*更新state*/}
Click me
</button>
</div>
);
}
复制代码
useState 唯一的参数就是初始 state,在例子中就是useState(0),0就是初始state。useState 会返回一对值:当前状态(count)和一个更新它的函数(setCount)。可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。以下是等价的class hook实现的例子:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p> {/*使用State时*/}
<button onClick={() => this.setState({ count: this.state.count + 1 })}> {/*更新state*/}
Click me
</button>
</div>
);
}
}
复制代码
通过以上比较发现:在 class 中,通过在构造函数中设置 this.state
为 { count: 0 }
来初始化 count
state 为 0
,显示当前state时,需读取this.state.count
,在函数中可直接使用count
。更改state时,在 class 中,需要this.setState()
来更新 count 值。在函数中,已经有了setCount
和count
变量,不需要 this
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
复制代码
在函数组件中没有 this,所以不能分配或读取 this.state。可直接在组件中调用 useState Hook,useState Hook可以在一个组件中多次使用,可以同时创建多个变量,在调用useState 时可以给 state 变量取不同的名字:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
复制代码
注意:调用 useState 方法的时候做了什么? 定义了一个变量
count
,与 class 里面的this.state
提供的功能完全相同,一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。useState()里唯一的参数就是初始 state,与class不同的是,不必须使用对象,可以按照需要使用数字或字符串进行赋值。useState 方法的返回值是什么? 就是const [count, setCount] = useState()
解构出来的count
和setCount
这与 class 里面this.state.count
和this.setState
类似,唯一区别就是需要成对的获取它们。所以上面是示例可以理解为:声明了一个叫count
的 state 变量,然后把它设为0
。React 会在重复渲染时记住它当前的值,并且提供最新的值给setCount
函数,通过调用setCount
来更新当前的count
。
2、使用 useEffect
在熟悉 React class 的生命周期函数后,可以将 useEffect Hook 看做 componentDidMount
componentDidUpdate
和 componentWillUnmount
这三个函数的组合。与 componentDidMount
或 componentDidUpdate
不同的是,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,使应用看起来响应更快。 大多数情况下,effect 不需要同步地执行。useEffect可以在函数组件中执行副作用操作(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)以下是分别是class组件/函数组件 例子:
//class组件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
//函数组件
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 与componentDidMount和componentDidUpdate类似:
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
注意: 在 class 组件中,要在两个生命周期函数中编写重复的代码。而使用useEffect,告诉了组件在渲染后执行某些操作,将其保存,在执行 DOM 更新之后调用它。为什么在函数组件内部调用呢?因为将 useEffect 放在组件内部让我们可以在 effect 中直接访问
count
state 变量(或其他 props),Hook 使用了 JavaScript 的闭包机制,不用引入特定的 React API来读取它。默认情况下,useEffect在第一次渲染之后和每次更新之后都会执行。
将 useEffect Hook 看做 componentDidMount
componentDidUpdate
和 componentWillUnmount
这三个函数的组合的写法如下:当返回一个函数时,即做了清除操作。更新逻辑不需要特定的代码来处理,因为 useEffect 默认就会处理,它会在调用一个新的 effect 之前对前一个 effect 进行清理。可使用多个effect,将不相关逻辑分离到不同的 effect 中,
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => { //清除操作
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
//性能优化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
复制代码
3、使用useContext
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值,当前值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定,当组件上最近的<MyContext.Provider>
更新时,useContext会触发重渲染,并使用最新值传递给 MyContext provider
的 context value
值,如以下示例:
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() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
复制代码
4、使用useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init)
是useState
的替代方案,当state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 时,useReducer 会比 useState 更适用,且使用 useReducer 还能给那些会触发深更新的组件做性能优化,以下是用 reducer 重写 useState 例子:
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>
</>
);
}
复制代码
5、使用useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback,返回该函数的当前版本,该回调函数仅在某个依赖项改变时才会更新
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
注意:依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。
6、使用useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。传入 useMemo 的函数会在渲染期间执行,不要在这个函数内部执行与渲染无关的操作,如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值,可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证,useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
6、使用useRef
const refContainer = useRef(initialValue)
useRef 返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。将 ref 对象以 <div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。useRef() 比 ref 属性更有用,它可以很方便地保存任何可变值,因为ref 属性创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
function TextInputWithFocusButton() {
const myRef = useRef(null); // null为初始值
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
myRef.current.focus();
};
return (
<>
<input ref={myRef} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
复制代码
7、使用useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
// 渲染 <FancyInput ref={inputRef} /> 的父组件可以调用 inputRef.current.focus()。
复制代码
8、使用useLayoutEffect
函数形参与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
9、使用useDebugValue
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。例如:一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:
useDebugValue(date, date => date.toDateString());