1

I am not able to save the object which my axios return to my useState hook. Regular hook with strings and boolean works perfectly. This one does not. The axios call return the correct data in correct format.

I got following useState hook:

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

And I am fetching data from my api which I try to save to my hook.

  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
  }

3 Answers 3

2

TL;DR: State updates in react are asynchronous.


setUser is not directly updating user when it's called. react will update user but it will not tell you when exactly this is going to happen. The updated value will most likely be available in the next render.

If you want to sort of await a state update it's most of the time enough to use useEffect:

useEffect(() => console.log(user), [user])

I also wrote a blog post about this topic which is kind of a deep-dive into it.

Sign up to request clarification or add additional context in comments.

Comments

1

You might want to consider a more generic approach -

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 }
}

Using useAsync in a component looks like this -

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>
}

If you have many components which need to query a user, you can specialize useAsync accordingly -

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>
}

Here's a code snippet you can run to verify the results in your own browser -

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>


truly custom

Of course that's just one way you can design useAsync. How you design your custom hooks can wildly change how they are used -

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>
  })

Such a custom hook could be implemented like this -

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)
}

And useUser is updated to pass along cata

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

Verify the results by running the snippet below -

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>

Comments

1

Your code works perfectly fine but you are logging the data at wrong place. On getUSer method both await and setUser are async api call but console is synchronous, that's why it console the user before it gets updated. Initially the user is {} that's why it gives nothing.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.