Article Directory
-
- 1. Project launch: project initialization and configuration
- 2. React and Hook application: implement the project list
- 3. TS Application: JS God Assist - Strong Type
- 4. JWT, user authentication and asynchronous request
- 5. CSS is actually very simple - add styles with CSS-in-JS
- 6. User experience optimization - loading and error state handling
- 7. Hook, routing, and URL state management
- 8. User selector and item editing function
Source of learning content: React + React Hook + TS Best Practice - MOOC
Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):
item | Version |
---|---|
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 |
The specific configuration, operation and content will be different, and the "pit" will also be different. . .
1. Project launch: project initialization and configuration
2. React and Hook application: implement the project list
3. TS Application: JS God Assist - Strong Type
4. JWT, user authentication and asynchronous request
5. CSS is actually very simple - add styles with CSS-in-JS
6. User experience optimization - loading and error state handling
7. Hook, routing, and URL state management
8. User selector and item editing function
1~3
4. Refresh after editing - lazy initialization of useState and save function state
The previous legacy problems are now trying to solve
Modification src\utils\use-async.ts
(new rerun
method, save the last run
running state):
...
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,
};
};
Compared with defining variables directly, variables defined through useState will maintain the previous state when the component is refreshed, unless the State is reset, while direct definition will reinitialize
Try to call in src\screens\ProjectList\index.tsx
, print it before use:
...
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>
);
};
...
...with an error:Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Try to print in rerun
the run
previous step, edit src\utils\use-async.ts
:
...
export const useAsync = <D>(...) => {
...
const run = (promise: Promise<D>) => {
...
setRerun(() => {
console.log('set rerun')
run(promise)
})
...
};
...
};
Keep printing "set rerun", but it is not running at this time rerun
, so there is reason to suspect that rerun
it will be executed directly when assigning a value, that is, useState
the function cannot be saved directly
Test it on 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>
);
}
Sure enough, not only at the time of assignment, but also directly at the time of initialization
Look at the function signature of 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>>];
It can be noticed initialState
that is a joint type and S
is a commonly used form. () => S
Why should it be listed separately?
You can view the official documentation: Lazy initial state | Hook API index – React
It can be known from the documentation that this initialization method is only executed once, and it is used when the initialization value requires complex calculations to obtain (expensive calculations, consuming performance)
That being the case, why not add another layer of functions outside? try it:
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>
);
}
Sure enough, it can be called directly after initialization callback
, and setCallback
it is another function to call after
In addition to this way, you can also use 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
Note when using useRef
, changing the value defined by it will not trigger the component to re-render, so callback
is still the previous value and must be executed directlycallbackRef.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>
);
}
Next, use the first method, add an extra layer of function to deal with it
5. Refresh function after editing
EDIT src\utils\use-async.ts
(adding an extra layer of functions outside):
...
export const useAsync = <D>(...) => {
...
const [rerun, setRerun] = useState(() => () => {
})
...
const run = (promise: Promise<D>) => {
...
setRerun(() => () => run(promise))
...
};
...
};
Or not. . . Through analysis rerun
, it is found run
that the execution is still the last execution Promise
(the last call to the interface), so the data obtained from the last execution Promise
is naturally the last data, which shows that it needs to be updated Promise
(recall the interface)
EDIT src\screens\ProjectList\index.tsx
( rerun
button uncommented):
...
export const ProjectList = () => {
...
return (
<Container>
<h1>项目列表</h1>
<Button onClick={
rerun}>rerun</Button>
...
</Container>
);
};
...
Edit src\utils\project.ts
(extracted separately fetchProject
, the first execution is used as run
the first parameter, and the pre-executed packaging is used as the second parameter):
...
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;
};
...
EDIT src\utils\use-async.ts
(Added runConfig for run):
...
export const useAsync = <D>(...) => {
...
// run 来触发异步请求
const run = (promise: Promise<D>, runConfig?: {
rerun: () => Promise<D> }) => {
...
setRerun(() => () => {
if(runConfig?.rerun) {
run(runConfig.rerun(), runConfig)
}
});
...
};
...
};
Although defined
runConfig
is an optional parameter, if you want to be available next timererun
, you must configure the pre-request in the previous time, sosetRerun
itrunConfig
must be added in , and if you don’t need this function in other places, you don’t need to add it! ! !
View the page, click the button to execute rerun
, it works! ! !
Next refine to make it automatic after editingrerun
Modification src\screens\ProjectList\index.tsx
(delete the button and log printing used in the previous test, pass List
in refresh
: rerun
):
...
export const ProjectList = () => {
...
return (
<Container>
...
<List refresh={
rerun} loading={
isLoading} users={
users || []} dataSource={
list || []} />
</Container>
);
};
...
Modification src\screens\ProjectList\components\List.tsx
(receives incoming incoming refresh
, and starProject
executes at the end of):
...
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 (...);
};
Check the page effect, perfect!
Here are some remaining questions:
- optimistic update
- Success? No loading: rollback and prompt
- What should I do if the method I want to call is too far away from the triggering component?
- Status improvement? Not useful when it's too complicated
- global state management
Some reference notes are still in draft stage, so stay tuned. . .