React Suspense is not working as intended - reactjs

I want to render fallback component when my Powers is being fetched/undefined. I implemented React.Suspense in my logic using the code:
<Suspense fallback={<p>Loading</p>}>
<RoutesPowers />
</Suspense>
and my RoutesPowers is
const Powers = [ ... ];
const RoutesPowers = () => {
const [powers, setPowers] = useState(null);
const fetchPowers = () => setTimeout(() => setPowers(Powers), 3000);
useEffect(() => fetchPowers(), []);
return ( powers.map(route => <RoutePowers route={route}/> )
};
but it gives me Cannot read property "map" of null probably because powers is null. That means that React.Suspense isn't working as it should. Can anybody help me on this?

For suspense to have any effect, a component farther down the tree needs to throw a promise. When that happens, suspense will catch it and display the fallback until the promise resolves, and then it resumes rendering its normal children. Your code doesn't throw any promises, so suspense doesn't do anything.
So if you want to use suspense for this, you need to have your component throw a promise during rendernig if it detects it doesn't have the data. Then do your fetching, and save data such that when the component mounts again it will have the data (you can't set state, because the component doesn't exist during this time, the fallback does).
const App = () => (
<React.Suspense fallback={<p>Loading</p>}>
<RoutesPowers />
</React.Suspense>
)
let savedPowers = null;
const fetchPowers = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
savedPowers = ['first', 'second']
resolve();
}, 3000)
});
}
const RoutesPowers = () => {
const [powers, setPowers] = React.useState(savedPowers);
if (!powers) {
throw fetchPowers();
}
return powers.map(value => <div key={value}>{value}</div>);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.js"></script>
<div id="root"></div>
Be aware that it's uncommon to use this pattern. More common is to handle it all in the one component, rendering a placeholder if there is no data, then kicking off the fetch in an effect, then updating state when it's done. Your current code is almost there, you'd just want to delete the <Suspense> and add code inside RoutesPowers to return the loading paragraph.
const RoutesPowers = () => {
const [powers, setPowers] = useState(null);
const fetchPowers = () => setTimeout(() => setPowers(Powers), 3000);
useEffect(() => fetchPowers(), []);
if (!powers) {
return <p>loading</p>
}
return ( powers.map(route => <RoutePowers route={route}/> )
};

Related

What is the good practice way to prevent a useEffect from triggering on initial render? [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

React useState rerender trigger behaves different depending of when its called

Assuming you have a hook that has multiple useState, for example:
function useMyHook() {
const [s1, setS1] = useState();
const [s2, setS2] = useState();
const setFn = useCallback(
() => {
console.log('set s1');
setS1(Date.now());
console.log('set s2');
setS2(Date.now());
},
[setS1, setS2]
);
return setFn;
}
Now, depending of WHEN you call the setFn react will trigger either one or two rerender.
One rerender:
function MyComponent() {
console.log('render')
const setTwoStates = useMyHook()
// sync call => triggers only one re render
useEffect(() => setTwoStates(), [])
// console.logs are: render, set s1, set s2, render
return (
<div>hello</div>
);
}
Two rerenders:
function MyComponent() {
console.log('render')
const setTwoStates = useMyHook()
// as soon as the setFn is called in a later tick, react triggers two re renders
useEffect(() => setTimeout(setTwoStates, 1), [])
// console.logs are: render, set s1, render, set s2, render
return (
<div>hello</div>
);
}
Does anyone have an explanation for this? This can lead to unexpected behaviour depending how you call a hook.
I also created a small example repository in case you want to play around with this
https://github.com/JohannesMerz/react-setstate-rerenders
React will batch state setters when they set state in useEffect callbacks or event handlers (while the function is running) but in your async example the state is set after the effect function has returned.
const syncFn = () =>
console.log('in sync function');
const asyncFn = () => setTimeout(
()=>console.log('in async function'),
10
);
syncFn();
console.log('sync function returned');
asyncFn();
console.log('async function returned');
You can see in that snippet that in async function logs after async function returned so any set states that would happen would happen after the effect callback or event handler returned and cannot be batched unless you explicitly tell React to batch it.
You can use unstable_batchedUpdates to tell react to batch the updates:
const App = () => {
const [item1, setItem1] = React.useState(0);
const [item2, setItem2] = React.useState(0);
const render = React.useRef(0);
//mutate render ref (shows how often component is rendered)
render.current++;
const asyncUpdates = React.useCallback(() => {
Promise.resolve().then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
});
});
}, []);
const syncUpdates = React.useCallback(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
}, []);
const asyncNonBatchedUpdates = React.useCallback(() => {
Promise.resolve().then(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
});
}, []);
//using asyncUpdates in effect on mount
React.useEffect(asyncUpdates, []);
return (
<div>
<h1>Rendered: {render.current}</h1>
<div>
<div>item1:{item1}</div>
<div>item2:{item2}</div>
</div>
{/* run async updates as event handler */}
<button onClick={asyncUpdates}>async updates</button>
<button onClick={syncUpdates}>sync updates</button>
<button onClick={asyncNonBatchedUpdates}>
async non bathed updates
</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

clear timeout in react if component state changes

I have code using react hooks that is fetching some data from an API after a search value has been entered.
I am trying to activate a Timeout that will stop in case the value of const [searchResults, setSearchResults] = useState([]);changes.
I am not able to find out the way to do it, I can activate it when the use effect for fetching the information is activated, but I can not deactivate it in case new information is found.
In a nutshell:
I would like to know how to clearTimeout in case searchResults has value.
My code looks like:
useEffect(() => {
setIsLoading(true);
ProductsApi.getSearchProducts(query,setSearchResults, setIsLoading);
setTimeout(() => {
setError(false)
}, 2000);
}, [query]);
useEffect(() => {
clearTimeout(timer);
}, [searchResults]);
A generic answer to your problem
const MyComponent = () => {
const [shouldRun, setShouldRun] = useState(true);
useEffect(() => {
const TIMEOUT_DURATION = 10000;
const myTimeoutCallback = () => {
if (!shouldRun) return;
// Add your logic here
console.log("My callback was invoked after a delay.");
};
const timeoutId = setTimeout(myTimeoutCallback, TIMEOUT_DURATION);
return () => clearTimeout(timeoutId);
}, [shouldRun]);
};
You can use shouldRun as a condition if the callback function should be invoked or not.
To clear the time out you can do this: const timer = setTimeout(() => {.
However with your code, timer will be out of scope when you call clearTimeout(timer).
You should adjust your code so that timer is in scope allowing access to the variable timer.
Use setInterval
function DemoApp() {
React.useEffect(() => {
const timer = window.setInterval(() => {
console.log('2 seconds has passed');
}, 2000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>
My Demo Timer | Please check console.
</div>
);
}
ReactDOM.render(
<div>
<DemoApp />
</div>,
document.querySelector("#DemoApp")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="DemoApp"></div>

Loading spinner not showing in React Component

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>

Suspense component gets called before the data actually loads

I'm trying to add react lazy in my application, and for some reason, it doesn't seem to work.
The component in which I want the lazy load to work on, fetches its data from a server, then it renders the data. The problem is, the component in which the data is getting fetched, which is in the suspense tag, gets loaded before the data actually loads. Here's my code:
AnotherTest.jsx
const AnotherTest = () => {
const [toDoListData, setToDoListData] = useState([]);
useEffect(() => {
async function fetchData() {
setTimeout(async () => {
const result = await axios.get(`/api/ToDos/filter/completed`);
setToDoListData(result.data);
}, 5000);
}
fetchData();
}, []);
if (!toDoListData.length) return null;
return (
<div>
{toDoListData.map(item => {
return <div>{item.name}</div>;
})}
</div>
);
};
Test.jsx
import React, { lazy, Suspense } from 'react';
const AnotherTest = React.lazy(() => import('./AnotherTest'));
const Testing = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<AnotherTest />
</Suspense>
</div>
);
};
The only way I know of that's currently possible is by using fetch-suspense.
Read this for a complete article on how he did it.
This would turn your component into
const AnotherTest = () => {
const toDoListData = useFetch('/api/ToDos/filter/completed', { method: 'GET' });
return (
<div>
{toDoListData.map(item => {
return <div>{item.name}</div>;
})}
</div>
);
};
If for some reason the fetch-suspense package does not suit your needs the only way is to show a loader in the AnotherTest component itself while fetching the data.
Note that the lazy function is meant for code-splitting and thus lazy loading the JS file, not waiting for anything async in the component itself.
(There is also react-cache but they advice not to use it in real world applications.)

Resources