Async run function after useEffect - javascript

I was wondering how it is possible to run a function after you use the useEffect to fetch data, where the function is manipulating the data after its been pulled?
import React, { useState, useEffect } from 'react';
const Result = (props) => {
const [ playerName, setPlayerName ] = useState('');
const [ playerChoice, setPlayerChoice ] = useState(null);
const [ computerChoice, setComputerChoice ] = useState(null);
const [ result, setResult ] = useState(null);
useEffect(() => {
setPlayerName(props.location.state.playerName);
setPlayerChoice(props.location.state.playerChoice);
setComputerChoice(generateComputerChoice);
setResult(getResult())
}, []);
const getResult = () => {
// code that runs after the setting of the playerName and playerChoice. Will return "Win", "Lose", or "Draw"
};
const generateComputerChoice = () => {
const outcomes = [ 'Rock', 'Paper', 'Scissors' ];
return outcomes[Math.floor(Math.random() * outcomes.length)];
};
return (
<div className="app-container">
<strong>YOU {result}</strong>
<br />
<strong>{playerName}</strong> chose <strong>{playerChoice}</strong>
<br />
<strong>Computer</strong> chose <strong>{computerChoice}</strong>
</div>
);
};
export default Result;
So here in this example I grab the playerName and playerChoice from a previous page, and then add that to my useState on page load.
After that I randomly generate the computerChoice.
However, after that I want to use the playerChoice amd computerChoice that has been added to the state and use that to see if the game is a win, lose or draw.
result ends up being null because I assume that by the time the getResult function is called, the states haven't been set yet.
Do you guys know what should be done in this situation? Seems like this could be a common thing considering you might wanna grab data from an API and then do something with that data before wanting to render it.

That first effect is unneccessary. Just do
const [playerName, setPlayerName] = useState(props.location.state.playerName);

Use the useMemo hook and add the state variables to its dependency array. It will memoize the result for each render cycle so it is only ever computed when playerName or playerChoice update.
const getResult = useMemo(() => {
// code that runs after the setting of the playerName and playerChoice. Will return "Win", "Lose", or "Draw"
}, [playerName, playerChoice]);
Oops, I see now you're trying to save this to the result state variable, so you can either use a second useEffect with the same dependencies instead of the useMemo I suggested, or in your original snippet instead of calling a getResult() function you change the signature to getResult(name, choice) and call setResult with the current render cycle values (right from the props).
useEffect(() => {
const { playerName, playerChoice } = props.location.state;
setPlayerName(playerName);
setPlayerChoice(playerChoice);
setComputerChoice(generateComputerChoice);
setResult(getResult(playerName, playerChoice));
}, []);
const getResult = (name, choice) => {
// Will return "Win", "Lose", or "Draw"
};

setState is asynchronous and you'll be able to use the state only in the next render. Unlike in class components, hooks doesn't allow a callback after setting the state.
But looking at your component and assuming that is what the functionality of that would be, there's no reason for you to add playerName and playerChoice to the state of the component. You can use the data from the props itself.
import React, { useState, useEffect } from 'react';
const Result = (props) => {
const {playerName, playerChoice} = props.location.state;
const [ result, setResult ] = useState(null);
const [computerChoice, setComputerChoice] = useState(null);
useEffect(() => {
setComputerChoice(generateComputerChoice());
getResult();
}, []);
const getResult = () => {
// You can get the playerName, playerChoice from the props.
// You can also setResult here.
};
const generateComputerChoice = () => {
const outcomes = [ 'Rock', 'Paper', 'Scissors' ];
return outcomes[Math.floor(Math.random() * outcomes.length)];
};
return (
<div className="app-container">
<strong>YOU {result}</strong>
<br />
<strong>{playerName}</strong> chose <strong>{playerChoice}</strong>
<br />
<strong>Computer</strong> chose <strong>{computerChoice}</strong>
</div>
);
};
export default Result;
Hope this helps.

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

React custom country select hook

I am trying to create a reusable country select functionality by utilizing React Hooks. The idea behind the scenes is to create a function which would handle the country change within a custom select box. With this in mind, I have attempted the following code, which results in an endless loop. Is there any alternative to this code, or any ideas on how to implement it?
const { useState } = require("react");
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState([]);
const filteredCountries = defaultCountires.filter(
(country) => country !== defaultCountry
);
setCountries(filteredCountries);
return [countries, setCountries];
};
export default useCountrySelect;
Hook usage in a component:
const [country] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);
Now, Why is your code resulting in an endless loop?
it is because when u call useCountrySelect hook it will call setCountries(filteredCountries). now state is changed. react tries re-render the component. but in the rerendering process, setCountries(filteredCountries) will be called again. this will continue like a loop.
try this,
import {useState} from 'react'
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState(() => {
return defaultCountires.filter(
(country) => country !== defaultCountry
);
});
return [countries, setCountries];
};
export default useCountrySelect;
setCountries(filteredCountries); is unconditionally enqueueing state updates and triggering the render looping. Just set the state initially.
const { useState } = require("react");
const useCountrySelect = (defaultCountires = [], defaultCountry) => {
const [countries, setCountries] = useState(defaultCountires.filter(
(country) => country !== defaultCountry
));
return [countries, setCountries];
};
export default useCountrySelect;
Usage:
const [country] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);
Output:
["lv", "ee"]
Don't understand why your state variable is an array. It seems you want the custom hook to return the selected country as well as a function to update it.
If my understanding is correct, here is a possible solution :
const { useState } = require("react");
function useCountrySelect(countries, defaultCountry) {
const [country, setCountry] = useState(defaultCountry);
// reset the country when input countries change
useEffect(() => setCountry(defaultCountry), [countries.join()]);
function setCountryWrapper(nextCountry) {
// prevent updates with invalid country
if (!countries.includes(nextCountry) {
return;
}
setCountry(country);
}
return [country, setCountryWrapper, countries];
};
export default useCountrySelect;
Usage
const [country, setCountry] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);
const { useState, useEffect } = require("react");
function filter(defaultCountires, defaultCountry) {
const filteredCountries = defaultCountires.filter(
(country) => country !== defaultCountry
);
return filteredCountries;
}
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState(
filter(defaultCountires, defaultCountry) // this will set the initial state
);
// side-effects like this should go inside `useEffect`
useEffect(() => {
setCountries(filter(defaultCountires, defaultCountry));
}, [defaultCountires, defaultCountry]); // based on prop change, this effect will rerun again
// don't set any state directly in the root component/hook body! Else you'll be stuck in an endless loop
return [countries, setCountries];
};
export default useCountrySelect;

Which is the right way to detect first render in a react component

I have a scenario where I need to detect the first render of a component. Here I have build a small example. Could someone explain to me what is the correct approach?
Why do most of the people suggest to use a ref instead of a plain state.
https://codesandbox.io/s/condescending-burnell-0ex3x?file=/src/App.js
import React, { useState, useRef, useEffect } from "react";
import "./styles.css";
export default function App() {
const firstRender = useDetectFirstRender();
const [random, setRandom] = useState("123");
useEffect(() => {
if (firstRender) {
console.log("first");
} else {
console.log("second");
}
}, [random]);
return (
<div className="App">
<h1>Random Number is {random}</h1>
<button onClick={() => setRandom(Math.random())}>Change Name</button>
</div>
);
}
//Approach 1
// export function useDetectFirstRender() {
// const firstRender = useRef(true);
// useEffect(() => {
// firstRender.current = false;
// }, []);
// return firstRender.current;
// }
//Approach 2
export function useDetectFirstRender() {
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setFirstRender(false);
}, []);
return firstRender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You could create a reusable custom hook for that, based on useRef.
function useFirstRender() {
const ref = useRef(true);
const firstRender = ref.current;
ref.current = false;
return firstRender;
}
you can detect and save it by using useMemo or useCallback hook. but here the most preferable is useMemo as it prevent the same rendering again and again.
const firstRender = useMemo(
() =>console.log('first Render'),
[]
);
here it will render once and save value in the first Render,so you can use this anywhere where you need.
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
doSomething()
});
The useEffect hook takes a second parameter. This second param is an array of variables that the component will check ensure they've changed before re-rendering. However, if that array is empty, the hook is only called once during initial render. This is similar to the useMemo() trick posted previously.
useEffect(() => doSomethingOnce(), [])
^^

Custom React hook, infinite loop only if I add the second dependency. Bug or something I can't understand?

I've made a really simple React hook. That's something seen on many guides and websites:
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetchRemote = (remote, options, initialDataState) => {
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, options);
setData(result.data);
};
fetchData();
}, [remote]);
return data;
};
Example usage:
import { useFetchRemote } from '../utils';
export const UserList = () => {
const users = useFetchRemote('/api/users', {}, []);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
This is working. If I understand correctly:
With no dependencies like useEffect(() => { /*...*/ }), setting the state into the function would trigger a re-render, calling useEffect again, in an infinite loop.
With empty dependencies like useEffect(() => { /*...*/ }, []), my function will be called only the "very first time" component is mounted.
So, in my case, remote is a dependency. My function should be called again if remote changes. This is true also for options. If I add also options, the infinite loop starts. I can't understand... why this is happening?
export const useFetchRemote = (remote, options, initialDataState) => {
// ...
useEffect(() => {
// ...
}, [remote, options]);
// ...
};
The infinite loop is caused by the fact that your options parameter is an object literal, which creates a new reference on every render of UserList. Either create a constant reference by defining a constant outside the scope of UserList like this:
const options = {};
const initialDataState = [];
export const UserList = () => {
// or for variable options instead...
// const [options, setOptions] = useState({});
const users = useFetchRemote('/api/users', options, initialDataState);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
or if you intend the options parameter to be effectively constant for each usage of the userFetchRemote() hook, you can do the equivalent of initializing props into state and prevent the reference from updating on every render:
export const useFetchRemote = (remote, options, initialDataState) => {
const [optionsState] = useState(options);
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, optionsState);
setData(result.data);
};
fetchData();
}, [remote, optionsState]);
// ---------^
return data;
};
This second approach will prevent a new fetch from occuring though, if the options are dynamically changed on a particular call site of useFetchRemote().

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

Categories