Featured image of post react学习笔记

react学习笔记

react 基础

state同步的注意事项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  import { useState } from 'react';
  
  export default function Clock(props) {
    const [color, setColor] = useState(props.color);
    return (
      <h1 style={{ color: color }}>
        {props.time}
      </h1>
    );
  }

React 状态初始化机制:

useState(props.color) 仅在组件 首次渲染时 使用 props.color 的初始值 后续父组件传入的 props.color 变化时,这个初始化代码不会再执行, 造成了color和props的不同步.

可以简单的改为:

1
2
3
4
5
6
7
8
9
import { useState } from 'react';

export default function Clock(props) {
  return (
    <h1 style={{ color: props.color }}>
      {props.time}
    </h1>
  );
}

不应该把组件函数的定义嵌套起来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>点击了 {counter} </button>
    </>
  );
}

每次点击按钮,输入框的 state 都会消失!这是因为每次 MyComponent 渲染时都会创建一个 不同 的 MyTextField 函数。 在相同位置渲染的是不同的组件,所以 React 将其下所有的 state 都重置了。

不要在useEffect中改变state

1
2
3
4
const [count, setCount] = useState(0);
useEffect(() => {
  setCount(count + 1);
});

默认情况下, useEffect会在组件选然后执行, 所以会无限循环。

没看懂

https://zh-hans.react.dev/learn/manipulating-the-dom-with-refs 深入探讨 如何使用 ref 回调管理 ref 列表

react query

我们看一个react query的最小实现:

最小实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { QueryClient, useQuery } from '@tanstack/react-query'
import './index.css'

function App() {
  const { data, isPending, status } = useQuery({
    queryKey: ['dishes'],
    queryFn: async () => {
      return fetch(
        'http://localhost:8080/admin/dish/page?categoryId=&name=&page=1&pageSize=10&status=1'
      ).then(res => {
        if (!res.ok) {
          throw new Error(`请求错误:${res.status}`)
        }
        return res.json()
      })
    }
  })
  if (isPending) return <div>加载中...</div>
  if (status === 'error') return <div>请求错误</div>
  return <div>{JSON.stringify(data)}</div>
}

export default App

在这个最小实现中, react query实际包装和抽象了一个通用服务器请求的流程, 这个通用的流程是"创建useState, 发起请求并判断请求的状态(isPending, isError等). 如果请求成功, 将请求存入缓存(useState), 并附上一个 时间戳, 之后在这个queryKey对应的请求数据被用到时(组件重新挂载,窗口聚焦等时期)判断缓存是否过期, 如果过期则重新请求并更新缓存; 如果请求失败, 则隔一段时间继续请求;"

如果我们不用react query, 那么这个组件的实现应该是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { useState, useEffect, useRef } from 'react'
import './index.css'

// 缓存配置
const CACHE_EXPIRY_TIME = 5 * 60 * 1000 // 5分钟缓存有效期

function App() {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  //数据缓存
  const [cache, setCache] = useState({
    data: null,
    timestamp: 0
  })

  // 重试次数计数器
  const maxRetries = 3
  const retryRemainRef = useRef(maxRetries)

  // @ts-ignore
  async function queryFn() {
    const response = await fetch(
      'http://localhost:8080/admin/dish/page?categoryId=&name=&page=1&pageSize=10&status=1'
    )

    if (!response.ok) {
      throw new Error(`请求失败,状态码:${response.status}`)
    }
    const jsonData = await response.json()

    // 更新缓存. 但实际上reactQuery会在四种情况下更新缓存:
    //1. queryKey发生改变.
    //2. 拥有这个queryKey的组件挂载时
    //3. 窗口重新聚焦
    //4. 网络重新连接
    //这里只模拟了第二种情况.
    //同时, 在reactQuery里面, 可以通过设置refetchOnMount, refetchOnWindowFocus,
    //refetchOnReconnect来让后三种情况不触发缓存更新.
    setCache({
      data: jsonData.data?.records,
      timestamp: Date.now()
    })

    retryRemainRef.current = 0 // 将remain的retry值设置为0, 表示已成功, 不需要接着重试了.
  }

  // @ts-ignore
  async function query(): Promise<void> {
    setIsLoading(true)
    setError(null)

    try {
      await queryFn()
    } catch (err) {
      setError(err.message)

      // 重试, 重试时间间指数递增
      if (retryRemainRef.current >= 0) {
        const delay = Math.min(
          1000 * 2 ** (maxRetries - retryRemainRef.current),
          30000
        )
        setTimeout(() => {
          retryRemainRef.current--
          queryFn()
        }, delay)
      }
    } finally {
      setIsLoading(false)
    }
  }

  useEffect(() => {
    const now = Date.now()

    // 在缓存不存在的情况时进行查询
    // 检查缓存有效性, 模拟staleTime的机制.
    if (!cache.data || now - cache.timestamp > CACHE_EXPIRY_TIME) {
      query()
    }
  }, []) // 空依赖表示只在组件挂载时执行

  if (isLoading) return <div>加载中...</div>
  if (error) return <div>错误{error}</div>

  return <div>{JSON.stringify(cache.data?.slice(0, 10))}</div>
}

export default App

显然, react query的实现更简洁, 更易读, 更易维护, 更易扩展(我想谁也不愿意在js里面面对一大堆useState和useEffect). 而且上面不用reactQuery的这一段代码都是复用性很高的, 因此就算不用react query, 我们也应该把这一段逻辑抽象封装起来.

同时, 由于react query的key机制, react query用起来就像一个键值对数据库. 围绕于此, react也提供了很多实用的机制: 比如减少重复的请求(在缓存中有对应key的数据时, 不会发起请求), 垃圾回收(在一条查询缓存过久(超过gcTime)没有被组件使用后, 会被自动删除),

实际使用例子

使用 Hugo 构建
主题 StackJimmy 设计