React系列-Mixin、HOC、Render Props(上)
React系列-轻松学会Hooks(中)
React系列-自定义Hooks很简单(下)
我们在第二篇文章中介绍了一些常用的hooks,接着我们继续来介绍剩下的hooks吧
useReducer
作为useState 的替代方案。它接收一个形如
(state, action) => newState 的 reducer
,并返回当前的 state 以及与其配套的 dispatch 方法
。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
不明白Redux工作流的同学可以看看这篇Redux系列之分析中间件原理(附经验分享)
为什么使用
官方说法: 在某些场景下,useReducer 会比 useState 更适用,
例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化
,因为你可以向子组件传递 dispatch 而不是回调函数 。
总结来说:
如果你的state是一个数组或者对象等复杂数据结构
如果你的state变化很复杂,
经常一个操作需要修改很多state
如果你希望构建自动化测试用例来保证程序的稳定性
如果你需要在深层子组件里面去修改一些状态(也就是useReducer+useContext代替Redux)
如果你用应用程序比较大,希望UI和业务能够分开维护
登录场景
举个例子🌰:
登录场景
useState完成登录场景
function LoginPage() { const [name, setName] = useState(''); // 用户名 const [pwd, setPwd] = useState(''); // 密码 const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中 const [error, setError] = useState(''); // 错误信息 const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录 const login = (event) => { event.preventDefault(); setError(''); setIsLoading(true); login({ name, pwd }) .then(() => { setIsLoggedIn(true); setIsLoading(false); }) .catch((error) => { // 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识 setError(error.message); setName(''); setPwd(''); setIsLoading(false); }); } return ( // 返回页面JSX Element ) }
useReducer完成登录场景
const initState = { name: '', pwd: '', isLoading: false, error: '', isLoggedIn: false, } function loginReducer(state, action) { switch(action.type) { case 'login': return { ...state, isLoading: true, error: '', } case 'success': return { ...state, isLoggedIn: true, isLoading: false, } case 'error': return { ...state, error: action.payload.error, name: '', pwd: '', isLoading: false, } default: return state; } } function LoginPage() { const [state, dispatch] = useReducer(loginReducer, initState); const { name, pwd, isLoading, error, isLoggedIn } = state; const login = (event) => { event.preventDefault(); dispatch({ type: 'login' }); login({ name, pwd }) .then(() => { dispatch({ type: 'success' }); }) .catch((error) => { dispatch({ type: 'error' payload: { error: error.message } }); }); } return ( // 返回页面JSX Element ) }
❗️我们的state变化很复杂,经常一个操作需要修改很多state
,另一个好处是所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发登录success状态,只需要dispatch({ type: 'success' })。
笔者[狗头]认为,暂时应该不会用useReducer
替代useState
,毕竟Redux
的写法实在是很繁琐
复杂数据结构场景
刚好最近笔者的项目就碰到了复杂数据结构场景
,可是并没有用useReducer
来解决,依旧采用useState
,原因很简单:方便
// 定义list类型 export interface IDictList extends IList { extChild: { curPage: number totalSize: number size: number // pageSize list: IList[] } } const [list, setList] = useState<IDictList[]>([]) const change=()=>{ const datalist = JSON.parse(JSON.stringify(list)) // 拷贝对象 地址不同 不过这种写法感觉不好 建议用reducers 应该封装下reducers写法 const data = await getData() const { totalCount, pageSize, list } = data item.extChild.totalSize = totalCount item.extChild.size = pageSize item.extChild.list = list setList(datalist) // 改变 }
看typescript写的类型声明就知道了这个list变量是个复杂的数据结构,需要经常需要改变添加extChild.list数组的内容,但是这种Array.prototype.push
,是不会触发更新,做过是通过const datalist = JSON.parse(JSON.stringify(list))
。虽然没有使用useReducer
进行替代,笔者还是推荐大家试试
如何使用
const [state, dispatch] = useReducer(reducer, initialArg, init);
知识点合集
引用不变
useReducer返回的state
跟ref一样,引用是不变的,不会随着函数组件的重新更新而变化,因此useReducer也可以解决闭包陷阱
const setCountReducer = (state,action)=>{ switch(action.type){ case 'add': return state+action.value case 'minus': return state-action.value default: return state }}const App = ()=>{ const [count,dispatch] = useReducer(setCountReducer,0) useEffect(()=>{ const timeId = setInterval(()=>{ dispatch({type:'add',value:1}) },1000) return ()=> clearInterval(timeId) },[]) return ( <span>{count}</span> )}
把setCount改成useReducer的dispatch,因为useReducer的dispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props
useContext
,
useContext
肯定与React.createContext有关系的,接收一个context 对象(React.createContext 的返回值)
并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的<MyContext.Provider> 的 value prop 决定。
为什么使用
如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
简单点说就是useContext
是用来消费context API
的
如何使用
const value = useContext(MyContext);
知识点合集
useContext造成React.memo 无效
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使
祖先使用 React.memo 或 shouldComponentUpdate
,❗️也会在组件本身使用 useContext 时重新渲染
。
举个例子🌰:
// 创建一个 contextconst Context = React.createContext()// 用memo包裹const Item = React.memo((props) => { // 组件一, useContext 写法 const count = useContext(Context); console.log('props', props) return ( <div>{count}</div> )})const App = () => { const [count, setCount] = useState(0) return ( <div> 点击次数: { count} <button onClick={() => { setCount(count + 1) }}>点我</button> <Context.Provider value={count}> <Item /> </Context.Provider> </div> )}
结果:
可以看到即使props没有变化,一旦组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染
,此时Memo就失效了
Hooks替代Redux
有了
useReducer
和useContext
以及React.createContext
API,我们可以实现自己的状态管理
来替换Redux
实现react-redux
react-redux:React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.
简单理解就是连接组件和数据中心,也就是把React和Redux联系起来
,可以看看官方文档或者看看阮一峰老师的文章,这里我们要去实现它最主要的两个API
Provider 组件
Provider:组件之间共享的数据是 Provider 这个顶层组件通过 props 传递下去的,store必须作为参数放到Provider组件中去
利用
React.createContext
这个API,实现起来非常easy,react-redux
本身就是依赖这个API的
const MyContext = React.createContext()const MyProvider = MyContext.Providerexport default MyProvider // 导出
connect
connect:connect是一个高阶组件,提供了一个连接功能,可用于将组件连接到store,它 提供了组件获取 store 中数据或者更新数据的接口(mapStateToProps和mapStateToProps)
的能力
connect([mapStateToProps], [mapStateToProps], [mergeProps], [options])
function connect(mapStateToProps, mapDispatchToProps) { return function (Component) { return function () { const {state, dispatch} = useContext(MyContext) const stateToProps = mapStateToProps(state) const dispatchToProps = mapDispatchToProps(dispatch) const props = {...props, ...stateToProps, ...dispatchToProps} return ( <Component {...props} /> ) } }}export default connect // 导出
创建store
store: store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
利用useReducer来创建我们的store
import React, { Component, useReducer, useContext } from 'react';import { render } from 'react-dom';import './style.css';const MyContext = React.createContext()const MyProvider = MyContext.Provider;function connect(mapStateToProps, mapDispatchToProps) { return function (Component) { return function () { const {state, dispatch} = useContext(MyContext) const stateToProps = mapStateToProps(state) const dispatchToProps = mapDispatchToProps(dispatch) const props = {...props, ...stateToProps, ...dispatchToProps} return ( <Component {...props} /> ) } }}function FirstC(props) { console.log("FirstC更新") return ( <div> <h2>这是FirstC</h2> <h3>{props.books}</h3> <button onClick={()=> props.dispatchAddBook("Dan Brown: Origin")}>Dispatch 'Origin'</button> </div> )}function mapStateToProps(state) { return { books: state.Books }}function mapDispatchToProps(dispatch) { return { dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload}) }}const HFirstC = connect(mapStateToProps, mapDispatchToProps)(FirstC)function SecondC(props) { console.log("SecondC更新") return ( <div> <h2>这是SecondC</h2> <h3>{props.books}</h3> <button onClick={()=> props.dispatchAddBook("Dan Brown: The Lost Symbol")}>Dispatch 'The Lost Symbol'</button> </div> )}function _mapStateToProps(state) { return { books: state.Books }}function _mapDispatchToProps(dispatch) { return { dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload}) }}const HSecondC = connect(_mapStateToProps, _mapDispatchToProps)(SecondC)function App () { const initialState = { Books: 'Dan Brown: Inferno' } const [state, dispatch] = useReducer((state, action) => { switch(action.type) { case 'ADD_BOOK': return { Books: action.payload } default: return state } }, initialState); return ( <div> <MyProvider value={{state, dispatch}}> <HFirstC /> <HSecondC /> </MyProvider> </div> )}render(<App />, document.getElementById('root'));
结果:
嗯嗯😊,我们就这样实现了一个状态管理
缺陷
缺少时间旅行
不支持中间件
性能极差
可以看到上面的结果,一个状态变化,所有组件都重新渲染
,嗯嗯😊,所以我们这是个demo玩玩而已,不要用于生产中
最后贴下Redux作者的回答:
useLayoutEffect
useLayoutEffect和useEffect一样也是处理副作用
,其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前
,useLayoutEffect 内部的更新计划将被同步刷新。
❗️官方尽量推荐使用useEffect,因为useLayoutEffect,useLayoutEffect里面的callback函数会在DOM更新完成后立即执行
,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制
区别就是:useEffect是异步的,useLayoutEffect是同步的
为什么使用
解决一些闪烁场景
如何使用
useLayoutEffect(fn, []) // 接收两个参数 一个是回调函数 另外一个是数组类型的参数(表示依赖)
知识点合集
⛽️暂无...
自定义hooks
自定义Hooks很简单,利用官方提供的Hook我们可以把重用的逻辑抽离出来,也就是我们的自定义Hook,当你在一个项目中发现大量类似代码,那就抽离成Hooks吧
❗️前面我们分析了Mixin,HOC,Render Props
这些模式来实现状态逻辑复用
,这里的自定义hooks
也是解决状态逻辑复用问题
的一种模式(😊终于快完结了)
根据业务来说,我把自定义Hooks分为两类,一类是自定义基础Hooks
,另一类是自定义业务Hooks
业务Hooks
比如我们多个页面有相同的请求方法
// 以use开头export const useUserData = (category: string[], labelName?: string) => { const [baseTotal, setBaseTotal] = useState<number>(0) useEffect(() => { dealSearchTotal(category, labelName) }, [labelName, category]) const dealSearchTotal = async ( ) => { const data = await getUserData(curCategory, labelName) const { baseTotal, calculateTotal, basicTotal, extTotal } = data setBaseTotal(baseTotal) } // 最后return出想要的数据 return [baseTotal, calculateTotal, basicTotal, extTotal]}
❗️好如果你注意到你写了重复代码,抽离成自定义Hooks是没问题的
基础Hooks
基础Hooks就是平时与业务无关的工具方法
useEffectOnce
该Hooks在函数组件只执行一次
const useEffectOnce = (effect) => { useEffect(effect, []);};export default useEffectOnce;
useMount
该Hook在组件挂载时调用
const useMount = (fn) => { useEffectOnce(() => { fn(); });};export default useMount;
useUnmount
该Hook在组件销毁时调用
const useUnmount = (fn: () => any): void => { const fnRef = useRef(fn); fnRef.current = fn; useEffectOnce(() => () => fnRef.current());};export default useUnmount;
usePrevious
获取组件的state或者props的旧值
const usePrevious = (state): => { const ref = useRef(); useEffect(() => { ref.current = state; }); return ref.current;};export default usePrevious;
❗️其它参考Umi Hooks
最后
本文分享自微信公众号 - 前端壹栈(Ecmscript)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。