How do I set initial state to data fetched with useQuery? - javascript

I'm working with React and Apollo and I'm trying to initialize state with data fetched using useQuery hook but I'm getting "Cannot read property 'map' of undefined" when loading the page.
const { loading, error, data } = useQuery(FETCH_BLOGS);
const [state, setState] = useState(undefined);
useEffect(() => {
if (loading === false && data) {
setState(data.blogs);
}
}, [loading, data]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
In the JSX I'm calling {renderBlogs(state)} which maps over the array and is where the error is being thrown. If I pass in the initial data {renderBlogs(data.blogs)} it works but I need to store the data in state as it will be mutated.
When I console.log(state) it logs 2 lines:
The first is undefined.
The second is the array of blogs (as shown in the screenshot).
It appears that the page is trying to render the initial state (undefined) before before the state is set to the query data. Is this the case? I thought using useEffect would solve this but that doesn't seem to be the case. Any help is appreciated, thank you.

To quote the useEffect documentation:
The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
The problem is that useEffect will trigger after a render. This will result in the following chain of events. Assume loading is just set to false and there where no errors fetching the data.
When the component is now being rendered both the if (loading) and if (error) guard clauses will be skipped, because the data successfully loaded. However state is not yet updated because the useEffect callback will trigger after the current render. At this point when you call renderBlogs(state) state will still be undefined. This will trow the error you describe.
I might be missing some context, but from what is shown in the question there is no reason to use useEffect. Instead use data directly.
The example provided by Apollo for useQuery provides a good starting point:
import { gql, useQuery } from '#apollo/client';
const GET_GREETING = gql`
query GetGreeting($language: String!) {
greeting(language: $language) {
message
}
}
`;
function Hello() {
const { loading, error, data } = useQuery(GET_GREETING, {
variables: { language: 'english' },
});
if (loading) return <p>Loading ...</p>;
return <h1>Hello {data.greeting.message}!</h1>;
}
Applying the example to your scenario it might look very similar.
const { loading, error, data } = useQuery(FETCH_BLOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return renderBlogs(data.blogs);
If you need the setter you might want to provide a bit more context to the question. However a common reason to have a setter might be if you want to apply some sort of filter. A simple solution would be to have a computed blogs state.
const filteredBlogs = data.blogs.filter(blog => blog.title.includes(search));
Here search would be the state of some <input value={search} onChange={handleSearchChange} /> element. To improve performance you could also opt to use useMemo here.
const filteredBlogs = useMemo(() => (
data.blogs.filter(blog => blog.title.includes(search))
), [data.blogs, search]);
If you really need to use useState for some reason, you could try the onCompleted option of useQuery instead of useEffect.
const [blogs, setBlogs] = useState();
const { loading, error } = useQuery(FETCH_BLOGS, {
onCompleted: data => setBlogs(data.blogs),
});

Related

Can't loop throught State on React JS with map() method

I'm having a problem mapping through an array with objects, and I can't find what problem is, but I asume its because of async, but I want you to take a look at it.
I'm getting two error messages and I don't know if they relate:
TypeError: Cannot read property 'map' of null
1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
import {useState, useEffect} from 'react';
// import styled from 'styled-components';
export default function Admin() {
const [quotes, setQuotes] = useState(null);
const get_all_quotes = async () => {
const {data, error} = await supabase
.from('quotes_en')
.select('quote')
console.log(data);
if (error) console.table(error)
setQuotes(data)
}
useEffect(() => {
get_all_quotes()
}, [])
return (
<div>
{quotes.map(({id, quote}) => {
return <p key={id}>{quote}</p>
})
}
</div>
)
}
Issue
The initial quotes state value is null, so it can't be mapped.
const [quotes, setQuotes] = useState(null);
Solution
Provide valid initial state, I suggest using an empty array ([]).
const [quotes, setQuotes] = useState([]);
Now you'll have valid quotes state that can be mapped on the initial render. Array.prototype.map can safely handle empty arrays.
{quotes.map(({id, quote}) => {
return <p key={id}>{quote}</p>
})}
As #DrewReese said you can either set the initial value of state as an empty array or choose to show quotes conditionally using:
{quotes && quotes.map(({id, quote}) => {
return <p key={id}>{quote}</p>
})
What this code does is that it will check whether any value is there for quotes and if it's there only, it will call the quotes.map() function.
Update:
We can also use optional chaining, which is more readable than conditional (short-circuiting as in above).
{quotes?.map(({id, quote}) => {
return <p key={id}>{quote}</p>
})
What the above code does is that it checks whether quotes are there and if it's there only, it will call the map function. If "quotes" is undefined, or null or any other falsy value, it will return the falsy value and stops execution.
The advantage of these solutions is that even if, in any case, quotes are not having an array it will not cause any issue, but won't execute that code.
Thanks to #DrewReese for this solution.

CoinGecko API data undefined after re-render React JS

Trying to render data from the CoinGekco API in my React component. It works on first render but if I leave the page or refresh, coin.market_data is undefined. I also tried passing coin to the useEffect() dependency array and that didn't work.
import React, { useEffect, useState } from "react";
import axios from "../utils/axios";
import CoinDetail from "./CoinDetail";
function CoinPagePage() {
const [coin, setCoin] = useState({});
useEffect(() => {
const getCoin = () => {
const coinid = window.location.pathname.split("/").splice(2).toString();
axios
.get(`/coins/${coinid}`)
.then((res) => {
setCoin(res.data);
console.log(res.data);
})
.catch((error) => console.log(error));
};
getCoin();
}, []);
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
}
export default CoinPagePage;
The GET request only happens when rendering the parent page. Re-rendering the child component will not run the fetch code again. Instead of passing current_price as a prop to your <CoinDetail> component, you could try passing coinid and doing the fetch inside your detail page.
That way, when the page is refreshed, the request will be executed again.
Edit
If you try to access a not existing property on an object, your application will crash. What you could do to prevent this from happening is checking if the request is done, before trying to access the property.
One way you could do this by setting the initial state value to null
const [coin, setCoin] = useState(null);
Then, above the main return, you could check if the value is null, if it is, return some sort of loading screen
if(coin === null) return <LoadingScreen />;
// main render
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
This way, when the fetch is done, the state gets updated and the page will re-render and show the updated content.

Returning a component from a map function

I want to create a function that maps throw a firestore collection and return a Component for each document in the collection I have tried to use the code below
<div className="posts">
{
db.collection("posts")
.get()
.then((snapshot) => {
snapshot.docs.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />
))})}
</div>
but this error shows:
Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.ode
JSX is not the place to make Ajax calls. Instead make the call inside componentDidMount/useEffect and set the data received inside state. As soon as state is updated with received data, React will automatically re-render the component and display the expected content. Try something as follows:
const [snapshots, setSnapshots] = useState();
useEffect(()=> {
db.collection("posts")
.get()
.then((snapshot) => {
setSnapshots(snapshot.docs)
}
)
}, []);
render
<div className="posts">
{snapshots && snapshots.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />
)
}
</div>
Since you haven't given a hint as to whether you're using hooks or not, here is how you should deal with asynchronous stuff and setting states in React (using hooks) :-
function MyComponent () {
const [snapshot,setSnapshot] = useState();
useEffect(()=>{
async function getPosts(){
let snapshot = await db.collection("posts").get();
setSnapshot(snapshot);
}
getPosts();
},[])
return
(<div className="posts">
snapshot?.docs?.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />)
</div>)
}
What you are doing is returning Promise object as the error states. That async stuff is taken care either inside a useEffect,event handler or a custom-hook. There could be more ways but this is a general point to start.
As the error message states Promise is not a valid React child. To solve that issue you can introduce a state which keeps the result of db.collection('posts').get() method. Then iterating through on that array with your .map() which will be a valid React child.
See a possible solution for your scenario:
// in the root of you component, introduce a state
const [posts, setPosts] = useState([])
// here in useEffect you can do your API call and update the state
useEffect(() => {
db.collection("posts")
.get()
.then((snapshot) => {
setPosts(snapshot.docs)
})
}, [])
// ... rest of your component's code
return (
<div className="posts">
{
posts && posts.map((doc, i) => (
<div key={i}>
doc.data().title
</div>
// here you can use <PostContect /> component
// either doc.data().title or doc.title to get the value
))
}
</div>
)
Suggested reads:
Using the Effect Hook
Using the State Hook
The code you wrote returns a promise, which can't be rendered by react. You can use conditional rendering to return something else while the data is being fetched.
Check this question for more info

useSWR() and mutate() do not behave as expected when component is unmounted

I am having an issue with some unexpected behavior with regards to mutate(key). In my data fetching code, I have these hooks/functions:
const recentOptions = {
refreshInterval: 0,
revalidateOnFocus: false,
dedupingInterval: 180000000 //or 500
}
export const useRecent = () => {
const {data, error} = useSWR(
`${config.apiUrl}/GetRecentFoundations`,
getThenResolve,
recentOptions
);
return {
recent: data,
isLoading: !error && !data,
isError: error
};
}
export const setRecent = async(fid) => {
await postThenResolve(
`${config.apiUrl}/SetFoundationRecent`,
{fid});
//TODO: So the behavior I am seeing tends to indicate that
//mutate(key) sends out a message to swr to refetch, but doesn't
//necessarily cause the cache to be invalidated.
//This means that mutate will cause any CURRENTLY MOUNTED useSWR()
//to refetch and re-render, but ones that aren't mounted will
//return with stale data.
mutate(`${config.apiUrl}/GetRecentFoundations`);
}
I also have a component that fetches data with useRecent():
const FoundationRecents = props => {
const {recent, isLoading} = useRecent();
if(isLoading) return <div>...LOADING...</div>;
if(!recent) return null;
return <SimpleCard
titlebg={colors.blue}
titlefg={colors.white}
titlesz='small'
title='Recent'
contentbg={colors.white}
component={<FoundationRecentsView recent={recent}/>}
/>
}
I use this pattern in two places currently, one where the component is mounted when the setRecent() occurs. The other, where the component is unmounted. When the component is mounted, everything works fine. When mutate() is called, everything seems to refetch and rerender.
However, if setRecent() is called when the component is unmounted, and I later return to it, I get stale data, and no refetch.
I think I must be misunderstanding what mutate(key) does, or maybe I am unclear as to what the dedupingInterval is. I thought, that even with a high deduping interval, the mutate would cause the GetRecentFoundations cache to be invalid, and thus the next call to useSWR() would require a revalidate.

In React, failing to stop Axios request when component unmounts

With data fetching in React, the following is a common warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in ParentComponent
I've read multiple posts and suggestions on how to handle this, and none are working currently.
For this, we have the function useAxiosApi which fetches data asynchronously, and ParentComponent which is the component that uses the useAxiosApi() and needs the data. ParentComponent is the component being unmounted / being referenced in the warnings.
Parent Component
import useAxiosApi...
function ParentComponent({ info }) {
const dataConfig = { season: info.season, scope: info.scope };
const [data, isLoading1, isError1] = useAxiosApi('this-endpoint', [], dataConfig);
return (
{isLoading && <p>We are loading...</p>}
{!isLoading &&
... use the data to render something...
}
)
}
useAxiosApi
import axios from 'axios';
import { useState } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
const resources = {};
const useAxiosApi = (endpoint, initialValue, config) => {
// Set Data-Fetching State
const [data, setData] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// For Live Search on keystroke, Save Fetches and Skip Fetch if Already Made
if (endpoint === 'liveSearch' && resources[config.searchText]) {
return [resources[config.searchText], false, false];
}
// Otherwise, Continue Forward
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
console.log('isMounted: ', isMounted);
if (isMounted) {
setData(result.data);
}
// If LiveSearch, store the response to "resources"
if (endpoint === 'liveSearch') {
resources[config.searchText] = result.data;
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
// Call Function
fetchData();
// Cancel Request if needed in cleanup function
return () => {
console.log('Unmount or New Search? About to call source.cancel()');
isMounted = false; // is this doing its job?
source.cancel();
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
export default useAxiosApi;
createUrl is simply a function that takes the endpoint and dataConfig and creates the url that axios will fetch from. Note that our cancelTokens seem to be working in conjunction with the Live search, as new searches are cancelling the old search queries, and the saving of data results into resources for the one specific endpoint liveSearch works as well.
However, our problem is that when ParentComponent is unmounted quickly, before the data fetch is complete, we still receive the Cant perform a React state update warning. I've checked the console.logs(), and console.log('isMounted: ', isMounted) is always returning true, even if we unmount the component quickly after it is mounted / before data fetching is complete.
We're at a loss on this, as using the isMounted variable is the way that I've seen this problem handled before. Perhaps there's a problem with the useDeepCompareEffect hook? Or maybe we're missing something else.
Edit: Weve also tried to create the isMounted variable from inside of ParentComponent, and pass that as a parameter into the useAxiosApi function, however this did not work for us either... In general, it would be much better if we can handle this warning via an update to our useAxiosApi function, as opposed to in the ParentComponent.
Edit2: It seems like the cancelToken only works when a duplicate API call is fired off to the same endpoint. This is good for our liveSearch, however it means that all of the other fetches are not cancelled.

Categories

Resources