【Practice】VIII. User selector and project editing function (Part 2) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level project (15)


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 rerunmethod, save the last runrunning 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 rerunthe runprevious 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 rerunit will be executed directly when assigning a value, that is, useStatethe 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 initialStatethat is a joint type and Sis a commonly used form. () => SWhy 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 setCallbackit 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 callbackis 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 runthat the execution is still the last execution Promise(the last call to the interface), so the data obtained from the last execution Promiseis naturally the last data, which shows that it needs to be updated Promise(recall the interface)

EDIT src\screens\ProjectList\index.tsx( rerunbutton 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 runthe 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 runConfigis an optional parameter, if you want to be available next time rerun, you must configure the pre-request in the previous time, so setRerunit runConfigmust 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 Listin 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 starProjectexecutes 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. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/131931899