最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用 Hook 实现一个 Redux

    正文概述 掘金(凤晴铃玉)   2021-02-11   542

    使用 React 的时候,比较常见的状态管理工具就是 Redux,虽然 Redux 写的代码特别多,但是不可否认的是 Redux 的设计原则中的数据不可变这条原则,其实完美的契合了 React。因而在 Redux 中广泛使用,再加上丰富的中间件,比如 redux-thunkredux-sagaDva 等,Redux 的热度一直很高,在 Hook 时代,也有 useSelector useDispatch 这些很有用的 Hook,大大提高开发效率。不过 Hook 时代,也可以通过 useReducer Context API 实现类似 Redux,这样就不用导入 Redux

    React.createContext

    官方文档,官网文档如是说,其实就是一个父组件的状态,全部子组件都能获取,并且不需要通过属性一层一层传递

    React.useReducer

    useReducer,useReducer 的用法和 Redux 的用法非常相似,需要传入一个 reducer 和初始的 state,返回一个 state 和一个类似 Redux dispatch 的操作函数,同时也符合 Redux 的设计理念,数据只读,通过纯函数修改数据。

    Context 和 useReducer 结合

    可以通过将 useReducer 返回的 statedispatch 作为 Context.Providervalue,这样被 Provider 包裹的子组件就都能获得 state 和操作 state

    // store.js
    import { createContext, useReducer } from 'react';
    
    const initState = {
      count: 0,
      text: '',
    };
    
    const reducer = (state, action) => {
      const { type, payload } = action;
      switch (type) {
        case 'add':
          return { ...state, count: state.count + 1 };
        case 'minus':
          return { ...state, count: state.count - 1 };
        case 'change-text':
          return { ...state, ...payload };
        default:
          return state;
      }
    };
    
    export const Store = createContext(null);
    
    const Provider = (props) => {
      const [state, dispatch] = useReducer(reducer, initState);
      return (
        <Store.Provider value={{ state, dispatch }}>
          {props.children}
        </Store.Provider>
      );
    };
    
    export default Provider;
    

    store.js 里面初始化了一个 Store,初始化的时候默认可以不传值,可以通过 Store.Provider 传值,通过 useReducer 初始化 statedispatch,这里可以看到 useReducer 的入参和 Redux 一样,都是默认的 statereducer

    如何使用,只要把这个组件包裹在你需要这些状态的地方就好了,如果是全局的状态,直接包裹在最外面的组件就好

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import Provider from './store';
    
    ReactDOM.render(
      <Provider>
        <App />
      </Provider>,
      document.getElementById('root')
    );
    

    这样包裹的话,整个 React 应用都能使用 statedispatch

    子组件如何使用

    上面提到怎么创建一个 Provider 和怎么注入到组件中,组件怎么使用这个状态依旧没有提到,实现方法主要有两个。

    • 可以借用 useContext
    • 可以参考一下 React-Redux 的做法,用高阶组件 connect 包裹获需要获取状态的组件

    useContext

    const value = useContext(MyContext);
    

    官方文档,useContext 可以订阅 Context 对象,当 Context 对象的 Providervalue 发生改变的时候,useContext 会获取最新的最新的 value

    假设应用根组件就是 App.jsCountTxt 分别依赖 state.textstate.count

    // app.js
    import Count from './count';
    import Txt from './txt';
    
    function App() {
      return (
        <div>
          <Count />
          <Txt />
        </div>
      );
    }
    
    export default App;
    
    // count.js
    import { useContext } from 'react';
    import { Store } from './store';
    const Count = () => {
      const { dispatch, state } = useContext(Store);
      const add = () => {
        dispatch({ type: 'add' });
      };
      const minus = () => {
        dispatch({ type: 'minus' });
      };
      return (
        <div>
          <div className="count">{state.count}</div>
          <button onClick={add}>Add</button>
          <button onClick={minus}>Minus</button>
        </div>
      );
    };
    
    export default Count;
    
    import { useContext } from 'react';
    import { Store } from './store';
    
    const Txt = () => {
      const { dispatch, state } = useContext(Store);
      const changeText = () => {
        const text = Math.random().toString();
        dispatch({ type: 'change-text', payload: { text } });
      };
      return (
        <div>
          <div className="txt">Text: {state.text}</div>
          <button onClick={changeText}>Change Text</button>
        </div>
      );
    };
    
    export default Txt;
    

    按照上面的写法,CountTxt 都能获得到 state 并且还能操作 state 了,但是这样有一个问题,仔细看官网文档,只要 value 发生改变了就会重新渲染,这里明显有问题的,Count 只依赖 state.countTxt 只依赖 state.text,但是上面那个写法,摆明就就是 state 改变了两个组件就会重新渲染,解决方案也挺多的

    • 最简单的避免方式,在 App 中取这个变量,然后分发到不同的组件中去,例如
    import Count from './count';
    import Txt from './txt';
    import { Store } from './store';
    
    function App() {
      const { dispatch, state } = useContext(Store);
      return (
        <div>
          <Count count={state.count} dispatch={dispatch} />
          <Txt text={state.text} dispatch={dispatch} />
        </div>
      );
    }
    
    export default App;
    
    • 可以借助 useMemo

    useMemo useCallback

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    

    官方链接,useMemo 是一个记忆化的值,当它的依赖改变的时候,它才会重新执行函数,这个和 useCallback 差不多,useCallback 是返回一个记忆化的函数,当依赖发生改变的时候,才会重新生成一个新的函数

    这个看起来和上面的内容没什么关联,但是仔细想一下,组件每次执行也是返回一个 jsx 组件,如果只要必要依赖的值发生改变的时候,才返回新的组件,不然就返回一个记忆化的组件
    配合 useMemo, Txt 组件可以修改成这样,dispatch 是一定不会改变的,所以 useCallback 也可以把依赖写成空数组,如果不用 useCallback ,每次 state 改变的时候,changeText 就重新定义了,这样还是会导致 useMemo 重新执行,或者可以不把 changeText 加入 useMemo 依赖列表,在这个组件是可以的,最好还是建议写上去,Count 组件也是同样的修改

    const Txt = () => {
      const { dispatch, state } = useContext(Store);
      const changeText = useCallback(() => {
        const text = Math.random().toString();
        dispatch({ type: 'change-text', payload: { text } });
      }, [dispatch]);
      return useMemo(
        () => (
          <div>
            <div className="txt">Text: {state.text}</div>
            <button onClick={changeText}>Change Text</button>
          </div>
        ),
        [state.text, changeText]
      );
    };
    

    实现一个 connect

    使用 useContext 有个缺点,就是使用到 Store 的地方,必须把源 Store 导进来,写多了也很繁琐。解决的方式也是有的

    • 可以参考 react reduxconnect 高阶组件,实现一个 connect 如下
    import { useContext, useMemo } from 'react';
    import { Store } from './store';
    
    const connect = (fn) => {
      if (typeof fn !== 'function') {
        throw new Error('first param must be function');
      }
      return (WrappedComponent) => (props) => {
        const { dispatch, state } = useContext(Store);
        const value = fn(state);
        return useMemo(
          () => <WrappedComponent {...props} {...value} dispatch={dispatch} />,
          // 注意这里
          [JSON.stringify(value), dispatch, props]
        );
      };
    };
    
    export default connect;
    

    仔细看下,这里就是一个很简单的高阶组件,没有实现 connectequalFn 第二个参数,第一个参数和 connect 一样,都是必须返回一个对象.因为是返回一个对象,所以,每次执行函数的返回也是不一样的,value 的值只要 state 做了改变就会改变,就像刚刚的 TextCount 组件一样,修改了一个和组件内部无任何关联的属性,组件也重新渲染了,这样不符合要求。这里使用一个简单的 JSON.stringify 处理。Redux connect 的处理方式是实现一个 shallowEqual,实现起来也很简单

    • 使用 connect
    import connect from './connect';
    
    const Count = ({ num, dispatch, count }) => {
      const add = () => {
        dispatch({ type: 'add' });
      };
      const minus = () => {
        dispatch({ type: 'minus' });
      };
      console.log('wrapper reload');
      return (
        <div>
          <div className="count">count from context {count}</div>
          <div className="num">num from props {num}</div>
          <button onClick={add}>Add</button>
          <button onClick={minus}>Minus</button>
        </div>
      );
    };
    
    export default connect((state) => ({
      count: state.count,
    }))(Count);
    

    这样能解决问题,但是 Hook 时代也有 Hook 的解法,可以参考 react-reduxuseDispatch useSelector ,但是笔者太菜,折腾了半天依旧没有写出像 react-redux useSelector 那让无关属性不会导致组件重新渲染的,所以使用的时候还得搭配 useMemo

    useSelector useDispatch useMemo

    // useDispatch useReducer 生成的 dispatch 是绝对不会改变的,所以直接返回就好
    const useDispatch = () => {
      const { dispatch } = useContext(Store);
      return dispatch;
    };
    
    // 折腾了半天,也没有找到解决方法,干脆放飞自我了
    const useSelector = (fn) => {
      if (typeof fn !== 'function') {
        throw new Error('first param must be function');
      }
      const { state } = useContext(Store);
      const value = fn(state);
      return value;
    };
    
    // 使用的时候要调配 useMemo,这样也方便一些,不用每个组件都 useContext 一下
    const Count = () => {
      const count = useSelector((state) => state.count);
      const dispatch = useDispatch();
      const add = useCallback(() => {
        dispatch({ type: 'add' });
      }, [dispatch]);
      const minus = useCallback(() => {
        dispatch({ type: 'minus' });
      }, [dispatch]);
    
      return useMemo(() => {
        console.log(count);
        return (
          <div>
            <div className="count">{count}</div>
            <button onClick={add}>Add</button>
            <button onClick={minus}>Minus</button>
          </div>
        );
      }, [add, count, minus]);
    };
    

    总结

    Hook 出来之后,React 的可玩性就更高了,一个简单的状态管理,还能玩出花来。希望这篇文章给读者带来收获,还有,新年快乐!


    起源地下载网 » 使用 Hook 实现一个 Redux

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元