Set loading with React useEffect fails - javascript

I'm trying to show a loading spinner while fetching data from an api, and show error or results once finished using useEffect. For some reason isLoading is set to false before fetching is finished.
Could you please help me what could be the problem?
const ProblemTable: React.FC<IProblemTableProps> = (props) => {
// List of problems to show on the table
const [problems, setProblems] = useState<IProblem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error>();
useEffect(() => {
TierService.getProblems(props.tier, setProblems, setError);
}, []);
useEffect(() => {
setIsLoading(false);
}, [problems, error]);
if (isLoading) {
return <Loading message="Loading problems..." />;
}
if (error) {
return <ErrorMessage title="Failed fetching problems" />;
}
return <>...</>;

The useEffect that sets loading to false is called when the component mounts. Add a condition that would only turn loading to false when the api call ended.
For example - if there are any problems or there's an error set loading to false:
useEffect(() => {
if(problems.length || error) setIsLoading(false);
}, [problems, error]);
However, if there are no problems or an error, loading would stay true, so you'll probably need a more strict condition. A better way would be to set loading to false if any of the callbacks are called:
useEffect(() => {
TierService.getProblems(
props.tier,
p => {
setProblems(p);
setLoading(false);
}
e => {
setError(e);
setLoading(false);
}
);
}, []);
Depending on the implementation of TierService.getProblems the 2nd suggestion would fail if no callback is called when the response is empty.
I would probably create a Promise based API which would allow you to use async/await with try/catch/finally:
useEffect(() => {
const api = async () => {
try {
const problems = await TierService.getProblems(props.tier);
setProblems(problems);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};
api();
}, []);

Related

Prevent render before useEffect with search params

I have a component Browse that used to display search result requested from SearchBar component.
First search query when results state is null
Found some tracks now results state have array of tracks in
I make another search but changing the filter type from By Tracks to By Users (The results should have array of users objects)
I got an error because the render was called before the initResults() function so the results has always array of tracks object inside so I got error of undefined property
The output when I search for the first time :
> init useEffect
> render
The output when I searched another time inside the Browse Component :
> render
> init useEffect
How can I refresh the useEffect when search params change
Any idea ? Thank's !
Browse.js
export default function Browse() {
const [results, setResults] = UseSafeState(null)
const [isLoading, setIsLoading] = UseSafeState(true)
const location = useLocation()
const searchParams = new URLSearchParams(location.search)
const search = searchParams.get('search')
const identifierFilter = searchParams.get('identifier_filter')
const initResults = () => {
setIsLoading(true)
SearchAPI.search(search, identifierFilter)
.then((response) => {
setResults(response.data)
setIsLoading(false)
})
.catch((error) => {
setResults(null)
setIsLoading(false)
})
}
const renderTrackBrowse = () => {
return results.map((result, key) => <div key={key}>{result.track.name}</div>)
}
const renderUserBrowse = () => {
return results.map((result, key) => <div key={key}>{result.user.username}</div>)
}
const renderPage = () => {
console.log('render')
switch (identifierFilter) {
case FiltersTypesSearchBar.TRACKS.name:
return renderTrackBrowse()
case FiltersTypesSearchBar.ARTIST_NAME.name:
return renderUserBrowse()
default:
}
}
useEffect(() => {
console.log('init useEffect')
initResults()
}, [location, search, identifierFilter])
return <div className="main double-contained browse">{isLoading ? <Loader /> : renderPage()}</div>
}
Effects will always execute after the execution of the whole function. Even useLayoutEffect does not get invoked early enough to fix your issue. But with a tiny change, your code would already work.
const renderPage = () => {
console.log('render')
// do not continue execution if no results exist
if (!results) return;
switch (identifierFilter) {
case FiltersTypesSearchBar.TRACKS.name:
return renderTrackBrowse()
case FiltersTypesSearchBar.ARTIST_NAME.name:
return renderUserBrowse()
default:
}
}
Also one of your dependencies in the useEffect seems to be irrelevant and could cause more requests to be done as necessary. This should be sufficient.
useEffect(() => {
console.log('init useEffect')
initResults()
}, [search, identifierFilter])

Does order of state variable matters in React?

When I execute the code below gives an error "Cannot read properties of null (reading 'login')", because it reaches the return statement at the end, which it should not as I already have checks for the boolean before return.
import React, { useState, useEffect } from 'react';
const url = 'https://api.github.com/users/QuincyLarsn';
const MultipleReturns = () => {
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
fetch(url)
.then(data => {
if (data.status >= 200 && data.status <= 299)
return data.json();
else {
console.log("here");
setIsLoading(false);
setIsError(true);
console.log("here 2");
}
})
.then(result => {
setIsLoading(false);
setUser(result);
})
.catch(error => console.log(error))
}, []);
console.log(isError);
if (isLoading)
return <h2>Loading...</h2>
if (isError) {
return <h2>Error...</h2>
}
return <h2>{user.login}</h2>
};
export default MultipleReturns;
In the above code if setIsError(true) is placed before setIsLoading(false) in useEffect, then everything works fine but not vice versa, similarly if the url is correct then too things work fine if setUser(result) is placed before setIsLoading(false) and not vice versa.
I am not able to figure out why that is the case.
React is not batching state updates from fetch(). It is batched in case of event listeners. This is an async fetch call.
In this sandbox console, you can see that there is a render in between your state updates - setIsLoading(false) and setIsError(true).
So for one render cycle : isLoading is false and isError is also false. That will lead to the error condition.
You can use unstable_batchedUpdates to enforce batching.
import { useEffect, useState } from "react";
import { unstable_batchedUpdates } from "react-dom";
import "./styles.css";
const url = "https://api.github.com/users/QuincyLarsn";
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
fetch(url)
.then((data) => {
if (data.status >= 200 && data.status <= 299) return data.json();
else {
console.log("here");
unstable_batchedUpdates(() => {
setIsLoading(false);
setIsError(true);
});
console.log("here 2");
}
})
.then((result) => {
setIsLoading(false);
setUser(result);
})
.catch((error) => {
console.log(error);
});
}, []);
console.log("isError", isError);
if (isLoading) return <h2>Loading...</h2>;
if (isError) return <h2>Error...</h2>;
return <h2>{user.login}</h2>;
};
export default App;
Corrected Sandbox Link
In a such case, order does matter.
While React may batch updates in this case, it's not guaranteed and even if it does, it may call the render function with the in-between state.
So, when isLoading is set to false, but user is not yet set, you get an error.
You can fix this by setting the user first, and then making isLoading false.
But the real solution would be to eliminate the unnecessary state variables: isLoading is true while isError is false and user is null, and false otherwise.
So, you can do it like this:
should not as I already have checks for the boolean before return.
import React, { useState, useEffect } from 'react';
const url = 'https://api.github.com/users/QuincyLarsn';
const MultipleReturns = () => {
const [isError, setIsError] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
fetch(url)
.then(data => {
if (data.status >= 200 && data.status <= 299)
return data.json();
else {
console.log("here");
setIsError(true);
console.log("here 2");
}
})
.then(result => {
setUser(result);
})
.catch(error => console.log(error))
}, []);
console.log(isError);
if (isError) {
return <h2>Error...</h2>
}
if (user !== null) {
return <h2>{user.login}</h2>
}
return <h2>Loading...</h2>
};
export default MultipleReturns;
After you set isLoading as false, the code moves to the last return statement as the error is still false at the moment. So first setting the error blocks the code at the second return statement.
Similarly if you set isLoading as false then set the user, the code will move to the last return statement before the user is set and it will show error. Setting the user and then making isLoading as false shows the user perfectly.

Display no results component when there are no results

I'm trying to show the no results components whenever the api has finished loading and when no results are returned. The issue I'm having, is I am seeing the no results components displayed first for a few seconds and then the results showing whenever the api returns data. I should never want to see the no results component showing if there are results returned from the api.
import React, { useEffect, useState } from 'react';
import NoResults from './NoResults';
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
const isLoading = () => {
if (isResultsLoading) return <ResultsLoader />;
if (results && results.length > 0)
return (
<UserTableWrapper>
<UserTable
data={results}
/>
</UserTableWrapper>
);
return <NoResults heading="No users available." />;
};
useEffect(() => {
let isMounted = true;
const getData = async () => {
if (isMounted) {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) return { users, loading: false };
return { users: null, loading: true };
}
return { users: null, loading: true };
};
getData().then(({ users, loading }) => {
if (isMounted) {
if (users) setResults(users);
setIsResultsLoading(loading);
}
});
return () => {
isMounted = false;
};
}, []);
return (
<>
<h1>Users</h1>
{isLoading()}
</>
);
};
};
export default Users;
Check for the length of results with results.length since results already exists as an empty array.
When you get your data and parse it simply set both the states for results with the data, and isLoading to false.
Perhaps rename the isLoading function to something more representative of what the function does. I've called mine getJSX.
Here's a working example that uses a mock API. (Note I've had to use this without async and await because snippets haven't caught up with the latest Babel version.) You can change the JSON that's returned by the API by uncommenting/commenting out the JSON statements in the first couple of lines.
const {useState, useEffect} = React;
const json= '[1, 2, 3, 4]';
// const json = '[]';
function mockApi() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 3000);
});
}
function Example() {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
function getJSX() {
if (isResultsLoading) return <div>Loading</div>;
if (results.length) {
return results.map(el => <div>{el}</div>);
}
return <div>No users available.</div>;
};
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => {
setResults(data);
setIsResultsLoading(false);
});
}, []);
return <div>{getJSX()}</div>
};
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You should rely on conditional rendering and simplify your logic a little bit.
import React, { useEffect, useState } from "react";
import NoResults from "./NoResults";
const Users = () => {
// This holds the results - default to null till we get a successful API response
const [results, setResults] = useState(null);
// This should be a boolean stating if the API call is pending
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
const getData = async () => {
const users = await fetchUsers(); // is just an api call
if (users && users.length > 0) {
// if the result is good, store it
setResults(users);
}
// By the way, the api call is finished now
setIsResultsLoading(false);
};
getData();
}, []); // no deps => this effect will run just once, when the component mounts
if (isResultsLoading) {
// Render nothing while API call is pending
return null;
} else {
if (results) {
// The API has returned a good result, so render it!
return (
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
);
} else {
// No good result, render the fallback component
return <NoResults heading="No users available." />;
}
}
};
export default Users;
You've a lot of extraneous conditionals and code duplication (not as DRY as it could be). Try cutting down on the user checks before you've even updated state, and you likely don't need the mounted check. Conditionally render the UI in the return.
const Users = () => {
const [results, setResults] = useState([]);
const [isResultsLoading, setIsResultsLoading] = useState(true);
useEffect(() => {
fetchUsers() // is just an api call
.then(users => {
setResults(users);
})
.catch(error => {
// handle any errors, etc...
})
.finally(() => {
setIsResultsLoading(false); // <-- clear loading state regardless of success/failure
});
}, []);
return (
<>
<h1>Users</h1>
{isResultsLoading ? (
<ResultsLoader />
) : results.length ? ( // <-- any non-zero length is truthy
<UserTableWrapper>
<UserTable data={results} />
</UserTableWrapper>
) : (
<NoResults heading="No users available." />
)}
</>
);
};

React-Native infinite loop

I am trying to get data from my firebase-firestore I an showing a loading state to wait for the data to load however when it does load it keeps returning the firestore data infinite times. Please may someone help me.
This is my code Paper is just a custom component
import Paper from '../Components/Paper'
import firebase from 'firebase'
import { useState } from 'react'
const Home = (props) => {
const renderMealItem = (itemData) =>{
return (
<Paper
title={itemData.item.name}
serves={itemData.item.servings}
time={itemData.item.time}
image={itemData.item.imageUri}
/>
)
}
const [loading, setLoading] = useState(false)
const [all, setAll] = useState([])
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
},[])
const checkReturn = () => {
if(all !== undefined){
setLoading(false)
}
}
const getUser = async() => {
try {
await firebase.firestore()
.collection('Home')
.get()
.then(querySnapshot => {
querySnapshot.docs.forEach(doc => {
setAll(JSON.stringify(doc.data()));
});
});
}catch(err){
console.log(err)
}
}
return(
<View style={styles.flatContainer}>
<FlatList
data={all}
keyExtractor={(item, index) => index.toString()}
renderItem={renderMealItem}/>
</View>
)
}
useEffect without second parameter will get executes on each update.
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
})
so this will set the loading and tries to get the user. and when the data comess from server, it will get runned again.
So you should change it to : useEffect(() => {...}, []) to only get executed on mount phase(start).
Update: you should check for return on every update, not only at start. so change the code to:
useEffect(() => {
setLoading(true)
getUser()
}, [])
useEffect(() => {
checkReturn()
})
Ps: there is another issue with your code as well:
querySnapshot.docs.forEach(doc => {
setAll(JSON.stringify(doc.data()));
});
maybe it should be like :
setAll(querySnapshot.docs.map(doc => JSON.stringify(doc.data())));
Try passing an empty array as an argument to useEffect like so:
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
}, [])

How to use setTimeout() along with React Hooks useEffect and setState?

I want to wait for 10 seconds for my API call to fetch the category list array from backend and store in hook state. If nothing is fetched in 10 sec, I want to set error hook state to true.
But the problem is even after the array is fetched initially, the error state is set to true and categoriesList array in state blanks out after 10 sec.
import React, { useState, useEffect } from "react";
import { doGetAllCategories } from "../helper/adminapicall.js";
const ViewCategories = () => {
let [values, setValues] = useState({
categoriesList: "",
error: false,
});
let { categoriesList, error } = values;
const preloadCategories = () => {
doGetAllCategories()
.then((data) => {
if (data.error) {
return console.log("from preload call data - ", data.error);
}
setValues({ ...values, categoriesList: data.categories });
})
.catch((err) => {
console.log("from preload - ", err);
});
};
useEffect(() => {
preloadCategories();
let timerFunc = setTimeout(() => {
if (!categoriesList && !error) {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}
}, 10000);
return () => {
clearTimeout(timerFunc);
};
}, []);
//...further code
The problem is that the useEffect callback is a closure over categoriesList, so you'll always see the initial categories list inside the callback, and you won't see any changes to it. Now one could add categoriesList as a dependency to the useEffect hook, that way the hook will be recreated on every categoriesList change, and thus you can see the changed version:
useEffect(/*...*/, [categoriesList]);
Now the good thing is, that by updating the hook the timeout also gets canceled, so if the category list is set, we just don't have to create a new timeout:
useEffect(() => {
if(!categoriesList && !error) {
let timerFunc = setTimeout(() => {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}, 10000);
return () => clearTimeout(timerFunc);
}
}, [!categoriesList, !error]); // some minor optimization, changes to the list don't bother this hook
I recommend you to read this blog post on useEffect by Dan Abramov.
The problem with your code is that you expect to have a change of the state of the component inside the useEffect hook. Instead, you create two variables inside the useEffect that track whether the limit of 10 sec has passed or the data is fetched. In contrary to state variables, you can expect these variables to change because they lie within the same useEffect.
export default function App() {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let didCancel = false;
let finished = false;
async function fetchData() {
const data = await subscribeAPI();
if (!didCancel) {
finished = true;
setData(data);
}
}
const id = setTimeout(() => {
didCancel = true;
if (!finished) {
setError("Errorrrr");
}
}, 10000);
fetchData();
return () => {
clearTimeout(id);
};
}, []);

Categories

Resources