During rendering of the part below, the component CreateGarden appears for 1 sec even if the condition is true.
{userInfo.gardenName ?
<ShowPlants />
:
<CreateGarden setModal={setModal} />
}
Here is the custom hook I use to get userInfo
export default function useUserInfo() {
const [userInfo, setUserInfo] = useState([]);
const [categories, setCategories]= useState([]);
const userRef = doc(allUsers,auth.currentUser.uid);
useEffect(() => {
const unsub = onSnapshot(userRef, (doc)=>{
setUserInfo(doc.data())
setCategories(doc.data().categories)
})
return ()=>unsub()
},[auth.currentUser]);
return {userInfo, categories};
}
I tried creating loading state in the hook and use it as additional condition to render but it's still the same , I keep having a glimpse of the hidden component.
EDIT//SOLUTION:
Finally, after different versions I used the following solution
export default function useUserInfo() {
const [userInfo, setUserInfo] = useState({});
const [loading, setLoading ] = useState(true);
const [categories, setCategories]= useState([]);
const userRef = doc(allUsers,auth.currentUser.uid);
useEffect(() => {
const unsub = onSnapshot(userRef, (doc)=>{
setUserInfo(doc.data())
setCategories(doc.data().categories)
})
setLoading(false)
return ()=>unsub()
},[auth.currentUser]);
return {userInfo, categories, loading};
}
In the component:
{userInfo.gardenName && <ShowPlants />}
{!userInfo.gardenName && !loading && <CreateGarden setModal={setModal} />}
You could display a loading icon while the data is being fetched.
When
data hasn't been fetched yet (undefined) -> show loading icon (your hook could return a loading value as well as an error value in case something went wrong)
data has been fetched and there is a gardenName, display ShowPlants
data has been fetched and there is no gardenName, display CreateGarden
What if you use them separately?
{ userInfo.gardenName && <ShowPlants /> }
{ !userInfo.gardenName && <CreateGarden setModal={setModal} /> }
Related
In useEffect, I retrieve the data from the server and store it in the "products" array:
const { url } = props;
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
setLoadingSpinner(true);
const response = await fetch(url);
const responseData = await response.json();
setLoadingSpinner(false);
const loadedProducts = [];
for (const key in responseData) {
loadedProducts.push({
id: key,
name: responseData[key].name,
description: responseData[key].description,
price: responseData[key].price,
image: responseData[key].image,
processor: responseData[key].processor,
});
}
setProducts(loadedProducts);
setIsDataLoaded(true);
};
fetchProducts();
}, [url, isDataLoaded]);
I pass it to the ProductDetail component:
<ProductDetail products={products}></ProductDetail>
I always get a null value in the ProductDetail component:
function ProductDetail(props) {
const params = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
if (props.products && params.productId) {
const product = props.products.find(product => product.id === params.productId);
setProduct(product);
}
}, [props.products, params.productId]);
console.log(product)
I realized that when useEffect is run for the first time, my "products" array is still empty and it sends it to the component, so it's not good. But I don't know how to fix this.
There are a couple of things wrong here. I'm going to start with something that seems tangential at first but it will lead to a better answer.
In ProductDetail you introduce a new state called product. You then, in a useEffect, find the product from the list & product id in props and set this back into the state item. This would be unnecessary state duplication (even if not copying a prop verbatim into state, a value derived directly from props still counts) and is going to increase the surface areas for bugs in your application (this is probably the most common beginner's error).
You actually don't need that state item, you just need useMemo:
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
console.log(product)
To solve the issue with the product not being found briefly whilst then products load you can either (a) conditionally render the component so this code doesn't even run in the first place when products isn't fetched yet. Or (b) Change the ProductDetail so that it effectively does nothing until the product is found.
Solution A
const [products, setProducts] = useState(null); // We change the default to null to be able to distinguish empty list case from loading case.
// ...
{products !== null && <ProductDetail products={products}></ProductDetail>}
Solution B
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
if (!product) return null // Return early (do nothing) whilst product is not found
// ... your original render contents
edit: passing data as parameter into onSuccess worked for me
const [location, setLocation] = useState(null)
const [address, setAddress] = useState('')
const [desc, setDesc] = useState('')
const { data, isLoading, isError } = useQuery(["order"], async () => getOrders(id), {
onSuccess: (data) => {
setAddress([data[0].location.address.streetName, " ", data[0].location.address.buildingNumber, data[0].apartmentNumber ? "/" : '', data[0].apartmentNumber])
setDesc(data[0].description)
setLocation(data[0].location)
}
})
I can't initialize state with data from react-query. States are updating only after refocusing the window. What is the proper way to fetch data from queries and put it into states?
import { useParams } from "react-router-dom"
import { useQuery } from '#tanstack/react-query'
import { getOrders } from '../../api/ordersApi'
import { useState } from 'react'
import { getLocations } from '../../api/locationsApi'
const Order = () => {
const { id } = useParams()
const [address, setAddress] = useState('')
const [desc, setDesc] = useState('')
const [location, setLocation] = useState(undefined)
const { data, isLoading, isError } = useQuery(["order"], async () => getOrders(id), {
onSuccess: () => {
if (data) {
setDesc(data[0].description)
setAddress([data[0].location.address.streetName, " ", data[0].location.address.buildingNumber, data[0].apartmentNumber ? "/" : '', data[0].apartmentNumber])
}
}
})
const { data: locationData, isLoading: isLocationLoading, isError: isLocationError } = useQuery(["locations"], getLocations, {
onSuccess: () => {
if (locationData) {
setLocation(data[0].location)
}
}
})
if (isLoading || isLocationLoading) return (
<div>Loading</div>
)
if (isError || isLocationError) return (
<div>Error</div>
)
return (
data[0] && locationData && (
<div>
<h2>Zlecenie nr {data && data[0].ticket}</h2>
<h1>{address}</h1>
<textarea
type="text"
value={desc}
onChange={(e) => setDesc(e.target.value)}
placeholder="Enter description"
/>
<select
id="locations"
name="locations"
value={JSON.stringify(location)}
onChange={(e) => setLocation(JSON.parse(e.target.value))}
>
{locationData?.map((locationElement) => <option key={locationElement._id} value={JSON.stringify(locationElement)}>{locationElement.abbrev}</option>)}
</select>
<h1>{location?.abbrev}</h1>
<pre>{data && JSON.stringify(data[0], null, "\t")}</pre>
</div>)
)
}
export default Order
I know I am doing something wrong - data is queried but states are set to values from useState(). Please check my code and help me get into what is wrong here.
Consider the query keys (first parameter that you passing) of useQuery like useEffect's dependency array. Whatever you passed there, if it changes query will refetch too, just like how it's done on useEffect hook.
In your example, it seems like you have a desc state, and when it changes you want to query re-fetched and set to address and location states.
In your code, there is a logical mistake in that you have a text area that sets desc state and you are trying to make a request with it and storing the response in the same state.
True approach is create a state to store your desired query (like location) use it as a key in useQuery and store the data to another state. Even better, you can use the useReducer hook to store your data in a form format to have more readable and stable approach
At the moment, there is a component that is essentially copied 4 times. I would like to make it more abstract, and simply render it 4 times, and pass in the dynamic data each time. The data that's passed into each component are state hooks.
With that being the goal, could I get some help on the implementation?
Here's what a component call looks like in the parent:
const [allBlueItems, setAllBlueItems] = useState([]);
const [selectedBlueItems, setSelectedBlueItems] = useState([]);
const [allRedItems, setAllRedItems] = useState([]);
const [selectedRedItems, setSelectedRedItems] = useState([]);
<BlueSelection
allBlueItems={allBlueItems}
selectedBlueItems={setSelectedBlueItems}
setSelectedBlueItems={selectedBlueItems}
/>
<RedSelection
allRedItems={allRedItems}
selectedRedItems={setSelectedRedItems}
setSelectedRedItems={selectedRedItems}
/>
Then, the ItemSelection component uses those useState values passed in as props to render data, and updates the state accordingly:
const BlueSelection = ({ allBlueItems, selectedBlueItems, setSelectedBlueItems }) => {
useEffect(() => {
setSelectedBlueItems([]);
}
}, []);
Then I repeat myself and implement the exact same code to handle the RedItem
const RedSelection = ({ allRedItems, selectedRedItems, setSelectedRedItems }) => {
useEffect(() => {
setSelectedRedItems([]);
}
}, []);
Refactor
export default const Selection = () => {
const [allItems, setAllItems] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
return (
<Selection
allItems={allItems}
selectedItems={setSelectedItems}
setSelectedItems={selectedItems}
/>)}
import this wherever you need it like....
import Selection as RedSelection from ...
import Selection as BlueSelection from...
I am creating a React.js app which got 2 components - The main one is a container for the 2nd and is responsible for retrieving the information from a web api and then pass it to the child component which is responsible for displaying the info in a list of items. The displaying component is supposed to present a loading spinner while waiting for the data items from the parent component.
The problem is that when the app is loaded, I first get an empty list of items and then all of a sudden all the info is loaded to the list, without the spinner ever showing. I get a filter first in one of the useEffects and based on that info, I am bringing the items themselves.
The parent is doing something like this:
useEffect(() =>
{
async function getNames()
{
setIsLoading(true);
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
useEffect(() =>
{
async function getItems()
{
setIsLoading(true);
const items= await WebAPI.getItems(selectedName);
setAllItems(items);
setIsLoading(false);
};
getTenants();
},[selectedName]);
.
.
.
return (
<DisplayItems items={allItems} isLoading={isLoading} />
);
And the child components is doing something like this:
let spinner = props.isLoading ? <Spinner className="SpinnerStyle" /> : null; //please assume no issues in Spinner component
let items = props.items;
return (
{spinner}
{items}
)
I'm guessing that the problem is that the setEffect is asynchronous which is why the component is first loaded with isLoading false and then the entire action of setEffect is invoked before actually changing the state? Since I do assume here that I first set the isLoading and then there's a rerender and then we continue to the rest of the code on useEffect. I'm not sure how to do it correctly
The problem was with the asynchronicity when using mulitple useEffect. What I did to solve the issue was adding another spinner for the filters values I mentioned, and then the useEffect responsible for retrieving the values set is loading for that specific spinner, while the other for retrieving the items themselves set the isLoading for the main spinner of the items.
instead of doing it like you are I would slightly tweak it:
remove setIsLoading(true); from below
useEffect(() =>
{
async function getNames()
{
setIsLoading(true); //REMOVE THIS LINE
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
and have isLoading set to true in your initial state. that way, it's always going to show loading until you explicitly tell it not to. i.e. when you have got your data
also change the rendering to this:
let items = props.items;
return isLoading ? (
<Spinner className="SpinnerStyle" />
) : <div> {items} </div>
this is full example with loading :
const fakeApi = (name) =>
new Promise((resolve)=> {
setTimeout(() => {
resolve([{ name: "Mike", id: 1 }, { name: "James", id: 2 }].filter(item=>item.name===name));
}, 3000);
})
const getName =()=> new Promise((resolve)=> {
setTimeout(() => {
resolve("Mike");
}, 3000);
})
const Parent = () => {
const [name, setName] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const fetchData =(name) =>{
if(!loading) setLoading(true);
fakeApi(name).then(res=>
setData(res)
)
}
const fetchName = ()=>{
setLoading(true);
getName().then(res=> setName(res))
}
React.useEffect(() => {
fetchName();
}, []);
React.useEffect(() => {
if(name)fetchData(name);
}, [name]);
React.useEffect(() => {
if(data && loading) setLoading(false)
}, [data]);
return (
<div>
{loading
? "Loading..."
: data && data.map((d)=>(<Child key={d.id} {...d} />))}
</div>
);
};
const Child = ({ name,id }) =>(<div>{name} {id}</div>)
ReactDOM.render(<Parent/>,document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
My component roughly looks like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook("example-key", {ready: false});
return (
data.ready ?
<>
{console.log("data is ready");}
<ComponentA />
</> :
<>
{console.log("data is not ready");}
<ComponentB />
</>
)
}
useCustomHook is helper hook that pulls from AsyncStorage for my native application, so it has a slight delay. When I run this, I see the console logs "data is not ready" followed by "data is ready", but I only see ComponentB render, and never ComponentA.
If it's helpful, the custom hook looks like this. It basically just serializes the JSON into a string for storage.
export default (key, initialValue) => {
const [storedValue, setStoredValue] = React.useState(initialValue);
React.useEffect(() => {
const populateStoredValue = async () => {
const storedData = await AsyncStorage.getItem(key);
if (storedData !== null) {
setStoredValue(JSON.parse(storedData))
}
}
populateStoredValue()
}, [initialValue, key]);
const setValue = async (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
}
return [storedValue, setValue];
}
Anyone have ideas on what might be happening here?
Thanks!
small PS: I also see the warning Sending "onAnimatedValueUpdate" with no listeners registered., which seems like it's a react-navigation thing that's unrelated. But just wanted to put that here.
First of all, as your key param in custom hook is undefined so data will never be set. Pass the key to the custom hook as the prop.
Secondly, you need to update your condition to check if data is present or not, assuming ready property exist on data after set, like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook(/* Add key here */);
return (
data && data.ready ?
<>
console.log("data is ready");
<ComponentA />
</> :
<>
console.log("data is not ready");
<ComponentB />
</>
)
}