【实战】 九、深入React 状态管理与Redux机制(二) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十七)


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom ^18.2.0
react-router & react-router-dom ^6.11.2
antd ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
husky ^8.0.3
lint-staged ^13.1.2
prettier 2.8.4
json-server 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
qs ^6.11.0
dayjs ^1.11.7
react-helmet ^6.1.0
@types/react-helmet ^6.1.6
react-query ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/react & @emotion/styled ^11.10.6

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、项目起航:项目初始化与配置

二、React 与 Hook 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求


五、CSS 其实很简单 - 用 CSS-in-JS 添加样式


六、用户体验优化 - 加载中和错误状态处理



七、Hook,路由,与 URL 状态管理



八、用户选择器与项目编辑功能


九、深入React 状态管理与Redux机制

1&2

3.合并组件状态,实现useUndo

功能描述:

可以对一个数字进行不断地赋值,同时记录下历史值;可以通过undo对当前值进行撤销操作,一步步地回到最初值。在进行撤销操作的同时,记录下undo掉的值;通过redo可以回到undo之前的值,不断地redo最终可以回到执行所有撤销操作之前的值。

代码实现

使用useState实现该hook,由于多个state值之前相互引用。因此useCallback的依赖项中会填入多个state作为依赖项,但是在useCallback的回调函数中,在调用时会更新state值导致页面重新渲染,useCallback的回调函数也被重新定义了。

接下来模仿实现 use-undo

新建 src\utils\use-undo.ts

import {
    
     useState } from "react";

export const useUndo = <T>(initialPresent: T) => {
    
    
  // 记录历史操作的合集
  const [past, setPast] = useState<T[]>([])
  const [present, setPresent] = useState(initialPresent)
  // 记录未来操作的合集
  const [future, setFuture] = useState<T[]>([])

  const canUndo = past.length !== 0
  const canRedo = future.length !== 0

  const undo = () => {
    
    
    if (!canUndo) return

    const previous = past[past.length - 1]
    const newPast = past.slice(0, past.length - 1)

    setPast(newPast)
    setPresent(previous)
    setFuture([present, ...future])
  }

  const redo = () => {
    
    
    if (!canRedo) return

    const next = future[0]
    const newFuture = future.slice(1)

    setFuture(newFuture)
    setPresent(next)
    setPast([...past, present])
  }

  const set = (newPresent: T) => {
    
    
    if (newPresent === present) {
    
    
      return
    }
    setPast([...past, present])
    setPresent(newPresent)
    setFuture([])
  }

  const reset = (newPresent: T) => {
    
    
    setPast([])
    setPresent(newPresent)
    setFuture([])
  }

  return [
    {
    
    past, present, future},
    {
    
    redo, undo, set, reset, canRedo, canUndo}
  ] as const
}

现需要对代码做以下优化:

  • 使用 useCallback 避免 造成之前的那种 依赖不等导致的循环渲染
  • 将用到的 变量合并声明 后续也同步改动
  • SetXXX 使用函数形式 直接使用历史状态 避免外界状态的使用,减少依赖
import {
    
     useCallback, useState } from "react";

export const useUndo = <T>(initialPresent: T) => {
    
    
  // 合并声明
  const [state, setState] = useState<{
    
    
    past: T[],
    present: T,
    future: T[]
  }>({
    
    
    past: [],
    present: initialPresent,
    future: []
  })
  
  // 另一种写法
  // const [state, setState] = useState({
    
    
  //   past: [] as T[],
  //   present: initialPresent as T,
  //   future: [] as T[]
  // })

  const canUndo = state.past.length !== 0
  const canRedo = state.future.length !== 0

  const undo = useCallback(() => {
    
    
    setState(currentState => {
    
    
      const {
    
     past, present, future } = currentState
      if (past.length === 0) return currentState
   
      const previous = past[past.length - 1]
      const newPast = past.slice(0, past.length - 1)
   
      return {
    
    
        past: newPast,
        present: previous,
        future: [present, ...future]
      }
    })
  }, [])
  

  const redo = useCallback(() => {
    
    
    setState(currentState => {
    
    
      const {
    
     past, present, future } = currentState
      if (future.length === 0) return currentState
  
      const next = future[0]
      const newFuture = future.slice(1)

      return {
    
    
        past: [...past, present],
        present: next,
        future: newFuture
      }
    })
  }, [])

  const set = useCallback((newPresent: T) => {
    
    
    setState(currentState => {
    
    
      const {
    
     past, present } = currentState
      if (newPresent === present) {
    
    
        return currentState
      }
      return {
    
    
        past: [...past, present],
        present: newPresent,
        future: []
      }
    })
  }, [])

  const reset = useCallback((newPresent: T) => {
    
    
    setState({
    
    
      past: [],
      present: newPresent,
      future: []
    })
  }, [])

  return [
    state,
    {
    
    redo, undo, set, reset, canRedo, canUndo}
  ] as const
}

4.用useReducer进行状态管理

替代方案:

useReducer作为useState 的替代方案。它接收一个形如 (state, action) => newState的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

相比较于useStateuseReducer 具有如下优点:

  • state中的状态值之间相互关联;
  • 下一个 state的更新依赖于之前的 state

下面使用 useReducer 再对 use-undo 进行改写

编辑 src\utils\use-undo.ts

import {
    
     useCallback, useReducer, useState } from "react";

const UNDO = 'UNDO'
const REDO = 'REDO'
const SET = 'SET'
const RESET = 'RESET'

type State<T> = {
    
    
  past: T[];
  present: T;
  future: T[];
}

type Action<T> = {
    
     newPresent?: T, type: typeof UNDO | typeof REDO | typeof SET | typeof RESET }

const undoReducer = <T>(state: State<T>, action: Action<T>) => {
    
    
  const {
    
     past, present, future } = state
  const {
    
     newPresent, type } = action

  switch(type) {
    
    
    case UNDO: {
    
    
      if (past.length === 0) return state;

      const previous = past[past.length - 1];
      const newPast = past.slice(0, past.length - 1);

      return {
    
    
        past: newPast,
        present: previous,
        future: [present, ...future],
      };
    }
    case REDO: {
    
    
      if (future.length === 0) return state;

      const next = future[0];
      const newFuture = future.slice(1);

      return {
    
    
        past: [...past, present],
        present: next,
        future: newFuture,
      };
    }
    case SET: {
    
    
      if (newPresent === present) {
    
    
        return state;
      }
      return {
    
    
        past: [...past, present],
        present: newPresent,
        future: [],
      };
    }
    case RESET: {
    
    
      return {
    
    
        past: [],
        present: newPresent,
        future: [],
      }
    }
    default: 
      return state
  }
}

export const useUndo = <T>(initialPresent: T) => {
    
    
  const [state, dispatch] = useReducer(undoReducer, {
    
    
    past: [],
    present: initialPresent,
    future: [],
  } as State<T>)

  const canUndo = state.past.length !== 0;
  const canRedo = state.future.length !== 0;

  const undo = useCallback(() => dispatch({
    
     type: UNDO }), []);

  const redo = useCallback(() => dispatch({
    
     type: REDO }), []);

  const set = useCallback((newPresent: T) => dispatch({
    
    newPresent, type: SET}), []);

  const reset = useCallback((newPresent: T) => dispatch({
    
    newPresent, type: RESET}), []);

  return [state, {
    
     redo, undo, set, reset, canRedo, canUndo }] as const;
};

统一状态管理后 虽然代码量多了,但是经过多重封装,层次更加清晰

可以发现 useReducer 相对 useState 适合定义多个相互影响的状态量

鉴于 useReducer 针对复杂的state关系和更新的前后依赖的优势,因此 useAsync 非常适合使用 useReducer 来重构

接下来使用 useReducer 改造一下 与 use-undo 结构类似的 use-async(src\utils\use-async.ts):

...
const useSafeDispatch = <T>(dispatch: (...args: T[]) => void) => {
    
    
  const mountedRef = useMountedRef()
  return useCallback((...args: T[]) => (mountedRef.current ? dispatch(...args) : void 0), [dispatch, mountedRef])
}

export const useAsync = <D>(...) => {
    
    
  const config = {
    
     ...defaultConfig, ...initialConfig };
  const [state, dispatch] = useReducer((state: State<D>, action: Partial<State<D>>) => ({
    
    ...state, ...action}), {
    
    
    ...defaultInitialState,
    ...initialState,
  });
  const safeDispatch = useSafeDispatch(dispatch);
  const [rerun, setRerun] = useState(() => () => {
    
    });

  const setData = useCallback(
    (data: D) =>
      safeDispatch(...),
    [safeDispatch]
  );

  const setError = useCallback(
    (error: Error) =>
      safeDispatch(...),
    [safeDispatch]
  );

  // run 来触发异步请求
  const run = useCallback(
    (...) => {
    
    
      ...
      safeDispatch({
    
     stat: "loading" });
      return promise
        .then((data) => {
    
    
          setData(data);
          return data;
        })
        .catch(...);
    },
    [config.throwOnError, safeDispatch, setData, setError]
  );
  ...
};

部分引用笔记还在草稿阶段,敬请期待。。。

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/132035398