文章目录
-
- 一、项目起航:项目初始化与配置
- [二、React 与 Hook 应用:实现项目列表](#二、React 与 Hook 应用:实现项目列表)
- [三、TS 应用:JS神助攻 - 强类型](#三、TS 应用:JS神助攻 - 强类型)
- 四、JWT、用户认证与异步请求
- [五、CSS 其实很简单 - 用 CSS-in-JS 添加样式](#五、CSS 其实很简单 - 用 CSS-in-JS 添加样式)
- [六、用户体验优化 - 加载中和错误状态处理](#六、用户体验优化 - 加载中和错误状态处理)
- [七、Hook,路由,与 URL 状态管理](#七、Hook,路由,与 URL 状态管理)
- 八、用户选择器与项目编辑功能
相对原教程,我在学习开始时(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 : 回滚并提示
- 想调用的方法离触发组件太远怎么办?
- 状态提升?太复杂的时候不好用
- 全局状态管理
部分引用笔记还在草稿阶段,敬请期待。。。