useState on React Hooks not updating Array - javascript

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?

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

It seems like useState is asynchronous and does not update the value instantly after calling it.
Review this same case here

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

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.

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

Related

Why is simple useState initialization is not working?

I have this React code:
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [players, setPlayers] = useState([]);
// Get all Players
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
const [playerCount, setPlayerCount] = useState(players.length);
return (
<div>
<p>{`This is how many there are: ${playerCount}`}</p>
</div>
);
}
export default App;
I want to print how many initial players using playerCount variable. However it says it's zero:
This is how many there are: 0
If I instead print players.length, it would output the correct number:
<p>{`This is how many there are: ${players.length}`}</p>
This is how many there are: 9
Even if I remove dependency array to keep rendering, playerCount still wont update:
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
});
I wonder why the useState is not working? Is there something I am missing in my code?
A good rule of thumb with state (and props) is to avoid duplicating state values when a value can be determined entirely by another. Otherwise, you can run into issues like these, where keeping multiple states in sync can be more challenging than it needs to be.
Here, you set the initial value of playerCount when the component mounts:
const [playerCount, setPlayerCount] = useState(players.length);
And the component mounts only once - and at that time, players is the empty array - so playerCount becomes 0, and because you never call setPlayerCount, it always remains 0.
While you could fix it by calling setPlayerCount inside your .then, a better approach would be to either calculate the player count from the players state only when needed:
function App() {
const [players, setPlayers] = useState([]);
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
return (
<div>
<p>{`This is how many there are: ${players.length}`}</p>
</div>
);
}
Or, if you really had to, to memoize the count depending on the players array (without creating additional state).
function App() {
const [players, setPlayers] = useState([]);
const playerCount = useMemo(() => players.length, [players]);
const getAllPlayersUrl = "http://localhost:5087/api/GetAllPlayers";
useEffect(() => {
axios.get(getAllPlayersUrl).then((response) => {
setPlayers(response.data);
});
}, []);
return (
<div>
<p>{`This is how many there are: ${playerCount}`}</p>
</div>
);
}

Issues upgrading async componentDidMount() to async useEffect()

// UPDATE: The issue was using the state immediately after setting it inside useEffect(). See my answer HERE for details.
I'm trying to upgrade one of my React app pages from class component to functional component with Hooks. However, I have some issues due to some async functions.
The way the old page behaves is that in componentDidMount() some data is async fetched from the database and displayed. It works properly, myName and myValue are displayed correctly.
// OLD APPROACH - CLASS COMPONENT
class MyPage extends Component {
constructor(props) {
super(props);
this.state = {
myName: null,
myValue: undefined,
}
}
componentDidMount = async () => {
try {
const myName = await getNameFromDatabase();
const myValue = await getValueFromDatabase();
this.setState({ myName, myValue });
} catch (error) {
alert(
"Some errors occured when fetching from DB"
);
console.error(error);
}
}
render() {
return (
<div>
<h1>{this.state.myName}</h1>
<h1>{this.state.myValue}</h1>
</div>
)
}
export default MyPage
I tried to update the page by carefully following this response.
// NEW APPROACH - FUNCTIONAL COMPONENT WITH HOOKS
function MyPage() {
const [myName, setMyName] = useState(null);
const [myValue, setMyValue] = useState(undefined);
useEffect(() => {
async function fetchFromDatabase() {
const myName = await getNameFromDatabase();
const myValue = await getValueFromDatabase();
setMyName(myName);
setMyValue(myValue);
}
fetchFromDatabase();
}, [])
return (
<div>
<h1>{myName}</h1>
<h1>{myValue}</h1>
</div>
)
}
However, when I do this, they no longer get displayed. I supposed they remain "null" and "undefined". Apparently if I do a console.log(), they eventually get fetched, but only after the page is rendered without them, which is not what was happening in the first case.
Why exactly is this happening? Why is it getting displayed correctly in the first case but not in the second? As far as I know, useEffect() does the same thing as componentDidMount(). Should I proceed another way if I wish to call async functions inside useEffect()?
The useEffect hook and state updates are fine. Function components are instanceless though, so the this is just undefined. Fix the render to just reference the state values directly.
It's also good practice to handle errors when working with asynchronous code.
function MyPage() {
const [myName, setMyName] = useState(null);
const [myValue, setMyValue] = useState(undefined);
useEffect(() => {
async function fetchFromDatabase() {
try {
const myName = await getNameFromDatabase();
const myValue = await getValueFromDatabase();
setMyName(myName);
setMyValue(myValue);
} catch(error) {
// handle any rejected Promises and thrown errors
}
}
fetchFromDatabase();
}, []);
return (
<div>
<h1>{myName}</h1>
<h1>{myValue}</h1>
</div>
);
}
First of all, you are giving the same name for your response as your useState(). Try using different names. Then, put just empty string into your useState() default value instead of null or undefined. Finally, you no longer need to use this but instead access directly the value. It should be something like this :
function MyPage() {
const [myName, setMyName] = useState('');
const [myValue, setMyValue] = useState('');
useEffect(() => {
async function fetchFromDatabase() {
const name = await getNameFromDatabase();
const value = await getValueFromDatabase();
setMyName(name);
setMyValue(value);
}
fetchFromDatabase();
}, [])
return (
<div>
<h1>{myName}</h1>
<h1>{myValue}</h1>
</div>
)
}
function MyPage() {
const [myName, setMyName] = useState(null);
const [myValue, setMyValue] = useState(undefined);
useEffect(() => {
(async () => {
const myName = await getNameFromDatabase();
const myValue = await getValueFromDatabase();
setMyName(myName);
setMyValue(myValue);
})();
}, []);
return (
<div>
<h1>{myName}</h1>
<h1>{myValue}</h1>
</div>
);
}
Alright, so the code in the original post is correct, as other remarked. However, it is a very simplified/abstract version of the actual code I'm working on.
What I was doing wrong is that I was using the state in useEffect() immediately after setting it there.
Something like that:
// WRONG
let fetchedName= getNameFromDatabase();
setMyName(fetchedName);
if(myName==="something") {
setMyValue(1000);
}
The conclusion is: Never use the state immediately after setting it in useEffect() or componentWillMount(), use an intermediary variable.
Instead do:
// CORRECT
let fetchedName= getNameFromDatabase();
setMyName(fetchedName);
if(fetchedName==="something") {
setMyValue(1000);
}

Rendering async promises through function components

Sorry if the post is duplicated i just find examples for class components.
I have this code:
export const getUniPrice = async () => {
const pair = await Uniswap.Fetcher.fetchPairData(HOKK, Uniswap.WETH[ETH_CHAIN_ID]);
const route = new Uniswap.Route([pair], Uniswap.WETH[ETH_CHAIN_ID]);
const priceUni = route.midPrice.toFixed(9);
return priceUni
}
It does work, answer me the promise object:
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: "1106278.001628948"
What i would like to know is, how can i properly work with this object in order to be able to render it through function components? I'm doing something like this which obviously will not work because react doesn't render objects.
const Price = () => {
const { state, dispatch } = useContext(AppContext);
return(<>
{state.dex === 'uni' ? getUniPrice() : state.dex === 'cake'
? getCakePrice() : getMDexPrice()
}
</>)
}
Could someone give me a hint? This function is running outside a function component so I can't just use useState
You're right. The result of getUnitPrice() is a Promise, not a value, so what React does is it prints out the stringified version of that Promise. If you need the fulfilled value, you need a state value that will re-render the page if updated. Something like this:
const [price, setPrice] = useState('');
useEffect(() => {
getUnitPrice().then((p) => setPrice(p));
}, []);
...
<div>Price: {price}</div>
If you're using a class component, you can initialize the state the same way like this:
state = {
price: '',
}
async componentDidMount() {
const p = await getUniPrice();
this.setState({ price: p });
}

How to set a state value to an array in react native?

I have a functional component, ListKeys. When it loads I want to set an empty array of keys equal to a list of all keys extracted from storage. Here is what I've got at the moment:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if(!areKeysLoaded){
loadSavedKeys();
setAreKeysLoaded(true);
console.log(keys)
}
});
async function loadSavedKeys(){
try {
var allKeys = await AsyncStorage.getAllKeys();
console.log(allKeys);
setKeys(allKeys);
}
catch {
console.log("Error: Cannot access saved data.");
}
}
return (
<View></View>
);
};
export default ListKeys;
This code correctly gets the list of keys and outputs it to the console. This is done on line 16: console.log(allKeys);
However, when I then setKeys(allKeys);, this doesn't work. I know this because line 9: console.log(keys) outputs an empty array.
I'm guessing I can't just set a state value array to another array but I'm not experienced enough with JS or React Native to know why.
Can someone tell me how to properly set the keys array to the allKeys array?
You need to wait for the loadSavedKeys to resolve first before trying to set since it is async:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if (areKeysLoaded) return
AsyncStorage
.getAllKeys()
.then((keys) => {
setAreKeysLoaded(true)
setKeys(keys)
})
.catch(e => console.error(e));
}, []);
return (
<View></View>
);
};
export default ListKeys;
Note I simplified the code a bit since it seemed a little more verbose then necessary. They key take away is you need to wait for getAllKeys to resolve before you'll get the keys since it is asynchronous.
Also, you prob don't need areKeysLoaded if you just want this to run once, but I guess you could use it instead for a loading indicator?

With a React Hooks' setter, how can I set data before the component renders?

I export a JS object called Products to this file, just to replace a real API call initially while I am building/testing. I want to set the function's state to the object, but mapped. I have the component looking like this:
function App() {
const [rooms, setRooms] = useState([]);
const [days, setDays] = useState([]);
const roomsMapped = products.data.map(room => ({
id: room.id,
title: room.title
}))
useEffect(() => {
setRooms(roomsMapped);
})
return ( etc )
This returns the following error: Error: Maximum update depth exceeded.
I feel like I'm missing something really obvious here, but am pretty new to React and Hooks. How can I set this data before the component renders?
Just declare it as initial value of rooms
const Component = () =>{
const [rooms, setRooms] = useState(products.data.map(room => ({
id: room.id,
title: room.title
})))
}
You can also use lazy initial state to avoid reprocessing the initial value on each render
const Component = () =>{
const [rooms, setRooms] = useState(() => products.data.map(room => ({
id: room.id,
title: room.title
})))
}
Change useEffect to this
useEffect(() => {
setRooms(roomsMapped);
},[])
With Lazy initialisation with function as a parameter of useState
import React, { useState } from "react";
function App() {
const [rooms, setRooms] = useState(() => {
// May be a long computation initialization
const data = products.data || [];
return data.map(({ id, title }) => ({ id, title }));
});
return (
// JSX stuffs
)
}
You can use default props for this.set initial value with empty list .
You are getting 'Error: Maximum update depth exceeded', because your useEffect function doesn't have dependency array. Best way to fix this is to pass empty array as the second argument to useEffect like this:
useEffect(() => {
setRooms(roomsMapped);
},[]) <= pass empty array here
this will prevent component to re render, it you want your component to re render on props change you can pass the props in the array like this:
useEffect(() => {
setRooms(roomsMapped);
},[props.props1,props.props2])
here you can pass as many props as you want...

Categories