更新时间:2022-08-12 21:55:35
分享一下用了将近一年hooks使用心得
useState const [state, setState] = useState(initialState)
function Love() { const [like, setLike] = useState(false) const likeFn = () => (newLike) => setLike(newLike) return ( <> 你喜欢我吗: {like ? 'yes' : 'no'} <button onClick={likeFn(true)}>喜欢</button> <button onClick={likeFn(false)}>不喜欢</button> </> ) }
function mountWorkInProgressHook() { // 注意,单个 hook 是以对象的形式存在的 var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; if (workInProgressHook === null) { firstWorkInProgressHook = workInProgressHook = hook; /* 等价 let workInProgressHook = hooks firstWorkInProgressHook = workInProgressHook */ } else { workInProgressHook = workInProgressHook.next = hook; } // 返回当前的 hook return workInProgressHook; }
每个 hook 都会有一个 next 指针,hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer 再看看更新阶段发生了什么
// ReactFiberHooks.js const HooksDispatcherOnUpdate: Dispatcher = { // ... useState: updateState, } function updateState(initialState) { return updateReducer(basicStateReducer, initialState); } function updateReducer(reducer, initialArg, init) { const hook = updateWorkInProgressHook(); const queue = hook.queue; if (numberOfReRenders > 0) { const dispatch = queue.dispatch; if (renderPhaseUpdates !== null) { // 获取Hook对象上的 queue,内部存有本次更新的一系列数据 const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); if (firstRenderPhaseUpdate !== undefined) { renderPhaseUpdates.delete(queue); let newState = hook.memoizedState; let update = firstRenderPhaseUpdate; // 获取更新后的state do { // useState 第一个参数会被转成 useReducer const action = update.action; newState = reducer(newState, action); //按照当前链表位置更新数据 update = update.next; } while (update !== null); hook.memoizedState = newState; // 返回新的 state 以及 dispatch return [newState, dispatch]; } } } // ... }
结合实际让我们看下面一组 hooks
let isMounted = false if(!isMounted) { [name, setName] = useState("张三"); [age] = useState("25"); isMounted = true } [sex, setSex] = useState("男"); return ( <button onClick={() => { setName(李四"); }} > 修改姓名 </button> );
首次渲染时 hook 顺序为
name => age => sex
二次渲染的时根据上面的例子,调用的 hook 的只有一个
setSex
所以总结一下初始化阶段构建链表,更新阶段按照顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染当两次顺序不一样的时候就会造成渲染上的差异。
为了避免出现上面这种情况我们可以安装 eslint-plugin-react-hooks
// 你的 ESLint 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则 "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖 } }
useEffect(effect, array)
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange); }; });
跟 useEffect 使用差不多,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题 useLayoutEffect 会阻塞渲染,请谨慎使用
import React, { useLayoutEffect, useEffect, useState } from 'react'; import './App.css' function App() { const [value, setValue] = useState(0); useEffect(() => { if (value === 0) { setValue(10 + Math.random() * 200); } }, [value]); const test = () => { setValue(0) } const color = !value ? 'red' : 'yellow' return ( <React.Fragment> <p style={{ background: color}}>value: {value}</p> <button onClick={test}>点我</button> </React.Fragment> ); } export default App;
const context = useContext(Context)
useContext 从名字上就可以看出,它是以 Hook 的方式使用 React Context, 先简单介绍 Context 的概念和使用方式
import React, { useContext, useState, useEffect } from "react"; const ThemeContext = React.createContext(null); const Button = () => { const { color, setColor } = React.useContext(ThemeContext); useEffect(() => { console.info("Context changed:", color); }, [color]); const handleClick = () => { console.info("handleClick"); setColor(color === "blue" ? "red" : "blue"); }; return ( <button type="button" onClick={handleClick} style={{ backgroundColor: color, color: "white" }} > toggle color in Child </button> ); }; // app.js const App = () => { const [color, setColor] = useState("blue"); return ( <ThemeContext.Provider value={{ color, setColor }}> <h3> Color in Parent: <span style={{ color: color }}>{color}</span> </h3> <Button /> </ThemeContext.Provider> ); };
const [state, dispatch] = useReducer(reducer, initialArg, init)
语法糖跟 redux 差不多,放个基础 ????
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: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); }
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
一个常用来做性能优化的 hook,看个 ????
const MemoDemo = ({ count, color }) => { useEffect(() => { console.log('count effect') }, [count]) const newCount = useMemo(() => { console.log('count 触发了') return Math.round(count) }, [count]) const newColor = useMemo(() => { console.log('color 触发了') return color }, [color]) return <div> <p>{count}</p> <p>{newCount}</p> {newColor}</div> }
我们这个时候将传入的 count 值改变 的,log 执行循序
count 触发了 count effect
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
useCallback 的用法和上面 useMemo 差不多,是专门用来缓存函数的 hooks
下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给onClick的时候每次都传不同的函数引用
import React, { useState, useCallback } from 'react' function MemoCount() { const [value, setValue] = useState(0) memoSetCount = useCallback(()=>{ setValue(value + 1) },[]) return ( <div> <button onClick={memoSetCount} > Update Count </button> <div>{value}</div> </div> ) } export default MemoCount
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook 一般我将 hooks 分为这几类
顾名思义工具类,比如 useDebounce、useInterval、useWindowSize 等等。例如下面 useWindowSize
import { useEffect, useState } from 'react'; export default function useWindowSize(el) { const [windowSize, setWindowSize] = useState({ width: undefined, height: undefined, }); useEffect( () => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', handleResize); }, [el], ); return windowSize; }
像之前的我们有一个公用的城市列表接口,在用 redux 的时候可以放在全局公用,不用的话我们就可能需要复制粘贴了。有了 hooks 以后我们只需要 use 一下就可以在其他地方复用了
import { useState, useEffect } from 'react'; import { getCityList } from '@/services/static'; const useCityList = (params) => { const [cityList, setList] = useState([]); const [loading, setLoading] = useState(true) const getList = async () => { const { success, data } = await getCityList(params); if (success) setList(data); setLoading(false) }; useEffect( () => {getList();}, [], ); return { cityList, loading }; }; export default useCityList; // bjs function App() { // ... const { cityList, loading } = useCityList() // ... }
逻辑类,比如我们有一个点击用户头像关注用户或者取消关注的逻辑,可能在评论列表、用户列表都会用到,我们可以这样做
import { useState, useEffect } from 'react'; import { followUser } from '@/services/user'; const useFollow = ({ accountId, isFollowing }) => { const [isFollow, setFollow] = useState(false); const [operationLoading, setLoading] = useState(false) const toggleSection = async () => { setLoading(true) const { success } = await followUser({ accountId }); if (success) { setFollow(!isFollow); } setLoading(false) }; useEffect( () => { setFollow(isFollowing); }, [isFollowing], ); return { isFollow, toggleSection, operationLoading }; }; export default useFollow;
只需暴露三个参数就能满足大部分场景
还有一些和 UI 一起绑定的 hook, 但是这里有点争议要不要和 ui 一起混用。就我个人而言一起用确实帮我解决了部分复用问题,我还是分享出来。
import React, { useState } from 'react'; import { Modal } from 'antd'; // TODO 为了兼容一个页面有多个 modal, 目前想法通过唯一 key 区分,后续优化 export default function useModal(key = 'open') { const [opens, setOpen] = useState({ [key]: false, }); const onCancel = () => { setOpen({ [key]: false }); }; const showModal = (type = key) => { setOpen({ [type]: true }); }; const MyModal = (props) => { return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />; }; return { showModal, MyModal, }; } // 使用 function App() { const { showModal, MyModal } = useModal(); return <> <button onClick={showModal}>展开</button> <MyModal onOk={console.log} /> </> }
越来越多的 react 配套的三方库都上了 hooks 版,像 react-router、redux 都出了 hooks。
同时也出现了一些好用的 hooks 库,比如 ahooks 这种。自从用了 hooks 以后我就两个字,真香