React Hook
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; }