using useEffect in combination with Axios fetching API data returns null - how to deal with this? - reactjs

Fetching data using Axios and useEffect results in null before the actual object is loaded.
Unfortunately, I am not able to use object destructuring before the actual object is not empty.
I am forced to use a hook to check whether an object is empty or not.
For instance, I would like to create multiple functions and split my code up in separate functions for better readability.
This is my HTTP request Hook:
import { useState, useEffect } from 'react';
import axios from 'axios';
export const useHttp = (url, dependencies) => {
const [isLoading, setIsLoading] = useState(false);
const [fetchedData, setFetchedData] = useState(null);
useEffect(() => {
setIsLoading(true);
axios
.get(url)
.then(response => {
setIsLoading(false);
setFetchedData(response.data);
})
.catch(error => {
console.error('Oops!', error);
setIsLoading(false);
});
}, dependencies);
return [isLoading, fetchedData];
};
Followed by my page component:
import React from 'react';
import { PAGE_ABOUT, API_URL } from 'constants/import';
import Header from './sections/Header';
import Main from './sections/Main';
import Aside from './sections/Aside';
import { useHttp } from 'hooks/http';
const About = () => {
const [isLoading, about] = useHttp(PAGE_ABOUT, []);
if (!isLoading && about) {
return (
<section className="about">
<div className="row">
<Header
featuredImage={API_URL + about.page_featured_image.path}
authorImage={API_URL + about.page_author_image.path}
authorImageMeta={about.page_author_image.meta.title}
title={about.about_title}
subtitle={about.about_subtitle}
/>
<Main
title={about.page_title}
content={about.page_content}
/>
<Aside
title={about.about_title}
content={about.about_content}
/>
</div>
</section>
);
}
};
export default React.memo(About);
The actual problem I am not able to nest functions before the object is actually returned.
Is there a way to fetch data without a check by any chance? or a cleaner solution would help.
I would like to use multiple components to split up the code.
Any advice or suggestion would be highly appreciated.

What you are asking would be bad for UIs. You don't want to block the UI rendering when you're fetching data. So the common practice is showing a Loading spinner or (if you're counting on the request being fast) just rendering nothing until it pops up.
So you would have something like:
const About = () => {
const [isLoading, about] = useHttp(PAGE_ABOUT, []);
if (isLoading) return null; // or <Loading />
return (
<section className="about">
<div className="row">
<Header
featuredImage={API_URL + about.page_featured_image.path}
authorImage={API_URL + about.page_author_image.path}
authorImageMeta={about.page_author_image.meta.title}
title={about.about_title}
subtitle={about.about_subtitle}
/>
<Main
title={about.page_title}
content={about.page_content}
/>
<Aside
title={about.about_title}
content={about.about_content}
/>
</div>
</section>
);
};
If your api has no protection for errors and you're afraid of about being null or undefined you can wrap the component with an Error Boundary Component and show a default error. But that depends if you use those in your app.
Error boundaries are React components that catch JavaScript errors
anywhere in their child component tree, log those errors, and display
a fallback UI instead of the component tree that crashed. Error
boundaries catch errors during rendering, in lifecycle methods, and in
constructors of the whole tree below them.

You are not returnig any if your condition in our About component is false so add a case if isLoading or !about you could add a loader maybe:
const About = () => {
const [isLoading, about] = useHttp(PAGE_ABOUT, []);
let conditionalComponent = null;
if (!isLoading && about) {
conditionalComponent= (
<section className="about">
<div className="row">
<Header
featuredImage={API_URL + about.page_featured_image.path}
authorImage={API_URL + about.page_author_image.path}
authorImageMeta={about.page_author_image.meta.title}
title={about.about_title}
subtitle={about.about_subtitle}
/>
<Main
title={about.page_title}
content={about.page_content}
/>
<Aside
title={about.about_title}
content={about.about_content}
/>
</div>
</section>
);
}
return conditionalComponent
};

As I recognize you want to get rid of condition if (!isLoading && about) {
You can make it in several ways:
1) You can create Conditional component by your own. This component should receive children prop as a function and should apply other props to this function:
function Conditional({ children, ...other }) {
if (Object.entries(other).some(([key, value]) => !value)) {
return "empty"; // null or some `<Loading />` component
}
return children(other);
}
Also I made a small demo https://codesandbox.io/s/upbeat-cori-ngenk
It helps you to create only the one place for condition and reuse it in dozens of components :)
2) You can use some library, which is built on a top of React.lazy and React.Suspense (but it wouldn't work on a server-side).
For example https://dev.to/charlesstover/react-suspense-with-the-fetch-api-374j and a library: https://github.com/CharlesStover/fetch-suspense

Related

Does it consider bad if a react component without any state is defined and rendered inside another react component?

I know that we shouldn't define and render a react component inside another react component because it's going to mess up the states of those components.
But for the example below, I'm wondering if there are any potential issues.
export default function Post() {
const { number } = useParams();
const headerQuery = useHeader(number);
const bodyQuery = useBody(number);
const Header = () => {
if (headerQuery.isLoading) {
return <p>Loading...</p>;
}
// Assume that headerQuery.data here is just a simple string
return <div>{headerQuery.data}</div>;
};
const Body = () => {
if (bodyQuery.isLoading) {
return <p>Loading...</p>;
}
return <div>{bodyQuery.data}</div>
};
return (
<div>
{Header()}
{Body()}
{/* or
<Header />
<Body />
*/}
</div>
);
}
Because those Header and Body components don't have any states so I think it's the same as below:
export default function Post() {
const { number } = useParams();
const headerQuery = useHeader(number);
const bodyQuery = useBody(number);
const header = headerQuery.isLoading ? <p>Loading...</p> : <div>{headerQuery.data}</div>
const body = bodyQuery.isLoading ? <p>Loading...</p> : <div>{bodyQuery.dat}</div>
return (
<div>
{header}
{body}
</div>
);
}
The reason I want to define header and body as components as in the first code snippet is because I don't want to use more than one ternary operator if I have to handle more than the loading state (isLoading ? isSomething ? <h1>...<h1> : <h2>...<h2> : <div>...<div>).
I can absolutely move header and body out of post but if they're nested inside post, are there any issues?
It's perfectly OK to have react components that contain other react components. The contained components will only re-render if they rely on state that has changed, and even if they do re-render - that just updates he virtual dom which isn't free but is pretty light weight.
Each component can have its own state. That is OK and how things work. What you cannot do is introduce "conditional state" into a component. i.e. all your useXXXXX code should be at the top of your component and not inside a function you call or within an if statement, etc.
You absolutely want to use components, e.g. <Header /> and not {Header()).
I recommend watching this video to learn about the differences between <Component /> and {Component()}.

Hook requires data from another hook, but getting Error: Rendered more hooks than during the previous render

I'm getting Error:
Rendered more hooks than during the previous render.
I found some answers saying I should put all hook calls on the top.
However, in the following code, session prints undefined 3 times while it's loading until it prints the session object the fourth time.
Hence, I added a check for the loading state.
import { signIn, signOut, useSession } from "next-auth/client";
import { request } from "graphql-request";
import useSWR from "swr";
export default function Profile() {
const [session, loading] = useSession();
if (loading) {
return <p className="">loading...</p>;
}
if (!session) {
signIn();
}
const { data: user } = useSWR([USER_QUERY, session.email], (query, email) =>
request("/api/graphql", query, { email })
);
return (
<div className="">
<p className="">{session.user.nickname}</p>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
However, this is throwing an error.
A workaround for this particular issue is to define a NextAuth callback for session, but I don't think that's the correct way to fix it, since I definitely will need to make other hook calls based on the value in the session object in the future.
How can I fix this?
Because hooks are tracked by array index internally by React, you can't invoke them conditionally. You need the same hook calls in the same order each time a given component renders.
You could move the useSWR and subsequent markup to a separate component to avoid the conditional hook calls.
In the example below I've moved the signIn call to a separate component too, but that's not strictly necessary. If you wanted to leave that inline you could just return null instead.
export default function Profile() {
const [session, loading] = useSession();
if (loading) {
return <p className="">loading...</p>;
}
return !session ? <SignIn /> : <UserStuff />
}
function SignIn () {
signIn();
return null;
}
function UserStuff () {
const { data: user } = useSWR([USER_QUERY, session.email], (query, email) =>
request("/api/graphql", query, { email })
);
return (
<div className="">
<p className="">{session.user.nickname}</p>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}

Data from useRef renders only after changing the code and saving it

I'm having an issue with my react app. I retrieve data from my elasticsearch server and trying to display it on the website.
const RecipesPage = (props: Props) => {
const recipes = useRef<Recipe[]>([]);
const avCategories = ['meats', 'pastas', 'vegan', 'seafood', 'desserts', 'all'];
const currentCategory = props.match.params.category_name.toLowerCase();
useEffect(() => {
const recipesReq = getRecipesByCategory(currentCategory);
recipesReq
.then((data) => recipes.current = data.hits.hits)
}, [currentCategory])
if (avCategories.includes(currentCategory)) {
return (
<div>
<Navbar />
<ul style={{marginTop: "5.5rem"}}>{recipes.current.map((recipe: Recipe) => <p key={recipe._id}>{recipe._source.recipe_name}</p>)}</ul>
</div>
);
} else {
return (
<div>
<Navbar />
<p style={{marginTop: "5.5rem"}}>No category named {currentCategory}</p>
</div>
);
}
};
export default RecipesPage
The problem is that when I'm trying to display the data it shows up only after saving the code and then after refreshing the page it's gone. I guess it's a problem related to useRef hook, but I'm not sure.
You should use state if you need the component to rerender.
When using useEffect, you shouldn't pass an array or object reference as a dependency. React uses referential comparison to check for changes, which means the useEffect hook will run every time a new object/array is created regardless if the actual data changes, which can cause an infinite render loop:
https://www.benmvp.com/blog/object-array-dependencies-react-useEffect-hook/

React Component is rendering twice

I have no idea why, the first render shows an empty object and the second shows my data:
function RecipeList(props) {
return (
<div>
{console.log(props.recipes)}
{/*{props.recipes.hits.map(r => (*/}
{/* <Recipe initial="lb" title={r.recipe.label} date={'1 Hour Ago'}/>*/}
</div>
)
}
const RECIPES_URL = 'http://cors-anywhere.herokuapp.com/http://test-es.edamam.com/search?i?app_id=426&q=chicken&to=10'
export default function App() {
const classes = useStyles();
const [data, setData] = useState({});
useEffect(() => {
axios.get(RECIPES_URL)
.then(res => {
setData(res.data);
})
.catch(err => {
console.log(err)
})
}, []);
return (
<div className={classes.root}>
<NavBar/>
<RecipeList recipes={data}/>
<Footer/>
</div>
);
}
I don't know why and I have struggled here for over an hour (React newbie), so I must be missing something.
This is the expected behavior. The reason you see two console logs is because, the first time RecipeList is called with no data (empty object), and the second time when the data becomes available. If you would like to render it only when the data is available you could do something like {Object.keys(data).length > 0 && <RecipeList recipes={data}/>}. By the way this is called conditional rendering.
This is perfectly normal, React will render your component first with no data. Then when your axios.get returns and update data, it will be rendered again with the new data

Skipping Effects does not work for Array of dynamic URL's

I have a React.SFC / react stateless / functional component which is unfortunately rendering a little too frequent due to some excess data coming in from redux in a parent component. Nothing I can do about that for now, so I'm just accepting the extra rerenders, and using useEffect to make sure data is only fetched whenever a certain property changes. In this case its called "urls" and it is an array of URL's (TypeScript URL Type).
Here's some example code illustrating the issue:
import React from "react";
import { useState, useEffect, useMemo } from "react";
import { render } from "react-dom";
import "./styles.css";
const useCustomHook = urls => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return onlyChangeWhenUrlsChange;
};
const dynamicUrls = (pageRouteParamId, someDynamicUrlParam) => {
return [
{
pageRouteParamId: 1337,
urls: [new URL(`https://someurl.com/api?id=${someDynamicUrlParam}`)]
}
];
};
const SomePage: React.SFC<any> = ({
simulateFrequentUpdatingData,
pageRouteParamId
}) => {
const someOtherId = 1;
// As suggested in SO answer, using useMemo seems to work, but will that not create a memory leak?
// Is there any good alternative?
// const urls = useMemo(() => dynamicUrls(pageRouteParamId, someOtherId).find(url => url.pageRouteParamId === pageRouteParamId).urls, [pageRouteParamId, someOtherId]);
const urls = dynamicUrls(pageRouteParamId, 1).find(
url => url.pageRouteParamId === 1337
).urls;
return (
<div>
<p>parent</p>
<p>{simulateFrequentUpdatingData}</p>
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && <MyStateLessFunctionalComponent {...{ urls }} />}
<p>
Page route param id (in real app this would come from react-router route
param): {pageRouteParamId}
</p>
{urls && urls.length && (
<MyStateLessFunctionalComponentWithHook {...{ urls }} />
)}
</div>
);
};
const MyStateLessFunctionalComponent: React.SFC<any> = ({ urls }) => {
const [onlyChangeWhenUrlsChange, setOnlyChangeWhenUrlsChange] = useState(
null
);
useEffect(
() => {
setOnlyChangeWhenUrlsChange(Math.random());
},
[urls]
);
return (
<div>
<p>MyStateLessFunctionalComponent</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
const MyStateLessFunctionalComponentWithHook: React.SFC<any> = ({ urls }) => {
const onlyChangeWhenUrlsChange = useCustomHook(urls);
return (
<div>
<p>MyStateLessFunctionalComponentWithHook</p>
<p>{JSON.stringify(urls)}</p>
<p>This should only change when urls change {onlyChangeWhenUrlsChange}</p>
</div>
);
};
function App() {
const [
simulateFrequentUpdatingData,
setSimulateFrequentUpdatingData
] = useState(null);
const [pageRouteParamId, setPageRouteParamId] = useState(1337);
useEffect(() => {
setInterval(() => setSimulateFrequentUpdatingData(Math.random()), 1000);
}, []);
return (
<div className="App">
<SomePage {...{ simulateFrequentUpdatingData, pageRouteParamId }} />
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Edit:
I had to change the title and question, since while reproducing it with the example code I realized the problem was not about "Skipping Effects inside a custom hook". Before I though I saw a difference when using useEffect directly vs inside a custom hook, and as the comments rightfully mentioned, there should not be any difference - and I came to the same conclusion while reproducing my issue with this sample code:
You can check out a live example here.
As it was suggested in the answer below, it seems like useMemo solves the issue (see line 36)
My guess is that urls is being declared inside a render higher up the tree, and thus getting a new identity every time. You can either useMemo on the place where it is being declared, JSON.stringify the urls in the deps-array, or a useRef which works as an additional guard against re-runs.
Edit: This is being discussed by smarter people than me: https://github.com/facebook/react/issues/14476#issuecomment-471199055.
If urls is an array of strings you can pass that as the second argument to useEffect
useEffect(() => {
loadData();
}, urls);
that way it will check the string values instead of the array reference.

Resources