【实战】 八、用户选择器与项目编辑功能(下) —— 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 状态管理



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

1~3

4.编辑后刷新-useState的懒初始化与保存函数状态

之前的遗留问题现在尝试解决

修改 src\utils\use-async.ts(新增 rerun 方法, 保存上一次 run 的运行状态):

...
export const useAsync = <D>(...) => {
    
    
  ...
  const [rerun, setRerun] = useState(() => {
    
    })
  ...

  // run 来触发异步请求
  const run = (promise: Promise<D>) => {
    
    
    if (!promise || !promise.then) {
    
    
      throw new Error("请传入 Promise 类型数据");
    }
    setRerun(() => run(promise))
    setState({
    
     ...state, stat: "loading" });
    return promise.then(...).catch(...);
  };

  return {
    
    
    ...
    // rerun 重新运行一遍 run, 使得 state 刷新
    rerun,
    ...state,
  };
};

相对直接定义变量,通过 useState 定义的变量在组件刷新时会保持之前的状态,除非重新setState, 而直接定义会重新初始化

src\screens\ProjectList\index.tsx 中尝试调用,使用前先打印一下:

...
export const ProjectList = () => {
    
    
  ...
  const {
    
     isLoading, error, data: list, rerun } = useProjects(useDebounce(param));
  ...
  console.log('rerun', rerun)

  return (
    <Container>
      <h1>项目列表</h1>
      {
    
    /* <Button onClick={rerun}>rerun</Button> */}
      ...
    </Container>
  );
};
...

…有报错:Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

尝试在 rerun 中,run 执行前面一步打印,编辑src\utils\use-async.ts

...
export const useAsync = <D>(...) => {
    
    
  ...
  const run = (promise: Promise<D>) => {
    
    
    ...
    setRerun(() => {
    
    
      console.log('set rerun')
      run(promise)
    })
    ...
  };
  ...
};

一直不停打印 “set rerun”, 但此时并没有运行 rerun,这样就有理由怀疑在 rerun 赋值时就会直接执行,即 useState 不能直接保存函数

codesandbox 上测试一下:

export default function App() {
    
    
  const [lazyValue, setLazyValue] = React.useState(() => {
    
    
    console.log('i am lazy')
  })
  console.log(lazyValue);
  return (
    <div className="App">
      <button onClick={
    
    () => setLazyValue(() => {
    
     console.log('update lazyValue') })}>
        setCallback
      </button>
      <button onClick={
    
    lazyValue}>call callback</button>
    </div>
  );
}

果然,不仅在赋值时,初始化时就直接执行了

看下 useState 的函数签名:

/**
 * Returns a stateful value, and a function to update it.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usestate
 */
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

可以注意到 initialState 是一个联合类型, S 是常用的形式,() => S 为啥要单列出来呢?

可以查看官方文档:惰性初始 state | Hook API 索引 – React

通过文档可以得知,这种初始化方式仅执行一次,且用于初始化值需要复杂计算才能得到的情况(昂贵的计算,消耗性能)

既然如此,何不在外面再加一层函数呢?试一下:

export default function App() {
    
    
  const [callback, setCallback] = React.useState(() => () => {
    
    
    console.log('i am callback')
  })
  console.log(callback);
  return (
    <div className="App">
      <button onClick={
    
    () => setCallback(() => () => {
    
     console.log('update callback') })}>
        setCallback
      </button>
      <button onClick={
    
    callback}>call callback</button>
    </div>
  );
}

果然在初始化后可以直接调用 callback,在 setCallback 后再调用又是另一个函数了

除了这种方式,还可以使用 useRef

export default function App() {
    
    
  const callbackRef = React.useRef(() => console.log('i am callback'));
  const callback = callbackRef.current;
  console.log(callback);
  return (
    <div className="App">
      <button onClick={
    
    () => (callbackRef.current = () => console.log('update callback'))}>
        setCallback
      </button>
      <button onClick={
    
    callback}>call callback</button>
    </div>
  );
}

https://codesandbox.io/s/blissful-water-230u4?file=/src/App.js

使用 useRef 时需要注意,改变用其定义的值 不会触发组件重新渲染,因此 callback 还是之前的值,必须直接执行 callbackRef.current()

export default function App() {
    
    
  const callbackRef = React.useRef(() => console.log("i am callback"));
  const callback = callbackRef.current;
  console.log(callback);
  return (
    <div className="App">
      <button
        onClick={
    
    () =>
          (callbackRef.current = () => console.log("update callback"))
        }
      >
        setCallback
      </button>
      <button onClick={
    
    () => callbackRef.current()}>call callback</button>
    </div>
  );
}

接下来使用第一种方式,外面多加一层函数来处理一下

5.完成编辑后刷新功能

编辑 src\utils\use-async.ts(外面多加一层函数):

...
export const useAsync = <D>(...) => {
    
    
  ...
  const [rerun, setRerun] = useState(() => () => {
    
    })
  ...

  const run = (promise: Promise<D>) => {
    
    
    ...
    setRerun(() => () => run(promise))
    ...
  };
  ...
};

还是不行。。。通过分析发现执行 rerunrun 中拿到的还是上一次执行的 Promise(上一次调用接口), 因此从上一次执行完的 Promise 拿数据自然还是上次的数据,由此可见需要更新 Promise(重新调用接口)

修改 src\screens\ProjectList\index.tsx(rerun 按钮取消注释):

...
export const ProjectList = () => {
    
    
  ...
  return (
    <Container>
      <h1>项目列表</h1>
      <Button onClick={
    
    rerun}>rerun</Button>
      ...
    </Container>
  );
};
...

编辑 src\utils\project.ts(单独抽离 fetchProject,执行第一次作为 run 的第一个参数,预执行包装后作为第二个参数):

...
export const useProjects = (param?: Partial<Project>) => {
    
    
  ...
  const fetchProject = () => client("projects", {
    
     data: cleanObject(param || {
    
    }) })

  useEffect(() => {
    
    
    run(fetchProject(), {
    
     rerun: fetchProject });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [param]);

  return result;
};
...

编辑 src\utils\use-async.ts(为 run 新增 runConfig):

...
export const useAsync = <D>(...) => {
    
    
  ...
  // run 来触发异步请求
  const run = (promise: Promise<D>, runConfig?: {
    
     rerun: () => Promise<D> }) => {
    
    
    ...
    setRerun(() => () => {
    
    
      if(runConfig?.rerun) {
    
    
        run(runConfig.rerun(), runConfig)
      }
    });
    ...
  };
  ...
};

虽然 定义的 runConfig 是可选参数,但是,若要下一次 rerun 可用,前一次就必须配置好预请求,因此在 setRerun 中,runConfig 是一定要加的,其他地方若是不需要这个功能,可以不加!!!

查看页面,点击按钮执行 rerun,可行了!!!

接下来完善使其编辑后自动 rerun

修改 src\screens\ProjectList\index.tsx(删掉之前测试用的按钮和日志打印,为 List 传入 refreshrerun):

...
export const ProjectList = () => {
    
    
  ...
  return (
    <Container>
      ...
      <List refresh={
    
    rerun} loading={
    
    isLoading} users={
    
    users || []} dataSource={
    
    list || []} />
    </Container>
  );
};
...

修改 src\screens\ProjectList\components\List.tsx(接收传入的传入 refresh,并在starProject的最后执行):

...
interface ListProps extends TableProps<Project> {
    
    
  users: User[];
  refresh?: () => void;
}

// type PropsType = Omit<ListProps, 'users'>
export const List = ({
     
      users, ...props }: ListProps) => {
    
    
  const {
    
     mutate } = useEditProject();
  // 函数式编程 柯里化
  const starProject = (id: number) => (star: boolean) => mutate({
    
     id, star }).then(props.refresh);
  return (...);
};

查看页面效果,完美!

下面遗留一些问题:

  • 乐观更新
    • 成功 ? 免loading : 回滚并提示
  • 想调用的方法离触发组件太远怎么办?
    • 状态提升?太复杂的时候不好用
    • 全局状态管理

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

猜你喜欢

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