React Hook是React16.8.0引入的。使可以在不引入class的情况下,可以使用state和其他React特性。
hooks本质上是一些函数。
1. 为什么引入Hook?
1. hooks中的useEffect可以解决class中各逻辑在生命周期函数中管理混乱的问题。
2.hooks中的自定义Hook使得可以不修改组件的结构的基础上,灵活的复用组件逻辑。
3.class组件不能很好的压缩,并且热重载不稳定。不利于组件优化。使用Hook的函数组件不存在这些问题
2. Hook的规则
1. 只能在函数组件中使用hooks
类组件中无效。
2. 只能在函数最外层使用
不能用于if,for等语句中,也不能用于普通js函数中。因为ReactHook通过调用顺序确定对应的state等对应的hook方法。使用语句等会改变顺序。
3. 只能在以名字use开头的自定义Hook中使用
3. useState
接受一个参数作为初始值,参数可以是常量,也可以是一个返回值的函数。
初始值如果是一个函数,在初次渲染执行;如果是一个函数的执行,每次渲染都会执行
// 初始值是一个函数
const [count, setCount] = useState(function init(){......;return ..})
// 初始值是一个函数调用
const [count, setCount] = useState(init())
以数组形式返回两个值,第一个是状态值(初次渲染创建变量),一个是改变状态的函数。
例如:
const [count, setCount] = useState(0); // 0是初始值
修改状态的函数(如setCount)和class中的setState类似,可以接受两种参数:
setCount(fn/exp)
1)表达式
可以是常量, 也可以是一个带state值的表达式
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(count+1)}>-</button>
2)函数
<button onClick={() => setCount(prevCount => prevCount + 1)}>-</button>
⚠️如果修改后的状态不变,重复调用只会刷新一次。
<button onClick={() => setCount(count)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<!-- 每次单击第一个按钮会渲染一次,继续单击不继续渲染;
但是如果再单击第二个按钮,切换到第一个按钮,还是会渲染一次-->
模拟getDerivedStateFromProps
在render前进行setState更新
应用:在异步函数中获取state的值
在setTimeout等异步函数中获取的状态值,是调用setTimeout方法时的状态值,不是执行时的状态值。
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
// count的取值形成了一个Example的闭包,每次刷新都是一个新的闭包
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}
ReactDOM.render(<Example/>, window.root);
4. useEffect
它相当于是componentDidMount,componentDidUpdate,componentWillUnMount三个生命周期的合成体。
它接受两个参数, 第一个是一个函数(effect),第二个参数可选,是一个数组(第一个函数中用到的可变数据集合)。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新;每次渲染,第一个函数都相当于一个新生成的函数
1. 执行时机
和生命周期不同的是,componentDidMount,componentDidUpdate是DOM渲染完成,屏幕更新前触发执行,会阻碍屏幕渲染;useEffect中的effect函数(非变更DOM的操作)在浏览器完成布局和绘制后,会延迟执行(异步,但是肯定在下一次渲染前执行),不会阻碍屏幕更新。但是如果是变更DOM的操作,需要同步执行。
1) 如果只在初次加载的时候运行,模拟componentDIdMount
useEffect(() => {
//TODO
}, []);
2)想要只在更新的时候运行,模拟componentDidUpdata
//使用useRef()存储的实例变量作为是否是第一次执行的标识
const [count, setCount] = useState(0);
const first = useRef(true);
useEffect(() => {
if (first.current) {
first.current = false;
} else {
console.error(count);
document.title = `You clicked ${count} times!`;
}
}, [count]);
2. 执行副作用
如果副作用需要取消,在传入useEffect的函数中返回一个函数,在返回的函数中执行取消操作,该返回函数会在组件卸载的时候执行。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
3. 性能优化
第二个参数传入的变量,会作为比较的依据。如果变量值不变,就会跳过本次副作用的执行。
作为参数的值是在组件域(即函数组件对应的函数域)中的可变量(props/state),而且在useEffect中使用。
5. useContext
使函数组件可以具有和Class组件中的contextType属性(使可以通过this.context访问值)一样的功能。
用法和Class中contextType基本一致。接受一个context对象作为参数,返回最近的Provider提供的值。
示例代码:
const ThemeContext = React.createContext('dark');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Child />
<button onClick={() => setTheme(preTheme => preTheme === 'dark' ? 'light' : 'dark')}>切换主题</button>
</ThemeContext.Provider>
);
}
function Child() {
const value = useContext(ThemeContext);
return (
<div>{value}</div>
);
}
6.useReducer
可以看做useState的复合版。当应用中需要多个状态时,一般需要调用多次useState(便于组合逻辑)。随着应用逐渐扩展,会越来越复杂,此时可以使用useReducer。
const [state, dispatch] = useReducer(reducer, initialArg, init); // 第三个值可以不传,是一个接受第二个参数,返回一个初始值的函数
示例:
import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';
function App() {
function reducer(state, action) {
switch(action.type) {
case 'add':
return state + 1;
case 'minuse':
return state - 1;
case 'reset':
return 0;
default:
throw new Error('error');
}
}
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>You clicked {count} times!</p>
<button onClick={() => dispatch({type: 'reset'})}>Reset</button>
<button onClick={() => dispatch({type: 'add'})}>+</button>
<button onClick={() => dispatch({type: 'minuse'})}>-</button>
</div>
);
}
模拟forceUpdate()
function Example() {
const [count, forceUpdate] = useReducer(x=>x+1, 0);
return (
<div>
<button onClick={() => forceUpdate()}>Click me</button>
</div>
);
}
7. useMemo
本质上是memorization技术。用于优化在渲染阶段,计算数据的函数。
它接受一个计算函数作为第一参数;第二个是个依赖项数组。
返回一个值,该值是第一个计算函数的返回结果。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
示例:
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import React, {useState, useMemo, useEffect} from 'react';
function App() {
const inputRef = React.createRef();
const [data, setData] = useState([]);
useEffect(() => {
inputRef.current.value = null;
});
return (
<div>
<input type='text' ref={inputRef}/>
<button onClick={() => setData(data.concat(inputRef.current.value))}>添加</button>
<Child data={data}/>
</div>
);
}
function Child(props) {
const [filterText, setFilterText] = useState('');
const lists = useMemo(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]);
return(
<div>
<input onChange={(e) => setFilterText(e.target.value)} />
<ul>
{
lists.map((i,inx) => <li key={inx}>{i}</li>)
}
</ul>
</div>
);
}
Child.propTypes = {
data: PropTypes.array
};
ReactDOM.render(<App/>, window.root);
8. useCallback
和 useMemo基本相同;
不同点在于它返回一个函数,这个和'memorize-one'的功能相同。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
代码示例:
// 用useCallback替换上面useMemo的示例 const listFilter = useCallback(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]); const lists = listFilter(filterText, props);
该方法比useRef的优点是,因为返回一个函数,它更容易抽取逻辑,形成自定义Hook
function MeasureDOM() {
const [rect, measureRef] = useClientRec();
return (
<>
<h1 ref={measureRef}>Hello, world</h1>
{rect && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
</>
);
}
function useClientRec() {
const [rect, setRect] = useState();
// 此时ref属性指向一个回调函数
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}
9 .useRef
useRef模拟的是Class组件中的实例属性。可以在函数组件中很方便的保存任何可变值,不引起渲染。
// initialValue是current属性的初始值 const refContainer = useRef(initialValue);
使用useRef和React.createRef的区别是:
1)useRef返回一个refContainer对象({current: XXX}),并且重新渲染,访问的都是同一个对象。
2)React.createRef在函数组件中是每次重新渲染,相当于重新创建了一个对象,每次都是一个新对象。
示例:
// 单击一次后,inputRef1为1, inputRef2仍然为0
import React, {useState,useEffect, useRef} from 'react';
import ReactDOM from 'react-dom';
function App() {
const [count, setCount] = useState(0);
const inputRef1 = useRef(null);
const inputRef2 = React.createRef();
useEffect(() => {
if(count === 1) {
inputRef1.current.value = count;
inputRef2.current.value = count;
}
}, [count,inputRef1,inputRef2]);
return (
<div>
<input ref={inputRef1} /><br/>
<input ref={inputRef2} /><br/>
inputRef1:{inputRef1.current && inputRef1.current.value || 0}<br/>
inputRef2:{inputRef2.current && inputRef2.current.value || 0}<br/>
<button onClick={() => setCount(count+1)}>Add</button>
</div>
);
}
ReactDOM.render(<App/>, window.root);
应用: 获取prevState或者prevProps
// 考虑到通用性,将其提取到自定义Hook中usePrevious
function App() {
const [count, setCount] = useState(0);
const previous = usePrevious(count);
return (
<div>
<p>previous: {previous}</p>
<p>current: {count}</p>
<button onClick={() => {setCount(count+1);}}>Add</button>
</div>
);
}
// 自定义Hook-获取prevState/prevProps
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
10. useImperativeHandle
该方法可以自定义(默认是DOM节点)子组件暴露给父组件的内容。
useImperativeHandle(ref, createHandle, [deps])
该方法基于Refs转发,需要和React.forwardRef结合使用。
示例:
import React, { useRef,useEffect, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';
function App() {
const inputRef = useRef(null);
useEffect(() => {
// 在父组件查看获取到的子组件中传递的ref的内容
console.log(inputRef); // {current: {focus: () => {...}}}
});
return (
<div>
<FancyInputWithRef ref={inputRef} />
</div>
);
}
// 子组件
function FancyInput(props, ref) {
// 自定义ref暴露的内容
useImperativeHandle(ref, () => ({
focus: () => ref.current.focus()
}), [ref]);
return(
<div>
<input ref={ref} />
</div>
);
}
// 转发ref
var FancyInputWithRef = React.forwardRef(FancyInput);
ReactDOM.render(<App/>, window.root);
11. useLayoutEffect
和useEffect功能基本相同。
主要区别是它的执行时机和componentDidMount,componentDidUpdate相同,都在浏览器绘制之前执行。
建议: 先使用useEffect, 有问题再用useLayoutEffect
12. useDebugValue
用于调试时在自定义Hook中添加标签。
useDebugValue(date, date => date.toDateString());
示例:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}