epicrato
epicrato

Reputation: 8408

useState on React Hooks not updating Array

I have tried many things and can't seem to understand why setTypes won't update the 'types' array??

import { useState, useEffect } from 'react';
import { PostList } from './post-list';
import * as api from '../utils/api';

export const PostSelector = (props) => {
  const [posts, setPosts]     = useState([]);
  const [loading, setLoading] = useState(false);
  const [type, setType]       = useState('post');
  const [types, setTypes]     = useState([]);
  
  const fetchTypes = async () => {
    setLoading(true);
    const response = await api.getPostTypes();
    delete response.data.attachment;
    delete response.data.wp_block;
    const postTypes = response.data;
    console.log(response.data); // {post: {…}, page: {…}, case: {…}}
    setTypes(postTypes);
    console.log(types); // []

    // Why types remain empty??
  }

const loadPosts = async (args = {}) => {
  const defaultArgs = { per_page: 10, type };
  const requestArgs = { ...defaultArgs, ...args };
  

  requestArgs.restBase = types[requestArgs.type].rest_base; // Cannot read property 'rest_base' of undefined
  
  const response = await api.getPosts(requestArgs);
  console.log(response.data);
}

useEffect(() => {
  fetchTypes();
  loadPosts();
}, []);

  return (
    <div className="filter">
      <label htmlFor="options">Post Type: </label>
      <select name="options" id="options">
        { types.length < 1 ? (<option value="">loading</option>) : Object.keys(types).map((key, index) => <option key={ index } value={ key }>{ types[key].name }</option> ) }
      </select>
    </div>
  );
}

Please, take a look at the console.log and notice the different responses.

What I am trying to do is to load list of types, in this case 'post', 'page' and 'case' and then render a list of posts based on the current 'type'. The default type is 'post'.

If I add [types] to useEffect. I finally get the values but the component renders nonstop.

Thanks to everyone for your comments. Multiple people have pointed out the problem, being that, the fact that we set the state doesn't mean it will set right away because it it asynchronous.

How do we solve this problem then? Regardless of the reasons, how do we get it done? How do we work with our state at any point in time and perform calculations based on our state if we don't know when it will become available? How do we make sure we wait whatever we need to and then use the values we expect?

Upvotes: 1

Views: 11090

Answers (5)

72GM
72GM

Reputation: 3114

For any one coming here and not being able to set/update a useState array you need to use a spread operator (...) and not just the array e.g. "[...initState]" instead of "initState" ... in Typescript

 //initialise
  const initState: boolean[] = new Array(data.length).fill(false);
  const [showTable, setShowTable] = useState<boolean[]>([...initState]);

  // called from an onclick to update
  const updateArray = (index: number) => {
    showTable[index] = !showTable[index];
    setShowTable([...showTable]);
  };

Upvotes: 1

Nguyễn Văn Phong
Nguyễn Văn Phong

Reputation: 14198

Ok, The short answer is due to Closures

It not due to asynchronous as other answers said !!!


Solution (☞゚ヮ゚)☞

You can check the changes by console.log at return function like this.

return (
    <div> Hello World! 
      {
        console.log(value) // this will reference every re-render
      } 
    </div>
  );

or create a new one useEffect with value as a dependency like below

 React.useEffect(() => {
    console.log(value); // this will reference every value is changed
  }, [value]);

function App() {
  const [value, Setvalue] = React.useState([]);
  
  React.useEffect(() => {
    Setvalue([1, 2, 3]);
    console.log(value); // this will reference to value at first time
  }, []);
  
  return (
    <div> Hello World! 
      {
        console.log(value) // this will reference every re-render
      } 
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

<div id="app"></div>

Read here in more detail: useState set method not reflecting change immediately

Upvotes: 0

PurpSchurp
PurpSchurp

Reputation: 625

You have declared your types to be an array, yet you are passing a dictionary of dictionaries through to it. Try this:

const [types, setTypes]     = useState({});

You also do not need to call

loadPosts()

becuase the useState hook will re-render your component, only updating what is needed.

Upvotes: 0

Muhammad Abdullah
Muhammad Abdullah

Reputation: 29

useState's setTypes is an asynchronous function so the changes do not take effect immediately. You can use useEffect to check if anything changes

useEffect(()=>{
    const defaultArgs = { per_page: 10, type };
    const requestArgs = { ...defaultArgs, ...args };
    requestArgs.restBase = types;
    console.log("types updated",types)
},[types])

You can remove loadPosts because now useEffect will run whenever types change

Upvotes: 0

nicoavn
nicoavn

Reputation: 73

It seems like useState is asynchronous and does not update the value instantly after calling it.

Review this same case here

Upvotes: 0

Related Questions