Hook
Hook是一些可以让你在函数组件里"勾入"React state"及生命周期等特性的函数。
Hooks 的动机是什么?
- 解决共享状态而产生的嵌套地狱。
- React 需要为共享状态逻辑提供更好的原生途径。
- 因为组件拆分粒度问题,状态逻辑无处不在。所有需要与状态管理库同时使用。但是会引入更多的抽象概念,需要你再不同的文件之间来回切换,使得开发成本变高和复用变得更加困难。
- 解决
Clss
带来的问题,class
不能很好的压缩,并且会使热重载出现不稳定的情况。 - 在组件之间复用状态逻辑很难
Hook的优点有哪些?
- Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- 使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
- Hook 使你在非 class 的情况下可以使用更多的 React 特性
- 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。
API介绍
什么是useState?
useState
就是一个 Hook (等下我们会讲到这是什么意思)。通过在函数组件里调用它来给组件添加一些内部 state。
React 会在重复渲染时保留这个 state。useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。
代码示例:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。
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);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
// componentDidMount
useEffect(() => {
console.log('只会进来一次这个方法');
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在组件销毁的时候如何取消事件的订阅?
副作用函数还可以通过返回一个函数来指定如何“清除”副作用。
代码示例:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => { // 在返回的函数中取消订阅
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
如何自定义Hook以实现封装或复用的目的?
代码示例:
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
现在我们可以在两个组件中使用它:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
这两个组件的 state 是完全独立的。Hook 是一种复用状态逻辑的方式,它不复用 state 本身。事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。
如何使用useReducer?
它接收一个形如(state, action) => newState
的reducer
,并返回当前的state
以及与其配套的dispatch
方法。
使用场景:
state
逻辑复杂且包含多个子值- 下一个
state
依赖于之前的state
等。 - 触发深更新的组件做性能优化,以为你可以向下传递
dispatch
,而不是回调函数。
代码示例如下:
export type StateType = {
count: number;
}
export type ActionType = {
type: string;
}
function reducer(state: StateType, action: ActionType): StateType {
switch (action.type) {
case 'inc':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
const initState: StateType = {
count: 0
};
function App() {
const [state, disptach] = useReducer(reducer, initState);
return (
<div>
<p>{state.count}</p>
<button onClick={() => disptach({ type: 'inc' })}>clickMe</button>
</div>
)
}
如何惰性初始化state
的初始值?
将 默认执行的action
对象作为 useReducer
的第三个参数传入,这样就会默认执行一次reducer
。
代码示例如下:
import React, { useState, useRef, ReactEventHandler, useReducer } from 'react'
import ReactDOM from 'react-dom'
export type ActionType = {
type: string;
payload?: number;
};
export type ReducerType = (state: number, action: ActionType) => number;
export type DispatchType = (action: ActionType) => void;
export type ExamplePropsType = {
initCount: number;
}
function todosReducer(state: number, action: ActionType): number {
switch (action.type) {
case 'add':
return state + 1;
case 'inc':
return state - 1;
case 'reset':
return init(action.payload || 99999);
default:
return state;
}
}
function init(initialCount: number): number {
return initialCount;
}
function Example({ initCount }: ExamplePropsType): JSX.Element {
const initData: ActionType = { type: 'reset', payload: initCount };
const [num, dispatch] = useReducer(todosReducer, 0, initData);
const [state, setState] = useState(0);
return (
<div>
<p>{num}</p>
<p>{state}</p>
<button onClick={() => {
dispatch({ type: 'reset', payload: initCount });
}}>reset</button>
<button onClick={handleClick.current}>ClickMe</button>
<button onClick={() => {
dispatch({ type: 'add' });
setState(state + 1);
}}>ClickMe</button>
</div >
)
}
ReactDOM.render(<Example initCount={10} />, document.getElementById('root'));
useReducer原理是什么?
import React, { useState, useRef, ReactEventHandler } from 'react'
import ReactDOM from 'react-dom'
export type ActionType = {
type: string
};
export type ReducerType = (state: number, action: ActionType) => number;
export type DispatchType = (action: ActionType) => void;
function todosReducer(state: number, action: ActionType): number {
switch (action.type) {
case 'add':
return state + 1;
case 'inc':
return state - 1;
default:
return state;
}
}
function useReducer(initState: number, reducer: ReducerType): [number, DispatchType] {
const [state, setState] = useState<number>(initState);
function dispatch(action: ActionType) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
function Example() {
const [num, dispatch] = useReducer(0, todosReducer);
const [state, setState] = useState(0);
return (
<div>
<p>{num}</p>
<p>{state}</p>
<button onClick={handleClick.current}>ClickMe</button>
<button onClick={() => {
dispatch({ type: 'add' });
setState(state + 1);
}}>ClickMe</button>
</div >
)
}
ReactDOM.render(<Example />, document.getElementById('root'));
什么是useCallback?
先看一下代码示例:
export type FuncType = (value: number) => number;
// 如果函数组件的外部,那么useCallback返回的永远都是同一个函数
// 只有放在函数组件内部,当你更改依赖项的时候才会返回新的函数。
// 那么是为什么呢,我还不知道
function handleChangeValue(value: number): number {
// TODO: sth
console.log('value', arguments);
return value;
}
const set = new Set(); // 用于useCallback返回的函数
// useMemo 和 useCallback
function Example(): JSX.Element {
const [state, setState] = useState<number>(0);
const [num, setNum] = useState<number>(0);
// const oThername = useMemo<number>(() => handleChangeValue(state), [state]);
// 如果注释掉这行代码,使用函数组件外的方法的话,useCallback返回的永远都是同一个方法,不管依赖项会不会更新,我们可以通过set.size观察到
function handleChangeValue(value: number): number {
return value;
}
const oThername: FuncType = useCallback<(value: number) => number>(handleChangeValue, [state]);
set.add(oThername);
return (
<>
<p>{set.size}</p>
<p>{state}</p>
<p>{num}</p>
<button onClick={() => setState(state + 1)}>Click Me</button>
<span> </span>
<button onClick={() => setNum(num + 1)}>Click oTher</button>
</>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
把内联回调函数及依赖项数组作为参数传入useCallback
,它将返回该回调函数的memoized
版本,该回调函数仅在某个依赖项改变时才会更新。当把你的回调函数传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时,它非常有作用。
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
什么是useMemo?
把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算
memoized
值。
观察下面的代码:
function handleChangeValue(value: number): number {
// TODO: sth
console.log('value', value);
return value;
}
// useMemo 和 useCallabck
function Example(): JSX.Element {
const [state, setState] = useState<number>(0);
const [num, setNum] = useState<number>(0);
const otherName = useMemo<number>(() => handleChangeValue(state), [state]);
// const oThername1= handleChangeValue(state);
return (
<>
<p>{otherName}</p>
<p>{num}</p>
<button onClick={() => setState(state + 1)}>Click Me</button>
<span> </span>
<button onClick={() => setNum(num + 1)}>Click Other</button>
</>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
上面的代码当你每次点击Click Me
的时候因为更新了state
,而state
又是useMemo
的依赖项,所以每次点击都会执行handleChangeValue
函数。而当你点击Click Other
时,因为作为依赖项state
并没有被更新,所以handleChangeValue
不会执行。
用在何处?
- 避免每次渲染都进行高开销的计算。
什么是useRef?
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
看下面的代码:
export type ThisType = {
num?: number;
}
function Example(): JSX.Element {
const [state, setState] = useState<number>(0);
const inputEl = useRef<HTMLParagraphElement>(null);
const instanceOfThis = useRef<ThisType>({});
// DidMount
useEffect(() => {
console.log(inputEl, instanceOfThis);
}, []);
// Did update
useEffect(() => {
console.log('DidUpdate')
});
return (
<div>
<p ref={inputEl}></p>
<button onClick={handleChangeInstanceOfThis}>Click Me</button>
<button onClick={() => setState(state + 1)}>Click Other</button>
</div>
)
function handleChangeInstanceOfThis() {
// 更新此值不会触发更新
instanceOfThis.current.num = 10;
console.log('instanceOfThis.current', instanceOfThis.current)
}
}
ReactDOM.render(<Example />, document.getElementById('root'));
从上面的代码可以看出,由useRef
初始化出来的变量,在更改的时候不会引发组件的更新。类似于类组件的this
的属性,非常好用。
useRef有哪些特点?
- 更新不会触发更新
- 可以当用类组件的
this
属性使用,也可以用来存储虚拟DOM
对象
什么是useImperativeHandle
?
useImperativeHandle
可以让你在使用ref
时自定义暴露给父组件的实例值。
语法:
useImperativeHandle(ref, createHandle, [deps])
代码示例:
function Input(props: any, ref: any): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle<HTMLInputElement, any>(ref, () => ({
focus: () => {
inputRef.current && inputRef.current.focus();
}
}));
return (
<div>
<input type="text" ref={inputRef} />
</div>
)
}
const FuncyInput = forwardRef(Input);
// useImperativeHandle 的引用----------------------------------------
function Example(): JSX.Element {
const inputRef: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
return (
<div>
<FuncyInput ref={inputRef} />
<button onClick={() => {
console.log(inputRef);
inputRef.current && inputRef.current.focus();
}}>Click Me</button>
</div>
)
}
ReactDOM.render(<Example />, document.getElementById('root'));
理想的书写方式?
- 多个
useEffect
会产生错乱,无法一眼看出每个useEffect
他们的执行过程,应当按照声明周期的顺序排列起来,易于查找和维护。
问题已经知道和不知道的
- 可以在class组件中使用hook吗?
- 不可以。
- 函数组件中可以多次调用useState和useEffect吗?
- 可以
- useEffect中的函数调用时机是什么时候?
- 执行 DOM 更新之后调用它
- useEffect 会在什么时候执行消除操作
- 组件卸载的时候
- 我想保存一些上下文变量,会更新,但是不需要更新的时候进行页面渲染,该如何处理呢?
- hook和组件都是一对一的关系,如何做到一对多?类似发布订阅功能。
- 细品函数运行,你能发现什么?你会写代码的时候会初始化大量闭包吗?
- hook 如何使函数组件更新的?
- hook函数式,没有结构,让我感觉很不舒服。
- hook中的数据什么时候被销毁?