react native state not changing - reactjs

i want to pass the string from asyncstorage to a state variable using hooks, but it doesn't work, trying for hours but still doesn't work.
here is the code:
const [cartitems, cartitemsfunc] = useState('');
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
console.log('items from cart2:' + cartitems);
};
d();
when i try console logging cartitems it logs an empty string in the console,but items logs out the string
can someone please tell me where i went wrong
thanks in advance!!!

As mentioned in the comments and link supplied, useState is asynchronous so setting a value and immediately reading it in the following line will not yield consistent result:
cartitemsfunc(items); // async call
console.log('items from cart2:' + cartitems); // cartitems not updated yet
It is important to also understand that whenever you update state using useState, the component will render again. If you have a method call in the body of the app it will be run everytime you update state. So what you have is a scenario where you are making a call to update state, but the method is being executed and ends up overwriting your changes.
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
console.log('items from cart2:' + cartitems);
};
d(); // this will run every time the component renders - after you update state
If you only need the value at initial render, then call the method from useEffect and set the dependency chain to [] so it only runs once at first render, and not every time state is updated.
Below is an example that demonstrates getting/setting values from localStorage and also updating the state directly.
CodeSandbox Demo
import React, { useState, useEffect } from "react";
import AsyncStorage from "#react-native-community/async-storage";
import "./styles.css";
export default function App() {
const [cartItems, setCartItems] = useState(null);
const setLSItems = async () => {
await AsyncStorage.setItem(
"items",
JSON.stringify([{ id: "foo", quantity: 1 }, { id: "bar", quantity: 2 }])
);
getLSItems(); // or setCartItems("Foo");
};
const clearLSItems = async () => {
await AsyncStorage.removeItem("items");
getLSItems(); // or or setCartItems(null);
};
const getLSItems = async () => {
const items = await AsyncStorage.getItem("items");
setCartItems(JSON.parse(items));
};
// bypass using local storage
const setCartItemsDirectly = () => {
setCartItems([{ id: "baz", quantity: 3 }]);
};
useEffect(() => {
getLSItems();
}, []); // run once at start
return (
<div className="App">
<div>
<button onClick={setLSItems}>Set LS Items</button>
<button onClick={clearLSItems}>Clear LS Items</button>
</div>
<div>
<button onClick={setCartItemsDirectly}>Set Cart Items Directly</button>
</div>
<hr />
{cartItems &&
cartItems.map((item, index) => (
<div key={index}>
item: {item.id} | quantity: {item.quantity}
</div>
))}
</div>
);
}

Since setState is async you will need to observe when it finishes, this can be easily done using useEffect and adding caritems to the dependencies array
const [cartitems, cartitemsfunc] = useState('');
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
};
useEffect(()=> {
console.log('items from cart2:' + cartitems);
}, [caritems]);
d();

I think here console.log() statement is executed before updating the state. As here await has been used so it will execute the next lines before getting the result.
Therefore , in this type of situation we use combination of useEffect() and useState(). useEffect() for getting the data and useState() for updating the state and re-render the UI.

Related

How to call custom hook useFetch with different query per render?

I've got a component, Crafts.js, which calls for useFetch() with a firestore query as an argument:
const q = query(craftsColRef, where("category", "==", currPage));
const { data:crafts, setData:setCrafts, mats, setMats } = useFetch(q);
The third argument of where() in the query is a prop passed to crafts component which is updated in a parent component. The currPage prop does update with the new value, but I think it's clear that React doesn't call useFetch on re-render, therefore I don't get the new data.
I'm trying to achieve some kind of navigation. User clicks a button and the docs are filtered in a different way. How can I achieve this?
Thank you!
I am not sure what is written in your useFetch but you can write your custom hook and from my understanding of your logic flow, I made a sample code. Hope it helps
import { useEffect, useState } from "react";
function firestoreQuery(currPage) {
// since this is just a fake api
// please change it to your query logic here
return new Promise(resolve => {
resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].sort(() => Math.random() - 0.5));
})
}
// Your custom hook
function useCustomFetch(currPage) {
const [items, setItems] = useState([]);
async function fetch() {
let result = await firestoreQuery(currPage);
setItems(result);
}
useEffect(() => {
if (!currPage) return;
console.log("fetch")
fetch();
}, [currPage]);
return { items };
}
function Craft({ currPage }) {
const { items } = useCustomFetch(currPage);
return <div>
{items.map(i => <span>{i}</span>)}
</div>
}
function ParentComponentPage() {
const [timestamp, setTimestamp] = useState();
return <div>
<button onClick={() => setTimestamp(new Date().getTime())}>Change currPage</button>
<Craft currPage={timestamp} />
</div>
}
export default ParentComponentPage;

How to execute a function AFTER an API-Call was made with setState within useEffect?

i am a React newbie (30h learning) and some basic Javascript background. Now i am learning with a course, and tried to "leave" the path. But i am curious how my intended goal could be achieved.
There is a Memegenerator who get all the images of memes from an API in the beginning of the rendering. This is solved with an useEffect-Hook. Now i want that the function getMemeImage() is running ONCE at the beginning AFTER the API-Call was made and the state was updated (this is not part of the course, but i want to try it anyway).
But its giving me an error: "Can't perform a React state update on an unmounted component"
While my research i found that things like didMount(tbh i not understand) and so on are not the "modern" way of react with primarily using hooks and you can simply use an second useEffect. But that is unfortunately not working for me.
How can i solve this and is there an "simple" way for a beginner or it is advanced stuff? I thought maybe to use a timeoutfunction, but seems to be very bad coding.
import React from "react"
export default function Meme() {
const [meme, setMeme] = React.useState({
topText: "",
bottomText: "",
randomImage: ""
})
const [allMemes, setAllMemes] = React.useState([])
React.useEffect(() => { /* first useEffect */
fetch("https://api.imgflip.com/get_memes")
.then(res => res.json())
.then(data => setAllMemes(data.data.memes))
}, [])
React.useEffect(() => { /* This should run after the setAllMemes in the first useEffect was complete */
getMemeImage()
}, [allMemes])
function getMemeImage() {
const randomNumber = Math.floor(Math.random() * allMemes.length)
const url = allMemes[randomNumber].url
setMeme(prevMeme => ({
...prevMeme,
randomImage: url
}))
}
function handleChange(event) {
const {name, value} = event.target
setMeme(prevMeme => ({
...prevMeme,
[name]: value
}))
}
return (
<main>
<div className="form">
<input
type="text"
placeholder="Top text"
className="form--input"
name="topText"
value={meme.topText}
onChange={handleChange}
/>
<input
type="text"
placeholder="Bottom text"
className="form--input"
name="bottomText"
value={meme.bottomText}
onChange={handleChange}
/>
<button
className="form--button"
onClick={getMemeImage}
>
Get a new meme image 🖼
</button>
</div>
<div className="meme">
<img src={meme.randomImage} className="meme--image" />
<h2 className="meme--text top">{meme.topText}</h2>
<h2 className="meme--text bottom">{meme.bottomText}</h2>
</div>
</main>
)
}
First of all, "Can't perform a React state update on an unmounted component" is a warning, not an error.
In order to call 'getMemeImage' only after the first useEffect hook execution, you can check the value of a binary flag that changes after the first useEffect was executed and the response of the async function was recieved.
import React from "react";
export default function Meme() {
const [meme, setMeme] = React.useState({
topText: "",
bottomText: "",
randomImage: ""
});
const [allMemes, setAllMemes] = React.useState([]);
const isInitialRender = React.useRef(true);
React.useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((res) => res.json())
.then((data) => {
isInitialRender.current = false;
setAllMemes(data.data.memes);
});
}, []);
React.useEffect(() => {
/* Check if it is called before the first useEffect data fetching was completed */
if (!isInitialRender.current) getMemeImage();
}, [allMemes]);
function getMemeImage() {
const randomNumber = Math.floor(Math.random() * allMemes.length);
const url = allMemes[randomNumber].url;
setMeme((prevMeme) => ({
...prevMeme,
randomImage: url
}));
}
}
Using the useRef hook persists the value it references between renders.
You can just add control to your second useEffect, it will solve your issue. This is working sample https://codesandbox.io/s/heuristic-matan-3kdbmv
React.useEffect(() => {
if (allMemes.length > 0) {
getMemeImage();
}
}, [allMemes]);

React Initializing and synchronizing state with backend

I wonder what is the best way/pattern to initialize state and keep it synced with the server. I've read and tried a lot for the last couple of days but haven't found anything that solves my question.
The example is really simple. There is a state - it's a number in the example for the sake of simplicity, although in real life it would be an object - that I need to retrieve from the server. Once retrieved, I want it to be synchronized with the server. The getValueFromServer is a mock that returns a random value after waiting a random amount of time in a setTimeout.
So, to initialize my state I use a useEffect on an empty array as a dependency, and to keep it synched I use a useEffect with the state as a dependency.
The problem is that it is trying to save an undefined value. The log is as follows.
1 -> Initializing for first time
2 -> API call to save in server value: undefined
3 -> API call to save in server value: 6.026930847574949
What I get:
1: Runs on mounting as expected.
2: This one I didn't expect. I guess it is triggered because of the "useState".
3: Runs once we get the response from the server. Kind of obvious but a pain in the ass, because why on earth would I want to save this.
What would the best approach be here? Using something like a "isInitialized" flag in the useEffect with dependency feels kind of hacked and not professional.
Code below and you can find it working here too: https://codesandbox.io/s/optimistic-rgb-uce9f
import React, { useState, useEffect } from "react";
import { getValueFromServer } from "./api";
export default function App() {
const [value, setValue] = useState();
useEffect(() => {
async function initialize() {
console.log("Initializing for first time");
let serverValue = await getValueFromServer();
setValue(serverValue);
}
initialize();
}, []);
useEffect(() => {
console.log("API call to save in server value: ", value);
}, [value]);
const handleClick = () => {
setValue(value + 1);
};
return (
<div className="App">
<h1>Value: {value}</h1>
<button onClick={handleClick}>Add 1 to value</button>
</div>
);
}
What would the best approach be here? Using something like a
"isInitialized" flag in the useEffect with dependency feels kind of
hacked and not professional.
You can either use a flag or initialize object with default value in useState
const { Fragment, useState, useEffect } = React;
const getValueFromServer = () => new Promise((resolve, reject) => setTimeout(() => resolve(Math.random())), 1000)
const App = () => {
const [value, setValue] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isUnmounted = false;
getValueFromServer().then(serverValue => {
console.log("Initializing for first time");
if(isUnmounted) {
return;
}
setValue(serverValue);
setLoading(false);
})
return () => {
isUnmounted = true;
}
}, []);
useEffect(() => {
if(!value) {
return () => {}
}
console.log("API call to save in server value: ", value);
setTimeout(() => setLoading(false), 50);
}, [value]);
const handleClick = () => {
setLoading(true);
setValue(value + 1);
};
return <div className="App">
{isLoading ? <Fragment>
<span>Loading...</span>
</Fragment> : <Fragment>
<h1>Value: {value}</h1>
<button onClick={handleClick}>Add 1 to value</button>
</Fragment>}
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.10.1/polyfill.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
your app does other stuff while it's busy awaiting something to happen in your first useEffect hook function. "other stuff" in this case is "executing your second useEffect hook function". serverValue, and therefore value, is not defined at this point, so your console.log prints undefined. once your await promise resolves to a value and setValue gets called, your second useEffect hook function's dependencies change causing your function to be run a second time (and printing out the value you expect).
if you have to have two useEffect hooks, then just bail out of the second one when value is undefined:
useEffect(() => {
if (value === undefined) {
return;
}
console.log('value is:', value);
}, [ value ]);

How can I update a component's state based on its current state with React Hooks?

In my component, I'm running a function that iterates through keys in state and updates properties as async functions complete. However, it looks like it's updating the state to the state as it existed prior to the function running.
This is the code for my component:
interface VideoDownloaderProps {
videos: string[];
}
const VideoDownloader: React.FC<VideoDownloaderProps> = ({ videos }) => {
const [progress, setProgress] = useState({} as { [key: string]: string });
const [isDownloading, setIsDownloading] = useState(false);
async function initialSetup(vids: string[]) {
const existingKeys = await keys();
setProgress(
vids.reduce<{ [key: string]: string }>((a, b) => {
a[b] = existingKeys.indexOf(b) > -1 ? "downloaded" : "queued";
return a;
}, {})
);
}
useEffect(() => {
initialSetup(videos);
}, [videos]);
async function download() {
setIsDownloading(true);
const existingKeys = await keys();
for (const videoUrl of videos) {
if (existingKeys.indexOf(videoUrl) === -1) {
setProgress({ ...progress, [videoUrl]: "downloading" });
const response = await fetch(videoUrl);
const videoBlob = await response.blob();
await set(videoUrl, videoBlob);
}
setProgress({ ...progress, [videoUrl]: "downloaded" });
}
setIsDownloading(false);
}
return (
<div>
<button disabled={isDownloading} onClick={download}>
Download Videos
</button>
{Object.keys(progress).map(url => (
<p key={url}>{`${url} - ${progress[url]}`}</p>
))}
</div>
);
};
Essentially, this iterates through a list of URLs, downloads them, and then sets the URL in state to "downloaded". However, the behavior I'm seeing is that the URL shifts from "queued" to "downloading" and then back to "queued" once the next URL begins downloading.
I think the culprit is this line:
setProgress({ ...progress, [videoUrl]: "downloaded" });
I think progress is always in the same state it was when download executes.
Prior to Hooks, I could pass an updater function to setState, but I'm not sure how to reuse existing state in a useState hook.
You can pass an updater function just like with setState. So, in this code, you'd run:
setProgress(progress => ({ ...progress, [videoUrl]: "downloading" }));
This will pass the current value of progress allowing you to update the state based on its current value.

React Hooks - Ref is not available inside useEffect

I am using ReactHooks. I am trying to access ref of User component in useEffect function, but I am getting elRef.current value as null, though I passed elRef.current as second argument to useEffect. I am supposed to get reference to an element, but outside (function body) of useEffect, ref value is available. Why is that ? How can I get elRef.current value inside useEffect?
code
import React, { Component, useState, useRef, useEffect } from "react";
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(
() => {
setIsLoading(true);
fetch(url)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
setIsLoading(false);
setData(json.data);
})
.catch(error => {
setIsLoading(false);
setError(error);
});
},
[url]
);
return { data, isLoading, error };
};
const User = ({ id }) => {
const elRef = useRef(null);
const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};
class App extends Component {
state = {
userId: 1
};
handleNextClick = () => {
this.setState(prevState => ({
userId: prevState.userId + 1
}));
};
handlePrevNext = () => {
this.setState(prevState => ({
userId: prevState.userId - 1
}));
};
render() {
return (
<div>
<button
onClick={() => this.handlePrevClick()}
disabled={this.state.userId === 1}
>
Prevoius
</button>
<button onClick={() => this.handleNextClick()}>Next</button>
<User id={this.state.userId} />
</div>
);
}
}
export default App;
Codesandbox link
Thanks !
You should use useCallback instead of useRef as suggested in the reactjs docs.
React will call that callback whenever the ref gets attached to a different node.
Replace this:
const elRef = useRef(null);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
with this:
const elRef = useCallback(node => {
if (node !== null) {
console.log("ref", node); // node = elRef.current
}
}, []);
It's a predictable behaviour.
As mentioned #estus you faced with this because first time when it's called on componentDidMount you're getting null (initial value) and get's updated only once on next elRef changing because, actually, reference still being the same.
If you need to reflect on every user change, you should pass [user] as second argument to function to make sure useEffect fired when user is changed.
Here is updated sandbox.
Hope it helped.
When you use a function as a ref, it is called with the instance when it is ready. So the easiest way to make the ref observable is to use useState instead of useRef:
const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;
Then you can use it in dependency arrays for other hooks, just like any other const value:
useEffect(() => {
if (element) console.log(element);
}, [element]);
See also How to rerender when refs change.
useEffect is used as both componentDidMount and componentDidUpdate,
at the time of component mount you added a condition:
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
because of the above condition at the time of mount, you don't have the user, so it returns null and div is not mounted in the DOM in which you are adding ref, so inside useEffect you are not getting elRef's current value as it is not rendered.
And on the click of next as the div is mounted in the dom you got the value of elRef.current.
The assumption here is that useEffect needs to detect changes to ref.current, so needs to have the ref or ref.currentin the dependencies list. I think this is due to es-lint being a bit over-pedantic.
Actually, the whole point of useEffect is that it guarantees not to run until the rendering is complete and the DOM is ready to go. That is how it handles side-effects.
So by the time useEffect is executed, we can be sure that elRef.current is set.
The problem with your code is that you don't run the renderer with <div ref={elRef}...> until after user is populated. So the DOM node you want elRef to reference doesn't yet exist. That is why you get the null logging - nothing to do with dependencies.
BTW: one possible alternative is to populate the div inside the effect hook:
useEffect(
() => {
if(!user) return;
elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
}, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.
That way the if (!user) return null; line in the User component is unnecessary. Remove it, and elRef.current is guaranteed to be populated with the div node from the very beginning.
set a useEffect on the elem's.current:
let elem = useRef();
useEffect(() => {
// ...
}, [elem.current]);

Resources