zoukankan      html  css  js  c++  java
  • React系列-自定义Hooks很简单

    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 时重新渲染

    举个例子????:

    // 创建一个 context
    const 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

    有了useReduceruseContext以及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.Provider
    
    export 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

    最后

  • 相关阅读:
    当苹果因为UIDevice、udid、uniqueIdentifier而把我们的应用拒之门外invalid binary的时候,呕心沥血解决方法啊
    IOS 7 自定义的UIAlertView不能在iOS7上正常显示
    IOS7 新特性(针对同样讨厌更新后IOS7的开发者)
    CFBundleVersion与CFBundleShortVersionString
    iOS 7 SDK: 如何使用后台获取(Background Fetch)
    IOS开发经验总结(二)
    iOS开发经验总结(一)
    让iOS应用支持不同版本的系统与设备
    控制iOS 7中的状态栏
    Implement CGLIB in ABAP
  • 原文地址:https://www.cnblogs.com/vnues/p/14300329.html
Copyright © 2011-2022 走看看