My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.
If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});
You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js
Related
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.
};
I have a working rest service that I want to invoke in react.
The code does display the list of countries.
import {React, useEffect, useState } from 'react'
export const Noc = () => {
const [nocs, setNoc] = useState();
useEffect(
() => {
const fetchNoc = async () => {
const response = await fetch('http://localhost:8080/countrydefinitions');
const data = await response.json();
setNoc(data);
};
fetchNoc();
},[]
);
return <div>
<div>NOC LIST</div>
{nocs.map(noc => <div>{noc.region}</div>)}
</div>
}
But most of the times I get this error
TypeError: Cannot read property 'map' of undefined
sometimes it prints the list soemetimes it does'nt. Is there some sort of a delay or wait that I need to introduce?
How can I introduce a delay or make sure that setnoc has been called and nocs has a value before printing it.
React will rerender when the props change. Indeed at the beginning its nothing yet.
2 things to make it more solid can be done.
Add a default value, so that map is available on the array.
const [nocs, setNoc] = useState([]);
And/Or wait until noc is not undefined anymore, and validating that It has a map function, before trying to use map.
{nocs && typeof nocs.map === 'function' && nocs.map(noc => <div>{noc.region}</div>)}
No. there is no need to introduce any delay/wait. That is already handled by the async/await syntax.
You can either set an initial value in your useState or early return a custom message if nocs is undefined.
If you are fetching from your own api, you can return a response with an error if the fetch request should fail. And then at client side, you can handle that error by wrapping your fetch call inside a try-catch block
import {useEffect, useState } from 'react'
export const Noc = () => {
const [nocs, setNoc] = useState();
useEffect(
() => {
const fetchNoc = async () => {
const response = await fetch('http://localhost:8080/countrydefinitions');
const data = await response.json();
setNoc(data);
};
try{
fetch()
}catch(error){
//handle error here
console.log(error);
}
},[]
);
if(!nocs){
return (<p>No NOC found</p>)
}
return <div>
<div>NOC LIST</div>
{nocs.map(noc => <div>{noc.region}</div>)}
</div>
}
at first render, nocs has no data then useEffect runs and the second render, nocs will have data.
you need to check if nocs is undefined
{nocs && nocs.length > 0 && nocs.map(noc => <div>{noc.region}</div>)}
So I have a hook that on mount, reads data from an indexedDB and stores that data in its internal state
The problem that I have, is that the indexedDB data gets changed from another component (added/removed), and I need to react to those changes in this hook. I'm not 100% familiar with hooks and how this would be done, but the first thought would be to have as hook dependency the value from the indexedDB.
HOWEVER, the reading of the data from the indexedDB is an async operation and the hook dependency would be a.. promise.
So basically, the flow is as follows:
Component 1 calls the hook like so:
const eventItems = useEventListItems({
sortBy,
sortGroupedBy,
eventTimestamp,
events,
assets,
touchedEventIds,
unsyncedEvents, // <--- this is the one that we need
order,
});
The useEventListItems hook, on mount, reads the data from the indexed DB, stores it in its internal state and returns it:
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [setUnsyncedEvents]);
where readUnsyncedEvents is:
export const readUnsyncedEvents = <T>(type: Type): Promise<DebriefStoreEntry<T>> =>
debriefStore
.get(type)
.then((entry) => entry && { data: entry.data, timestamp: entry.timestamp });
The unsyncedEvents from the indexedDB are then changed from another component.
What should happen now, is that the useEventListItems hook should listen to the changes in the IDB and update the unsyncedEvents in its internal state, passing them to the component that uses this hook. How would I achieve this?
My first thought was to have something like this in the useEventListItems hook:
useEffect(() => {
setUnsyncedEvents(..newValueFromIdb);
}, [ await readUnsyncedEvents()]);
but that won't work since it'll be a promise. Is there anyway I can have as hook dependency, a value returned by an async operation?
You can use Context API to refetch the data from IDB.
The idea here is to create a context with a counter variable which will be updated after each IDB update operation. And useEventListItems hook will read that counter variable from context and trigger the useEffect hook.
export const IDBContext = React.createContext({
readFromIDB: null,
setReadFromIDB: () => {}
});
export const IDBContextProvider = ({ children }) => {
const [readFromIDB, setReadFromIDB] = useState(0);
return (
<IDBContext.Provider value={{ readFromIDB, setReadFromIDB }}>
{children}
</IDBContext.Provider>
);
};
This is how your useEventListItems will look like.
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
const {readFromIDB} = useContext(IDBContext); // this variable will be updated after each IDB update.
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [readFromIDB,setUnsyncedEvents]); // added that to dependency array to trigger the hook on value change.
And here are the components:
const IDBUpdateComponent = ()=>{
const {readFromIDB,setReadFromIDB} = useContext(IDBContext);
const updateIDB = ()=>{
someIDBUpdateOpetation().then(res=>{
setReadFromIDB(readFromIDB+1) // update the context after IDB update is successful.
}).catch(e=>{})
}
return(
<div>IDBUpdateComponent</div>
);
}
const IDBConsumerComponent = ()=>{
return (
<div>IDBConsumerComponent</div>
)
}
Just make sure that both the components are wrapped inside the context so that they can access the values.
const App = ()=>{
return(
<div>
<IDBContextProvider>
<IDBUpdateComponent />
<IDBConsumerComponent />
</IDBContextProvider>
</div>
)
}
I watched a Youtube video and I made my own recipe app. I'm a beginner at React.js and I've been solving this problem for about 2 days. Seems that i cant pass the value of my state to useEffect hook. Here's an example of my code. The error says
"React Hook useEffect has a missing dependency: 'query'. Either include it or remove the dependency array" and everytime I typed in the input box it triggers the useEffect hook. Thank you and your help is very much appreciated.
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('steak');
const updateSearch = e => {
setSearch(e.target.value);
console.log(search)
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
}
useEffect(() => { // error Is from the Query variable
const GetRecipe = async () => {
const APP_ID = "3834705e";
const APP_KEY = "c23e9514f82c2440abf54b21edd4c3dc";
const res = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`);
const data = await res.json();
setRecipes(data.hits);
}
GetRecipe();
},[getSearch]) //this triggers everytime I typed in the input box which is not it is supposed to
return(
<div className='recipelist'>
<form onSubmit={getSearch}>
<input type="search" onChange={updateSearch}/>
<button type='submit'>submit</button>
</form>
As the error tells you, when using a useEffect hook, that hook can receive two arguments, the first one is the handler effect and the second one is an array containing all dependencies that effect will use, so as you are using the query state into the http url, you need to pass that dependency into the array, so could be something like this.
useEffect(() => { // error Is from the Query variable
const GetRecipe = async () => {
const APP_ID = "3834705e";
const APP_KEY = "c23e9514f82c2440abf54b21edd4c3dc";
const res = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`);
const data = await res.json();
setRecipes(data.hits);
}
GetRecipe();
},[getSearch, query])
so what is actually doing the array dependency, as React docs says, array dependency it's used to check if the effect should execute again based on its dependencies, so in your code everything you type something, getSearch method is re-creating again and again in memory, so it will check the last getSearch function that it took and compare it with the new ones, so it will check as equally checker like fn1 === fn2, so due to both function are exactly the same, both keeps different space in memory, so both are different objects, check this docs to understand the concept.
Here you have the react docs too
I've been loving getting into hooks and dealing with all the new fun issues that come up with real-world problems :) Here's one I've run into a couple of times and would love to see how you "should" solve it!
Overview: I have created a custom hook to capsulate some of the business logic of my app and to store some of my state. I use that custom hook inside a component and fire off an event on load.
The issue is: my hook's loadItems function requires access to my items to grab the ID of the last item. Adding items to my dependency array causes an infinite loop. Here's a (simplified) example:
Simple ItemList Component
//
// Simple functional component
//
import React, { useEffect } from 'react'
import useItems from '/path/to/custom/hooks/useItems'
const ItemList = () => {
const { items, loadItems } = useItems()
// On load, use our custom hook to fire off an API call
// NOTE: This is where the problem lies. Since in our hook (below)
// we rely on `items` to set some params for our API, when items changes
// `loadItems` will also change, firing off this `useEffect` call again.. and again :)
useEffect(() => {
loadItems()
}, [loadItems])
return (
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
)
}
export default ItemList
Custom useItems Hook
//
// Simple custom hook
//
import { useState, useCallback } from 'react'
const useItems = () => {
const [items, setItems] = useState([])
// NOTE: Part two of where the problem comes into play. Since I'm using `items`
// to grab the last item's id, I need to supply that as a dependency to the `loadItems`
// call per linting (and React docs) instructions. But of course, I'm setting items in
// this... so every time this is run it will also update.
const loadItems = useCallback(() => {
// Grab our last item
const lastItem = items[items.length - 1]
// Supply that item's id to our API so we can paginate
const params = {
itemsAfter: lastItem ? lastItem.id : nil
}
// Now hit our API and update our items
return Api.fetchItems(params).then(response => setItems(response.data))
}, [items])
return { items, loadItems }
}
export default useItems
The comments inside the code should point out the problem, but the only solution I can come up with right now to make linters happy is to supply params TO the loadItems call (ex. loadItems({ itemsAfter: ... })) which, since the data is already in this custom hook, I am really hoping to not have to do everywhere I use the loadItems function.
Any help is greatly appreciated!
Mike
If you plan to run an effect just once, omit all dependencies:
useEffect(() => {
loadItems();
}, []);
You could try with useReducer, pass the dispatch as loadItems as it never changes reference. The reducer only cares if the action is NONE because that is what the cleanup function of useEffect does to clean up.
If action is not NONE then state will be set to last item of items, that will trigger useEffect to fetch using your api and when that resolves it'll use setItems to set the items.
const NONE = {};
const useItems = () => {
const [items, setItems] = useState([]);
const [lastItem, dispatch] = useReducer(
(state, action) => {
return action === NONE
? NONE
: items[items.length - 1];
},
NONE
);
useEffect(() => {
//initial useEffect or after cleanup, do nothing
if (lastItem === NONE) {
return;
}
const params = {
itemsAfter: lastItem ? lastItem.id : Nil,
};
// Now hit our API and update our items
Api.fetchItems(params).then(response =>
setItems(response)
);
return () => dispatch(NONE); //clean up
}, [lastItem]);
//return dispatch as load items, it'll set lastItem and trigger
// the useEffect
return { items, loadItems: dispatch };
};