对象上的打字稿 useState 挂钩不保存

Typescript useState hook on an object does not save

我无法将我的 axios return 的对象保存到我的 useState 挂钩中。 带有字符串和布尔值的常规钩子非常有效。这个没有。 axios 以正确的格式调用 return 正确的数据。

我得到了以下 useState 挂钩:

  const [user, setUser] = useState<IUser>({} as IUser);

我正在从我的 api 中获取数据,我试图将其保存到我的挂钩中。

  const getUser = async () => {
    const { data } = await axios.get('me'); // this holds the correct data {id:1,...}
    setUser(data);
    console.log(user); // setting this return nothing
  }

您的代码运行良好,但您在错误的位置记录了数据。 在 getUSer 方法上,awaitsetUser 都是异步 api 调用,但控制台是同步的,这就是它在 user 更新之前对其进行控制台的原因。 最初 user 是 {} 这就是它什么也没给出的原因。

TL;DR: react 中的状态更新是异步的。


setUser 在调用时不会直接更新 userreact 将更新 user 但它不会告诉您确切的更新时间。更新后的值很可能在下一次渲染中可用。

如果您想对 await 状态更新进行排序,大多数情况下使用 useEffect:

就足够了
useEffect(() => console.log(user), [user])

我还写了一篇关于这个主题的 blog post 文章,对它进行了深入探讨。

您可能需要考虑更通用的方法 -

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [result, setResult] = useState(null)
  const [error, setError] = useState(null)

  useEffect(_ => { 
    Promise.reolve(runAsync(...deps))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, deps)

  return { loading, result, error }
}

在组件中使用 useAsync 看起来像这样 -

const MyComponent = ({ userId = 0 }) => {
  const { loading, error, result } =
    useAsync(UserApi.getById, [userId])

  if (loading)
    return <pre>loading...</pre>

  if (error)
    return <pre>error: {error.message}</pre>

  return <pre>result: {result}</pre>
}

如果你有很多组件需要查询一个用户,你可以相应地特化useAsync -

const useUser = (id = 0) =>
  userAsync(UserApi.getById, [id])

const MyComponent = ({ userId = 0 }) => {
  const { loading, error, result:user } =
    useUser(userId)

  if (loading)
    return <pre>loading...</pre>

  if (error)
    return <pre>error: {error.message}</pre>

  return <pre>user: {user}</pre>
}

这是一个代码片段,您可以 运行 在自己的浏览器中验证结果 -

const { useState, useEffect } =
  React

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [result, setResult] = useState(null)
  const [error, setError] = useState(null)

  useEffect(_ => { 
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, deps)

  return { loading, result, error }
}

const _fetch = (url = "") =>
  fetch(url).then(x => new Promise(r => setTimeout(r, 2000, x)))

const UserApi = {
  getById: (id = 0) =>
    id > 500
      ? Promise.reject(Error(`unable to retrieve user: ${id}`))
      : _fetch(`https://httpbin.org/get?userId=${id}`).then(res => res.json())
}
  
const useUser = (userId = 0) =>
  useAsync(UserApi.getById, [userId])

const MyComponent = ({ userId = 0 }) => {
  const { loading, error, result:user } =
    useUser(userId)

  if (loading)
    return <pre>loading...</pre>
  if (error)
    return <pre style={{color:"tomato"}}>error: {error.message}</pre>
  return <pre>result: {JSON.stringify(user, null, 2)}</pre>
}

const MyApp = () =>
  <main>
    <MyComponent userId={123} />
    <MyComponent userId={999} />
  </main>

ReactDOM.render(<MyApp />, document.body)
pre {
  background: ghostwhite;
  padding: 1rem;
  white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.js"></script>


真正的定制

当然,这只是您可以设计的一种方式 useAsync。您如何设计自定义挂钩会极大地改变它们的使用方式 -

const MyComponent = ({ userId = 0 }) =>
  useUser(userId, {
    loading: _ => <pre>loading...</pre>,
    error: e => <pre style={{color:"tomato"}}>error: {e.message}</pre>,
    result: user => <pre>result: {JSON.stringify(user, null, 2)}</pre>
  })

这样的自定义钩子可以像这样实现 -

const identity = x => x

const defaultVariants =
  { loading: identity, error: identity, result: identity }

const useAsync = (runAsync = identity, deps = [], vars = defaultVariants) => {
  const [{ tag, data }, update] =
    useState({ tag: "loading", data: null })

  const setResult = data =>
    update({ tag: "result", data })

  const setError = data =>
    update({ tag: "error", data })

  useEffect(_ => {
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
  }, deps)

  return vars[tag](data)
}

并更新useUser传递cata

const useUser = (userId = 0, vars) =>
  useAsync(UserApi.getById, [userId], vars)

通过运行下面的代码片段验证结果-

const { useState, useEffect } =
  React

const identity = x => x

const defaultVariants =
  { loading: identity, error: identity, result: identity }

const useAsync = (runAsync = identity, deps = [], vars = defaultVariants) => {
  const [{ tag, data }, update] =
    useState({ tag: "loading", data: null })
    
  const setResult = data =>
    update({ tag: "result", data })

  const setError = data =>
    update({ tag: "error", data })

  useEffect(_ => {
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
  }, deps)

  return vars[tag](data)
}

const _fetch = (url = "") =>
  fetch(url).then(x => new Promise(r => setTimeout(r, 2000, x)))

const UserApi = {
  getById: (id = 0) =>
    id > 500
      ? Promise.reject(Error(`unable to retrieve user: ${id}`))
      : _fetch(`https://httpbin.org/get?userId=${id}`).then(res => res.json())
}
  
const useUser = (userId = 0, vars) =>
  useAsync(UserApi.getById, [userId], vars)

const MyComponent = ({ userId = 0 }) =>
  useUser(userId, {
    loading: _ => <pre>loading...</pre>,
    error: e => <pre style={{color:"tomato"}}>error: {e.message}</pre>,
    result: user => <pre>result: {JSON.stringify(user, null, 2)}</pre>
  })

const MyApp = () =>
  <main>
    <MyComponent userId={123} />
    <MyComponent userId={999} />
  </main>

ReactDOM.render(<MyApp />, document.body)
pre {
  background: ghostwhite;
  padding: 1rem;
  white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.js"></script>