Suspense component gets called before the data actually loads - reactjs

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.)

Related

Ref always return null in Loadable Component

I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}

Standard way of fetching external data before showing webpage (2021 September)

I like the way in AngularJS of fetching external data before showing webpage. The data will be sent one by one to the frontend before showing the webpage. We are certain that the website and the data on it is good when we see it.
$stateProvider
.state('kpi', {
url:'/kpi',
templateUrl: '/htmls/kpi.html',
resolve: {
getUser: ['lazy', 'auth', function (lazy, auth) { return auth.getUser() }],
postPromise: ['posts', 'getUser', function (posts, getUser) { return posts.getAll() }],
userPromise: ['users', 'postPromise', function (users, postPromise) { return users.getAll() }],
logs: ['kpiService', 'userPromise', function (kpiService, userPromise) { return kpiService.getLogs() }],
subscribers: ['kpiService', 'logs', function (kpiService, logs) { return kpiService.getSubscribers() }]
},
controller: 'KpiCtrl'
})
Now, I would like to achieve this in ReactJS, I tried:
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1() {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({ logs.data });
});
};
render() {
return;
<div>
<HighchartsReact
highcharts={Highcharts}
options={this.getChartOptions1()}
{...this.props}
/>
<div>{JSON.stringify(this.state.logs)}</div>
</div>;
}
}
But it seems that it first called getChartOptions1 with unready data, rendered the webpage, then fetched the external data, then called again getChartOptions1 with ready data, rendered the webpage again.
I don't like the fact that getChartOptions was called twice (first with unready data), and the page was rendered twice.
There are several ways discussed: Hooks, React.Suspense, React.Lazy, etc. Does anyone know what's the standard way of fetching external data before showing the webpage in React?
As suggested in comments, conditional rendering might look like
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs.length ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
but it might be better to start with logs: null, in case the fetch returns an empty array
class Kpi extends React.Component {
state = { logs: null };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
Like mentioned above, the answer to your problem is is conditional rendering in the Kpi component. But you can take it one step further and make the conditional render directly on the chartoptions since they are the ones you need at the end.
You could also write Kpi as a functional component and go for a hook-solution. And by doing so de-coupling the data fetching from your Kpi component.
You can also make the hook more generic to handle different endpoints so the hook can be reused in multiple places in your app.
export const useBackendData = (initialEndpoint) => {
const [endpoint, setEndpoint] = useState(initialEndpoint);
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(endpoint);
setData(response.data);
}catch(e){
// error handling...
}
}
fetchData();
}, [endpoint])
return [data, setEndpoint]
}
const Kpi = (props) => {
[chartOptions, setChartOptions] = useState(null);
[logs] = useBackendData(`${BACKEND_URL}/httpOnly/kpi/logs`);
const getChartOptions1 = () => {
// do stuff with logs...
setChartOptions([PROCESSED LOG DATA]);
}
useEffect(() => {
if(!!logs.length)
setChartOptions(getChartOptions1())
},[logs]);
return !!chartOptions ? (
<div>
<HighchartsReact highcharts={Highcharts} options={chartOptions} {...props} />
<div>{JSON.stringify(logs)}</div>
</div>) : (
<div>Loading data...</div>
);
}
React lifecycle is like this - first, the render method and then the componentDidMount is called (as per your case) during mounting of components, so you are having this issue.
What I do is show a loader, spinner(anything) till the data is being fetched, and once the data is there, show the actual component. The app needs to do something till it gets the data.

How to fetch multiple API using typescript in reactjs?

I have a list of urls, I want to fetch all of them and to return the images found in all these APIs so I can render it in react component using react-responsive-masonry. I have made my function in javascript but I am not sure how to write it in typescript and also I don't know how to render it in my component.
Here's my function
var photos_info = [];
async function get_photos(urls) {
var promises = urls.map((url) => fetch(url).then((y) => y.json()));
await Promise.all(promises).then((results) => {
photos_info = results;
return photos_info;
});
return photos_info;
}
I want to render it in src in my component
<ResponsiveMasonry columnsCountBreakPoints={columnsCountBreakPoints}>
<Masonry gutter={4}>
{
<img src={} />
}
</Masonry>
</ResponsiveMasonry>
Edit
Another method using useState and useEffect
const [photosList, setPhotosList] = useState<any>();
useEffect(() => {
const photosPromises = urls.map((url) =>
fetch(url).then((res) => res.json())
);
Promise.all(photosPromises).then((data) => {
setPhotosList(data);
});
}, []);
console.log("hi", photosList);
I tried to render a simple one just to see what is inside
<div>
{photosList.map((photo: any) => {
return <pre>{JSON.stringify(photo)}</pre>;
})}
</div>
but it gives me this error Cannot read property 'map' of undefined

How can I set the function to render after another function?

I connected my react app to firebase and I think the problem is that the page loads before the data from my database is acquired, what can I do to delay the function until after it finishes acquiring the data?
function getPosts(){
db.collection("Posts").get().then(snapshot =>{
snapshot.docs.forEach(docs =>{
createPost(
docs.data().postName,
docs.data().createdAt,
docs.data().postContent,
)
})
})
}
getPosts();
function Blog(){
return (
<div>
<Navbar/>
<div className="container">
<div className="row" id="posts-collection">
</div>
</div>
</div>
)
}
export default Blog;
As MehmetDemiray already shows you can load data as an effect within a function component, but that answer assumes you only wish to track loading status.
If you want to use the data loaded to display the post data then you will also need to store the returned data.
const Blog: React.FunctionComponent = () => {
// state to store data returned by async call. (originally set to null).
const [posts, setPosts] = React.useState(null);
// use an effect to load async data.
// the effect only runs on component load and every time the data in
// the dependency array changes "[setPosts]" (reference comparison).
React.useEffect(() => {
// Create funtion to run async logic to load posts.
const getPosts = () => {
// load posts
db.collection("Posts").get().then(snapshot => {
// map loaded posts to an array of easy to manage objects.
const loadedPosts = snapshot.docs.map(docs => {
return {
name: docs.data().postName,
createdAt: docs.data().createdAt,
content: docs.data().postContent,
}
});
// store loaded posts in state.
setPosts(loadedPosts ?? []);
});
};
// run async function created above.
getPosts();
}, [setPosts])
// posts will remain null until the async function has loaded data.
// you can manually track loading in a separate state if required.
if (posts === null) {
// Show loading view while loading.
return (
<div>
Loading Posts...
</div>
);
}
// map out posts view after posts have been loaded.
return (
<div>
{posts.map(post => (
<div>
<div>{post.postName}</div>
<div>{post.createdAt}</div>
<div>{post.content}</div>
</div>
))}
</div>
);
};
You need to loading control before rendering jsx. Looks like this;
import {useEffect, useState} from 'react';
function Blog(){
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
function getPosts(){
db.collection("Posts").get().then(snapshot =>{
snapshot.docs.forEach(docs =>{
createPost(
docs.data().postName,
docs.data().createdAt,
docs.data().postContent,
)
})
setLoading(false);
})
}
getPosts();
}, [])
return (
loading ?
<div>
< Navbar/>
<div className="container">
<div className="row" id="posts-collection">
</div>
</div>
</div> : null
)
}
export default Blog;

React Suspense is not working as intended

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}/> )
};

Resources