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

js 复制代码
...
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 中尝试调用,使用前先打印一下:

js 复制代码
...
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:

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

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

codesandbox 上测试一下:

js 复制代码
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 的函数签名:

js 复制代码
/**
 * 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

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

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

js 复制代码
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:

js 复制代码
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()

js 复制代码
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(外面多加一层函数):

js 复制代码
...
export const useAsync = <D>(...) => {
  ...
  const [rerun, setRerun] = useState(() => () => {})
  ...

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

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

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

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

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

js 复制代码
...
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):

js 复制代码
...
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 传入 refresh:rerun):

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

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

js 复制代码
...
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 : 回滚并提示
  • 想调用的方法离触发组件太远怎么办?
    • 状态提升?太复杂的时候不好用
    • 全局状态管理

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

相关推荐
wangruofeng2 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng2 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈5 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技5 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569155 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马7 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng8 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding9 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天9 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo11 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构