// 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);
}
Related
In my nextjs-app I want to use localstorage, to store some values across my application.
so inside the pages-folder I have a [slug].tsx-file where I do this:
export default function Page({ data}) {
useEffect(() => {
const page = {
title: data.page.title,
subtitle: data.page.subtitle,
slug: data.page.slug,
}
localStorage.setItem("page", JSON.stringify(page))
})
return ( ... some html....)
}
this basically stores the title, subtitle and slug for the current route.
Now, inside my components-folder I have a Nav.tsx-file, where I do this:
const Nav= () => {
const [pageData, setPageData] = useState()
useEffect(() => {
const current = JSON.parse(localStoraget.getItem('page'))
if(current){
setPageData(current)
}
},[])
return(...some html)
}
So far, the setItem works and in the application-tab of the google inspector I can see, that the key-values changes, each time a new route/page gets rendered BUT the getItem- always returns the same e.g. the key values do not change at all. What am I doing wrong? Is it maybe because the Nav component only gets rendered once?
Can someone help me out?
you have a spelling error from:
localStoraget.getItem('page')
to:
localStorage.getItem('page')
believe your issue also falls under localstorage should be used with async/await so maybe try something like:
const Nav= () => {
const [pageData, setPageData] = useState()
useEffect(() => {
async function settingData() {
const current = await JSON.parse(localStorage.getItem('page'))
if(current)setPageData(current)
}
settingData()
},[])
return(...some html)
}
Note: You should avoid using localStorage to share the state over your App. React provides a good way of doing it with ContextAPI or you could use another lib such as Redux/MobX/Recoil.
At the time when the <Nav> component is rendered (and the useEffect runs) the localStorage probably still doesn't have the key-value set.
If you really want to use localStorage (but I suggest not using it), you can create a timeout to execute after some time and will try to get again the value. Something like this could work:
let localStorageTimer = null;
const Nav = () => {
const [pageData, setPageData] = useState()
useEffect(() => {
const getLocalStorageItems = () => {
const current = JSON.parse(localStorage.getItem('page'))
if (!current) {
localStorageTimer = setTimeout(() => getLocalStorageItems, 1000);
} else {
clearTimeout(localStorageTimer)
setPageData(current)
}
}
localStorageTimer = setTimeout(() => getLocalStorageItems, 1000);
return () => clearTimeout(localStorageTimer)
}, []);
return (.. your JSX code)
}
I'm trying to SetState but inside it, I have to get some data that needs async/await and I know it can't be done so I wonder if is there any way I can do it properly
codesanbox: https://codesandbox.io/s/infallible-mendeleev-6641iw?file=/src/App.js:0-614
Edit: It's hard for me to get data before setState because If I only want to get data If the newValue satisfies a condition so Get data force to inside setState
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [value, setValue] = useState([]);
const run = async () => {
setValue((oldValue) => {
const newValue = [...oldValue];
// do something makes newValue changes
if (newValue == true) { // if newValue satisfy a condition
const res = fetch(`https://fakestoreapi.com/products/${newValue.length}`);
const result = res.json();
newValue.push(result.title);
}
return newValue;
});
};
return (
<div className="App">
<button onClick={run}>get result</button>
{value.map((item, index) => {
return <h2 key={index}>{value[index]}</h2>;
})}
</div>
);
}
Why don't you run first the async code, and when the data are available set the state ?
const run = async (x) => {
const res = await fetch(`https://fakestoreapi.com/products/${x}`);
const result = await res.json();
setValue((oldValue) => {
// you have access to the fetched data here
const newValue = [...oldValue];
console.log(result.title);
return newValue;
});
};
And ofcourse the click handler should be
onClick={() => run(2)}
There's a React component called Suspense. I think it first appeared in v16, but in all honesty I have only used it with React v18, so unsure if it will work for you.
I'll refer you to a live demo I have: wj-config Live Demo
Here I use <Suspense> to wrap a component that requires data that is asynchronously obtained, just like when you use fetch().
Suspense works like this:
It attempts to load the inner children but places a try..catch in said loading process.
If an error is caught, and the caught error is a promise then Suspense will instead render the component in its fallback property.
After rendering what's in fallback, React awaits the caught promise.
Once the caught promise resolves, Suspense retries rendering the child components.
I hear there are frameworks that provides the necessary mechanisms to use Suspense in a simple and expedite manner, but in my live demo I did it all myself. Is not too bad I think.
The procedure to use this is:
Create a readXXX function that is a suspender function (a function that throws a promise).
Call this function at the beginning of your inner Suspense component's code.
Program the rest of your inner component as if the readXXX function has worked and returned the needed data.
Here's the code I have in the live demo to create suspender functions:
function suspenderWrapper(promise) {
let isPending = true;
let outcome = null;
const suspender = promise.then(
(r) => {
outcome = r;
isPending = false;
},
(e) => {
outcome = e;
isPending = false;
}
);
return () => {
if (isPending) {
throw suspender;
}
return outcome;
};
}
Open up my live code's demo and look for App.js where all the magic is shown.
You can get the old value from the value variable which is always storing the current state, and do the if check on it instead.
And if the if condition on value was true - then you can call fetch and after that call setState and update the state.
If the condition was not true, there is no need to update the state since it stayed the same.
See the code below:
const [value, setValue] = useState([]);
const run = async () => {
//some condition on the current value
if (value) {
const res = await fetch( `https://fakestoreapi.com/products/${newValue.length}`);
const result = await res.json();
// And here apply the changes on the state
setValue((oldValue) => {
const newValue = [...oldValue];
newValue.push(result.title);
return newValue;
});
}
//Outside the if block - no need to change the state.
};
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 });
}
I'm using async storage to retrieve my data but how do I use it with useState()?
Example:
async function getdata(){
let d= await AsyncStorage.getItem('Name');
return d;
}
export default function App() {
const [name, setName] = useState(() => getdata());
return (<View>...</View>)
}
but this doesn't work since getdata() is async, so how do I solve this problem?
Edit:
I forgot to mention I tried the useEffect() Hook like this:
async function getdata(){
let d= await AsyncStorage.getItem('Name');
console.log('retrieved Data!');
return d;
}
const [name, setName] = useState(()=>0);
useEffect(() => {
getData().then(setName);
console.log('moving on...');
}, []);
but the order of execution was:
'Moving on...'
'retrieved Data!'
where as it should have been the other way around
You'll initialize your state to some empty value. Maybe an empty string, maybe a null. If you like, you can have your component render something different during this time, such as a loading indicator. Then once the data loads, you can set state to render again.
export default function App() {
const [name, setName] = useState(null);
useEffect(() => {
getData().then(value => setName(value));
}, []);
if (name === null) {
return <Text>Loading...</Text>
} else {
return (<View>...</View>)
}
}
You have to use useEffect
export default function App() {
const [name, setName] = useState('');
useEffect(() => {
const bootstrap = async () => {
const name = await getdata();
setName(name);
};
bootstrap();
}, []);
return <View>...</View>;
}
I think the best solution here is to initialize name as false, and then use 'setName' in a .then() callback. setName (and those setter functions that come out of useState generally) is meant to be a way of updating that value asynchronously.
I'm setting name to the boolean 'false' here and checking it strictly. If there is a case where that name field is empty it can just be set to an empty string without causing an infinite loop.
async function getdata(){
let d= await AsyncStorage.getItem('Name');
return d;
}
export default function App() {
const [name, setName] = useState(false);
useEffect(() => {
if (name === false) {
getData().then((name) => {
setName(name);
});
}
}, []);
return (<View>...</View>)
}
Keep in mind that your view has to be able to deal with a false name value. How it does that is up to you.
^^^The above solution should effectively solve your problem but I'll make a quick note here: If your component is relatively complex it might be re-rendering a number of times for other reasons and will consequently run the if(name === false) condition as it waits for the callback to resolve. This won't break things but it'll hammer async storage more than necessary and might affect performance. If you find that to be the case, look into 'debouncing' callbacks using the helpful article below.
https://medium.com/#gabrielmickey28/using-debounce-with-react-components-f988c28f52c1
After a huge amount of trial and error for a complex webGL project I have landed on a solution that will reduce the amount of re-engineering working, threejs code (from another developer) and, as this project is extremely time restrained, reduce the amount of time needed. It's also worth noting my experience of this is limited and I am the only developer left on the team.
The project current accepts a large array of random user data, which is exported from a js file and then consumed here...
import Users from "./data/data-users";
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
Users.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
Here is my fetch request..
import { useState, useEffect } from "react";
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"https://swapi.co/api/planets/4/"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return JSON.stringify(users);
};
export default FetchUsers;
I have run into lots of issues as the UserManager is a class component and if I import my fetchUsers into this file, call it and save it to a variable like so const Users = fetchUsers(); it violates hooks.
I want to be able to return a function that will return my users from the database as an array.
That will then be able to be passed into the UserManager in the same way the hard coded data is and mapped over to be actioned by LOTS of other files.
I've mocked up a small codesandbox with what the flow would be ideally but I know I need a solution outside of hooks...
https://codesandbox.io/s/funny-borg-u2yl6
thanks
--- EDIT ---
import usersP from "./data/data-users";
class UsersManager {
constructor() {
this.mapUserCountries = {};
this.state = {
users: undefined
};
}
init() {
usersP.then(users => {
this.setState({ users });
});
console.log(usersP);
this.state.users.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
console.log (UsersManager.js:16 Uncaught TypeError: Cannot read property 'forEach' of undefined
at UsersManager.init (UsersManager.js:16)
at Loader.SceneApp.onLoadingComplete [as callback] (App.js:39)
at Loader.onAssetLoaded (index.js:20)
at index.js:36
at three.module.js:36226
at HTMLImageElement.onImageLoad)
I fixed your sandbox example.
You cannot load the users synchronously (using import) as you need to make a http call to fetch the users so it's asynchronous.
As a result you can fetch the users inside the componentDidMount lifecycle method and use a state variable to store them once they are fetched
There are a couple guidelines that will help separate functions that are Hooks and functions that are Components (these are true most of the time):
1 Component functions use pascal case (start with a capital letter) and always return JSX.
2 Custom Hooks functions conventionally begin with the word "use" and never return JSX.
In your case you probably want to make a custom Hooks function that must be called in a component;
function useUserData() {
const [hasError, setErrors] = useState(false);
const [users, setUsers] = useState({});
const networkCall = useCallback(async fetchData = () => {
const res = await fetch(
"https://swapi.co/api/planets/4/"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
} , [])
useEffect(() => {
fetchData();
}, []);
return {users, hasError};
}
Then call that custom hook in one of your components:
function App() {
const {users, hasError} = useUserData();
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>{users}</div>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
}
If you then need to share that fetched data throughout your app, you can pass it down via props or the context API: https://reactjs.org/docs/context.html
(post a message if you'd like an example of this).