【React】React Hook解读
什么是React Hook?
Hook 是一个特殊的函数,它可以让你”钩入”React state及生命周期等特性的函数。例如,useState是允许你在React函数组件中添加state的Hook。
使用React Hook的目的是什么?
使用Hook其中一个目的就是要解决class中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到几个不同方法中的问题。
什么时候会用React Hook?
如果你在编写函数组件并意识到需要向其添加一些state,以前的做法是必须将其它转化为class。现在你可以在现有的函数组件中使用Hook。
React Hook的规则是什么?
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
什么是Hook的副作用?
之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。
Effect Hook 可以让你在函数组件中执行副作用操作。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。
有的Hook是需要清理的(比如定时器),有的是不需要清理的的(比如数据获取)。需要清理的副作用,需要返回一个函数。
不需要清理的effect:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要清理的effect:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
effect性能优化:
如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。
推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
更多详细Hook API,详见:Hook API
常见Hooks API
useState
设置和改变state,代替原来的state和setState。
import React, {useState} from 'react';
function RenderStateInstance({initialState}) {
const [count, setCount] = useState(initialState);
return (
<>
Count: {count}<br />
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
useMemo
控制组件更新条件,可根据状态变化控制方法执行,优化传值。
import React, { useState, useMemo } from 'react'
function calcNumber(count) {
console.log("cal重新计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total
}
export default function MemoHookDemo01() {
const [count, setCount] = useState(10);
const [show, setShow] = useState(true);
// const total = calcNumber(count);
const total = useMemo(() => {
return calcNumber(count);
}, [count])
return (
<div>
<h2>计算数字的和:{total}</h2>
<button onClick={e => { setCount(count + 1) }}>+1</button>
<button onClick={e => { setShow(!show) }}>show切换</button>
</div>
)
}
这个例子中:
useMemo第一个参数为回调函数,第二个参数为依赖项;
当依赖项发生改变时,执行回调函数。
useCallback
useMemo优化传值,usecallback优化传的方法,是否更新。
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <div>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
使用场景:
有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
useRef
跟以前的ref,一样,只是更简洁了。它可以用来获取组件实例对象或者是DOM对象。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const couterRef = useRef();
useEffect(() => {
document.title = `The value is ${count}`;
console.log(couterRef.current);
}, [count]);
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
代码中用useRef创建了couterRef对象,并将其赋给了button的ref属性。这样,通过访问couterRef.current就可以访问到button对应的DOM对象。
而useRef这个hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据。
import React, { useState, useEffect, useMemo, useRef } from 'react';
export default function App(props){
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
return 2 * count;
}, [count]);
const timerID = useRef();
useEffect(() => {
timerID.current = setInterval(()=>{
setCount(count => count + 1);
}, 1000);
}, []);
useEffect(()=>{
if(count > 10){
clearInterval(timerID.current);
}
});
return (
<>
<button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
</>
);
}
useEffect
页面加载完成以后,想要做的一些操作,这个经常是componentDidMount里面做的一些事情,但是不会block browser painting。
代替原来的生命周期,componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(count)
}, 3000)
})
// 在3秒内快速点击5次按钮,控制台打出的结果是什么样的?
// 0 1 2 3 4 5
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。
useContext 共享状态钩子
createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。
import React, { useContext } from "react";
import ReactDOM from "react-dom";
const TestContext= React.createContext({});
const Navbar = () => {
const { username } = useContext(TestContext)
return (
<div className="navbar">
<p>{username}</p>
</div>
)
}
const Messages = () => {
const { username } = useContext(TestContext)
return (
<div className="messages">
<p>1 message for {username}</p>
</div>
)
}
function App() {
return (
<TestContext.Provider
value={{
username: 'superawesome',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
<TestContext.Provider/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useReducer():action 钩子
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState。
useReducers()钩子用来引入 Reducer 功能。
const [state, dispatch] = useReducer(reducer, initialState);
上面是useReducer()的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的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>
);
}
useImperativeHandle
useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用。
ref和forwardRef
我们先来看下ref和forwardRef的使用。
- 通过forwardRef可以将ref转发给子组件
- 子组件拿到父组件创建的ref, 绑定到自己的某一个元素中
import React, { useRef, forwardRef } from 'react'
// forwardRef可以将ref转发给子组件
const JMInput = forwardRef((props, ref) => {
return <input type="text" ref={ref} />
})
export default function ForwardDemo() {
// forward用于获取函数式组件DOM元素
const inputRef = useRef()
const getFocus = () => {
inputRef.current.focus()
console.log(inputRef.current);
}
return (
<div>
<button onClick={getFocus}>聚焦</button>
<JMInput ref={inputRef} />
</div>
)
}
forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:
- 直接暴露给父组件带来的问题是某些情况的不可控
- 父组件可以拿到DOM后进行任意的操作
- 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法
比如上面的inputRef.current打印出来的就是一个DOM。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle使用简单总结:
作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
参数1: 父组件传递的ref属性
参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
通过useImperativeHandle可以只暴露特定的操作。通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起,所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
const JMInput = forwardRef((props, ref) => {
const inputRef = useRef()
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
test: () => {
console.log('abcd');
}
}))
return <input type="text" ref={inputRef} />
})
export default function ImperativeHandleDemo() {
// useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
// 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
// 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
const inputRef = useRef();
const action = () => {
inputRef.current.focus();
inputRef.current.test();
console.log(inputRef.current);
}
return (
<div>
<button onClick={action}>聚焦</button>
<JMInput ref={inputRef} />
</div>
)
}
所以上面inputRef.current返回的是一个对象:
useState 与useContext实例
import React, {useState, useContext} from 'react';
const ThemeContext = React.createContext('light');
function RenderStateInstance({initialState}) {
const [count, setCount] = useState(initialState);
const contextType = useContext(ThemeContext);;
return (
<>
Count: {count}<br />
Theme: {contextType}<br /><br />
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
export default class Mine extends React.Component {
render() {
let initialState = 0;
return (
<ThemeContext.Provider value="dark">
<div>
<p>My 页面 </p>
<RenderStateInstance initialState={initialState} />
</div>
</ThemeContext.Provider>
)
}
}
更多自定义Hooks
React 官网:https://zh-hans.reactjs.org/docs/hooks-custom.html
更多Github实例:https://github.com/ecomfe/react-hooks
自定义useTtitle
import { useEffect } from 'react';
const useTitle = title => {
useEffect(() => {
document.title = title;
}, [])
return;
};
export default useTitle;
使用useTitle:
import React from 'react';
import useTitle from '../hooks/useTitle';
const Title = () => {
useTitle('修改Title')
return (
<>
<p>这里用了自定义的Hooks title</p>
</>
);
};
export default Title;
自定义Hooks的规则
- 自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
- 自定义 Hook 必须以 “use” 开头