Next.js getServerSideProps show loading - javascript

I am using getServerSideProps in pages/post/index.js:
import React from "react";
import Layout from "../../components/Layout";
function Post({ post }) {
console.log("in render", post);
return (
<Layout title={post.name}>
<pre>{JSON.stringify(post, undefined, 2)}</pre>
</Layout>
);
}
export async function getServerSideProps({ query }) {
return fetch(
`${process.env.API_URL}/api/post?id=${query.id}`
)
.then(result => result.json())
.then(post => ({ props: { post } }));
}
export default Post;
When I directly load /post/2 it works as expected but when I go from /posts to /post/2 by clicking on a link:
<Link
as={`/post/${post.id}`}
href={`/post?id=${post.id}`}
>
It looks like nothing happens for 2 seconds (the api delay) and then the content shows. I can see in the network tab that _next/data/development/post/9.json is being loaded by fetchNextData.
I would like to show a loading spinner when I move from one route to another using next/Link but I can't find any documentation on getServerSideProps that allows me to do this.
When I directly go to /post/:id I'd like the data to be fetched server side and get a fully rendered page (works) but when I then move to another route the data should be fetched from the client (works). However; I would like to have a loading indicator and not have the UI freeze up for the duration of the data request.

Here is an example using hooks.
pages/_app.js
import Router from "next/router";
export default function App({ Component, pageProps }) {
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const start = () => {
console.log("start");
setLoading(true);
};
const end = () => {
console.log("finished");
setLoading(false);
};
Router.events.on("routeChangeStart", start);
Router.events.on("routeChangeComplete", end);
Router.events.on("routeChangeError", end);
return () => {
Router.events.off("routeChangeStart", start);
Router.events.off("routeChangeComplete", end);
Router.events.off("routeChangeError", end);
};
}, []);
return (
<>
{loading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}

You can use nprogress in your _app.js
import NProgress from 'nprogress';
import "nprogress/nprogress.css";
import Router from 'next/router';
NProgress.configure({
minimum: 0.3,
easing: 'ease',
speed: 800,
showSpinner: false,
});
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
or dynamic import to _app.js to reduce bundle size
ProgessBar.js
import Router from 'next/router';
import NProgress from 'nprogress';
import "nprogress/nprogress.css";
NProgress.configure({
minimum: 0.3,
easing: 'ease',
speed: 500,
showSpinner: false,
});
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
export default function () {
return null;
}
_app.js
import dynamic from 'next/dynamic';
const ProgressBar = dynamic(() => import('components/atoms/ProgressBar'), { ssr: false });
const App = () => {
...
return <>
...
<ProgressBar />
</>
}
Ps: If you want to change color of progress bar, you can override in global css, something like this
#nprogress .bar {
background: #6170F7 !important;
height: 3px !important;
}

You can create a custom hook:
usePageLoading.ts
import Router from 'next/router';
import { useEffect, useState } from 'react';
export const usePageLoading = () => {
const [isPageLoading, setIsPageLoading] = useState(false);
useEffect(() => {
const routeEventStart = () => {
setIsPageLoading(true);
};
const routeEventEnd = () => {
setIsPageLoading(false);
};
Router.events.on('routeChangeStart', routeEventStart);
Router.events.on('routeChangeComplete', routeEventEnd);
Router.events.on('routeChangeError', routeEventEnd);
return () => {
Router.events.off('routeChangeStart', routeEventStart);
Router.events.off('routeChangeComplete', routeEventEnd);
Router.events.off('routeChangeError', routeEventEnd);
};
}, []);
return { isPageLoading };
};
and then inside your App component use it:
_app.js
import Router from "next/router";
import { usePageLoading } from './usePageLoading';
export default function App({ Component, pageProps }) {
const { isPageLoading } = usePageLoading();
return (
<>
{isPageLoading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}

How about simply adding a component level loading state to Post (vs. adding a loader on App Level for every route change since some route changes might not require server side rendering).
Setting the isLoading state to true when the relevant query param changes, in this case the post id, and setting the state to false once the props, in this case the post data, updated.
Along these lines:
pages/post/index.js:
import React from "react";
import Layout from "../../components/Layout";
import { useRouter } from 'next/router';
function Post({ post }) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
// loading new post
useEffect(()=> {
setIsLoading(true);
}, [router.query?.id]);
// new post loaded
useEffect(()=> {
setIsLoading(false)
}, [post]);
return (
<>
{isLoading ? (
<h1>Loading...</h1>
) : (
<Layout title={post.name}>
<pre>{JSON.stringify(post, undefined, 2)}</pre>
</Layout>
)}
</>
);
}
export async function getServerSideProps({ query }) {
return fetch(
`${process.env.API_URL}/api/post?id=${query.id}`
)
.then(result => result.json())
.then(post => ({ props: { post } }));
}
export default Post;

Just adding to the previous answers, you can receive a url parameter in the event handlers, and use those to filter out which route you want a loading state and which not. Simple example in _app.js:
function MyApp({ Component, pageProps: { ...pageProps } }: AppProps) {
const router = useRouter();
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
const handleChangeStart = (url: string) => {
if (url === "<root_to_show_loading>") {
setIsLoading(true);
}
};
const handleChangeEnd = (url: string) => {
if (url === "<root_to_show_loading") {
setIsLoading(false);
}
};
router.events.on("routeChangeStart", handleChangeStart);
router.events.on("routeChangeComplete", handleChangeEnd);
router.events.on("routeChangeError", handleChangeEnd);
}, []);
return (
<main>
{isLoading ? <LoadingSpinner /> : <Component {...pageProps} />}
</main>
);
}
export default MyApp;

**Here is how I did it in NextJs with Material UI and nprogress**
import '../styles/globals.css';
import { useEffect, useState } from 'react';
import Router from 'next/router';
import NProgress from 'nprogress';
import { useStyles } from '../src/utils';
import { CircularProgress } from '#material-ui/core';
NProgress.configure({ showSpinner: false });
function MyApp({
Component,
pageProps
}) {
const classes = useStyles();
const [loading, setLoading] = useState(false);
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
const start = () => {
console.log('start');
NProgress.start();
setLoading(true);
};
const end = () => {
console.log('findished');
NProgress.done();
setLoading(false);
};
Router.events.on('routeChangeStart', start);
Router.events.on('routeChangeComplete', end);
Router.events.on('routeChangeError', end);
return () => {
Router.events.off('routeChangeStart', start);
Router.events.off('routeChangeComplete', end);
Router.events.off('routeChangeError', end);
};
}, []);
return (
<>
{loading ? (
<div className={classes.centered}>
<CircularProgress size={25} color='primary' />
</div>
) : (
<Component {...pageProps} />
)}
</>
);
}
export default MyApp;
Result:

Progress bar like NProgress in 90 lines of code (vs NProgress v0.2.0 is 470 lines .js + 70 lines .css):
import { useEffect, useReducer, useRef } from 'react';
import { assert } from './assert';
import { wait } from './wait';
import { getRandomInt } from './getRandomNumber';
let waitController: AbortController | undefined;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function useProgressBar({
trickleMaxWidth = 94,
trickleIncrementMin = 1,
trickleIncrementMax = 5,
dropMinSpeed = 50,
dropMaxSpeed = 150,
transitionSpeed = 600
} = {}) {
// https://stackoverflow.com/a/66436476
const [, forceUpdate] = useReducer(x => x + 1, 0);
// https://github.com/facebook/react/issues/14010#issuecomment-433788147
const widthRef = useRef(0);
function setWidth(value: number) {
widthRef.current = value;
forceUpdate();
}
async function trickle() {
if (widthRef.current < trickleMaxWidth) {
const inc =
widthRef.current +
getRandomInt(trickleIncrementMin, trickleIncrementMax); // ~3
setWidth(inc);
try {
await wait(getRandomInt(dropMinSpeed, dropMaxSpeed) /* ~100 ms */, {
signal: waitController!.signal
});
await trickle();
} catch {
// Current loop aborted: a new route has been started
}
}
}
async function start() {
// Abort current loops if any: a new route has been started
waitController?.abort();
waitController = new AbortController();
// Force the show the JSX
setWidth(1);
await wait(0);
await trickle();
}
async function complete() {
assert(
waitController !== undefined,
'Make sure start() is called before calling complete()'
);
setWidth(100);
try {
await wait(transitionSpeed, { signal: waitController.signal });
setWidth(0);
} catch {
// Current loop aborted: a new route has been started
}
}
function reset() {
// Abort current loops if any
waitController?.abort();
setWidth(0);
}
useEffect(() => {
return () => {
// Abort current loops if any
waitController?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
start,
complete,
reset,
width: widthRef.current
};
}
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useProgressBar } from './useProgressBar';
const transitionSpeed = 600;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function RouterProgressBar(
props?: Parameters<typeof useProgressBar>[0]
) {
const { events } = useRouter();
const { width, start, complete, reset } = useProgressBar({
transitionSpeed,
...props
});
useEffect(() => {
events.on('routeChangeStart', start);
events.on('routeChangeComplete', complete);
events.on('routeChangeError', reset); // Typical case: "Route Cancelled"
return () => {
events.off('routeChangeStart', start);
events.off('routeChangeComplete', complete);
events.off('routeChangeError', reset);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return width > 0 ? (
// Use Bootstrap, Material UI, Tailwind CSS... to style the progress bar
<div
className="progress fixed-top bg-transparent rounded-0"
style={{
height: 3, // GitHub turbo-progress-bar height is 3px
zIndex: 1091 // $zindex-toast + 1 => always visible
}}
>
<div
className="progress-bar"
style={{
width: `${width}%`,
//transition: 'none',
transition: `width ${width > 1 ? transitionSpeed : 0}ms ease`
}}
/>
</div>
) : null;
}
How to use:
// pages/_app.tsx
import { AppProps } from 'next/app';
import Head from 'next/head';
import { RouterProgressBar } from './RouterProgressBar';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>My title</title>
<meta name="description" content="My description" />
</Head>
<RouterProgressBar />
<Component {...pageProps} />
</>
);
}
More here: https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047

To add to the previous answers and show complete code, you can add a delay with setTimeout when setting state in the event hook to avoid a flicker of loading on fast loading routes (either static routes, or server routes ready to go).
import Router from 'next/router';
import { useEffect, useRef, useState } from 'react';
const usePageLoad = (delay = 200) => {
const timeoutRef = useRef();
const [loading, setLoading] = useState(false);
useEffect(() => {
const start = () => {
timeoutRef.current = window.setTimeout(() => {
setLoading(true);
}, delay);
};
const end = () => {
window.clearTimeout(timeoutRef.current);
setLoading(false);
};
Router.events.on('routeChangeStart', start);
Router.events.on('routeChangeComplete', end);
Router.events.on('routeChangeError', end);
return () => {
Router.events.off('routeChangeStart', start);
Router.events.off('routeChangeComplete', end);
Router.events.off('routeChangeError', end);
};
}, [delay]);
return loading;
};
export default usePageLoad;
Then use this hook in _app and adjust the delay as needed for your application.
import PageLoader from '../components/PageLoader';
import usePageLoad from '../components/use-page-load';
const App = ({ Component, pageProps }) => {
const loading = usePageLoad();
return (
{
loading
? <PageLoader />
: <Component {...pageProps} />
}
);
};

Related

Can I do client side fetching inside the server side render component?

Can I perform client-side data fetching inside a component that's being rendered on a server-side rendered page? I have a page located at pages/solution/[solutionId]/index.js, which is server-side rendered, and it contains three components that should be performing client-side fetching. However, I am not getting any data, and it is returning null.
index.js
const Solution = ({ solution }) => {
const [isOpen, setIsOpen] = useState(false)
const router = useRouter()
const { id } = router.query
const { user } = useAuthContext()
return (
<>
<div className="px-5 row-start-2 row-end-3 col-start-2 col-end-3 mb-4">
// doing client-side fetching
<ShowWebsite
url={solution?.liveWebsiteUrl}
github={solution?.githubUrl}
title={solution?.title}
isPlayground={solution?.isPlayground}
/>
<div className="grid grid-col-1 md:grid-cols-[1fr_160px] items-start gap-x-5 mt-10">
<SolutionComments /> // doing client side fetching
<EmojiSection /> // doing client side fetching
</div>
</div>
</>
)
}
export default Solution
export async function getServerSideProps({ query }) {
const { solutionId } = query
console.log(solutionId)
const solution = await getDocument("solutions", solutionId)
return {
props: {
solution,
},
}
}
SolutionsComment:
import { useState } from "react"
import { useRouter } from "next/router"
import { useCollection } from "../../hooks/useCollection"
import Comment from "./Comment"
import CommentForm from "./CommentForm"
const SolutionComments = () => {
const router = useRouter()
const { id } = router.query
const { documents } = useCollection(`solutions/${id}/comments`)
return (
<div className="mt-10 md:mt-0">
<CommentForm docID={id} />
<div className="mt-10">
{documents &&
documents.map((comment) => (
<Comment
key={comment.id}
comment={comment}
replies={comment.replies}
/>
))}
</div>
</div>
)
}
EmojiSection:
import React from "react"
import { useRouter } from "next/router"
import { useDocument } from "../../hooks/useDocument"
import Emoji from "./Emoji"
const EmojiSection = () => {
const router = useRouter()
const { id: docID } = router.query
const { document: reactions } = useDocument(`solutions/${docID}/reactions`, "emojis")
console.log(reactions)
return (
// JSX CODE
)
}
useCollection:
import { collection, onSnapshot} from "firebase/firestore"
export const useCollection = (c) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let ref = collection(db, c)
const unsubscribe = onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
setDocuments(results)
setIsLoading(false)
setError(null)
})
return () => unsubscribe()
}, [])
return { documents, error, isLoading }
}

Content provider data no being loaded into a component

I have the main page 'feed' where i used to have three functions, but I moved it into custom context. The console logs in context file output all the objects correctly, but nothing is visible in feed when i concole.log them.
context file:
import React, { useEffect, createContext, useContext, useState } from 'react'
import { useMemo } from 'react';
import getPosts from '../api/getPosts';
import filterImportedPosts from '../utils/filterImportedPosts';
export const ItemContext = createContext({
postData: {}, setPostData: () => { }
});
export const FilteredItemsContext = createContext({ filteredItems: [], setFilteredItems: () => { } })
export const FilterContext = createContext({ filter: '', setFilter: () => { } })
export function useItemContext() {
return useContext(ItemContext)
}
export function useFilteredItemsContext() {
return useContext(FilteredItemsContext)
}
export function useFilterContext() {
return useContext(FilterContext)
}
export default function PostProvider({ children }) {
const [postData, setPostData] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
const [filter, setFilter] = useState('');
useEffect(() => {
getPosts(setPostData)
console.log('postData: ', postData)
}, []);
useEffect(() => {
// console.log(filter);
const tempFiltItems = filterImportedPosts(postData, filter);
setFilteredItems(tempFiltItems);
console.log('tempFiltItems: ', filteredItems)
}, [filter, postData]);
const filteredItemsState = useMemo(() => {
return { filteredItems, setFilteredItems }
}, [filteredItems, setFilteredItems])
return (
<FilterContext.Provider value={{ filter, setFilter }}>
<FilteredItemsContext.Provider value={filteredItemsState}>
<ItemContext.Provider value={{ postData, setPostData }}>
{children}
</ItemContext.Provider>
</FilteredItemsContext.Provider >
</FilterContext.Provider>
)
}
and here the feed file:
import React, { useState, useEffect } from 'react';
import SinglePost from '../components/singlePost/singlePost';
import FilterPane from '../components/filterPane/filterPane.feedPost';
import { Box, Spinner, Text } from '#chakra-ui/react';
import getPosts from '../api/getPosts';
import Loader from '../../common/Loader';
import filterImportedPosts from '../utils/filterImportedPosts';
import PostProvider, { useFilterContext, useFilteredItemsContext, useItemContext } from './../context/PostDataContext';
export default function Feed() {
//-----------------IMPORT DATA FROM SERVER----------------------
const [error, setError] = useState(null);
const { postData, setPostData } = useItemContext();
const { filter, setFilter } = useFilterContext();
const { filteredItems, setFilteredItems } = useFilteredItemsContext();
useEffect(() => {
console.log(filteredItems)
}, [filteredItems, setFilteredItems])
// this helps while the data is loaded
// if (postData.length === 0) {
// return (
// <Box pos='absolute' top='45vh' left='40%'>
// <Loader />
// </Box>
// )
// }
// console.log(filteredItems);
return (
<PostProvider>
<Box mt={'7vh'} mb={'7vh'} ml={'3vw'} mr={'3vw'} zIndex={200}>
<FilterPane
setFilter={setFilter}
filter={filter}
filteredItems={filteredItems}
/>
{error && (
<div>Error occurred while loading profile info. Details: {error}</div>
)}
{!error && (
<>
{filteredItems.map((item, index) => {
return <SinglePost key={index} item={item} />;
})}
</>
)}
</Box>
</PostProvider>
);
}
console window printscreen. As you can see the filteredItems in context exist but nothing gets shown in the actual feed - the objects are empty. Could someone assist please?

Next.js getServerSideProps loading state

Is there a way we can have a loading state similar to when fetching data on the client-side?
The reason I would like a loading state is to have something like a loading-skeleton with for instance react-loading-skeleton
On the client-side we could do:
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
But for SSR (getServerSideProps) I cannot figure out if that is doable for example could we have a loading state?
function AllPostsPage(props) {
const router = useRouter();
const { posts } = props;
function findPostsHandler(year, month) {
const fullPath = `/posts/${year}/${month}`;
router.push(fullPath);
}
if (!data) return <div>loading...</div>; // Would not work with SSR
return (
<Fragment>
<PostsSearch onSearch={findPostsHandler} />
<PosttList items={posts} />
</Fragment>
);
}
export async function getServerSideProps() {
const posts = await getAllPosts();
return {
props: {
posts: posts,
},
};
}
export default AllPostsPage;
Recently Next.js has released getServerSideProps should support props value as Promise https://github.com/vercel/next.js/pull/28607
With that we can make a promise but am not sure how to implement that and have a loading state or if that is even achievable. Their example shows:
export async function getServerSideProps() {
return {
props: (async function () {
return {
text: 'promise value',
}
})(),
}
}
Currently watching Next.conf (25/10/2022) this issue looks promising:
https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense
You can modify the _app.js component to show a Loading component while the getServerSideProps is doing async work like a fetch as shown here https://stackoverflow.com/a/60756105/13824894. This will apply on every page transition within your app.
You can still use your loading logic client-side independently.
you can set loading state on _app.js
import Router from "next/router";
export default function App({ Component, pageProps }) {
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const start = () => {
console.log("start");
setLoading(true);
};
const end = () => {
console.log("findished");
setLoading(false);
};
Router.events.on("routeChangeStart", start);
Router.events.on("routeChangeComplete", end);
Router.events.on("routeChangeError", end);
return () => {
Router.events.off("routeChangeStart", start);
Router.events.off("routeChangeComplete", end);
Router.events.off("routeChangeError", end);
};
}, []);
return (
<>
{loading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}
My choice is to use isReady method of useRouter object
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
function MyApp({ Component, pageProps }) {
const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
useEffect(() => {
router.isReady && setIsLoading(false)
}, []
)
return <>{isLoading ? <>loading...</> : <Component {...pageProps} />}</>
}
export default MyApp
I have not tried this feature yet but in theory I think it should work. If all you want is to have the client side access to a promise via server props, try as below. Basically your props is a async lambda function so you do any work needed e.g fetching data etc inside it so the client-side should access props as a promise and await for it.
export async function getServerSideProps() {
return {
props: (async function () {
const posts = await getAllPosts();
return {
posts: posts,
}
})(),
}
}
//then on client-side you can do the following or similar to set loading state
function MyComponent(props) {
const [isLoading, setIsLoading] = useState(false);
const [posts, setPosts] = useState({});
useEffect(async () => {
setIsLoading(true);
const tempPosts = await props?.posts;
setPosts(posts);
setIsLoading(false);
}, [])
return (
{isLoading && <div>loading...</div>}
);
}
export default MyComponent;
This works for me using MUI v.5
import Router from "next/router";
import Head from "next/head";
import { useEffect, useState } from "react";
import { CacheProvider } from "#emotion/react";
import {
ThemeProvider,
CssBaseline,
LinearProgress,
CircularProgress,
circularProgressClasses,
Box,
} from "#mui/material";
import { alpha } from "#mui/material/styles";
import createEmotionCache from "/src/createEmotionCache";
import theme from "/src/theme";
import Layout from "/src/components/layout/Layout";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
function Loader(props) {
return (
<Box
sx={{
position: "fixed",
top: 0,
left: 0,
right: 0,
}}
>
<LinearProgress />
<Box sx={{ position: "relative", top: 8, left: 8 }}>
<CircularProgress
variant="determinate"
sx={{
color: alpha(theme.palette.primary.main, 0.25),
}}
size={40}
thickness={4}
{...props}
value={100}
/>
<CircularProgress
variant="indeterminate"
disableShrink
sx={{
animationDuration: "550ms",
position: "absolute",
left: 0,
[`& .${circularProgressClasses.circle}`]: {
strokeLinecap: "round",
},
}}
size={40}
thickness={4}
{...props}
/>
</Box>
</Box>
);
}
function MyApp({
Component,
pageProps,
emotionCache = clientSideEmotionCache,
}) {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
Router.events.on("routeChangeStart", () => {
setIsLoading(true);
});
Router.events.on("routeChangeComplete", () => {
setIsLoading(false);
});
Router.events.on("routeChangeError", () => {
setIsLoading(false);
});
}, [Router]);
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
{isLoading && <Loader />}
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
</CacheProvider>
);
}
export default MyApp;

How to enable text copy in React grid built using React-Data-Grid

I am using React-Data-Grid to show my data in the table, but the copy functionality is not working.
Below is my Grid Component in React, In which I have connectd it to the Back end API as well as with React Redux Store.
import React, { Component, useState, useEffect } from "react";
import { connect } from "react-redux";
import ApiHelper from "../../api/ApiHelper";
import ReactDataGrid from "react-data-grid";
import { Toolbar, Data, Filters } from "react-data-grid-addons";
import JarvisSpinner from "../presentationalComponents/JarvisSpinner";
import { withRouter } from "react-router-dom";
import "../../css/styles.css";
import "../../css/JarvisGrid.css";
const JarvisGrid = props => {
const [pagesize, setPageSize] = useState(15);
const [data, setdata] = useState([]);
const [filters, setFilters] = useState({
filterfield: "Application Send",
filtervalue: "Base"
});
const [sort, setSort] = useState({
Field: "PublicOrderNumber",
Direction: "desc"
});
const [loading, setloading] = useState(true);
useEffect(() => {
//Set data
ReloadData();
}, [props.uid, props.CalendarDates.fromDate, props.CalendarDates.toDate]);
const ReloadData = () => {
ApiHelper.GetGridQueryResult(
props.uid,
props.filterField,
props.filterValue,
props.CalendarDates.fromDate,
props.CalendarDates.toDate
)
.then(response => {
console.log("response");
console.log(response);
console.log(response.status);
console.log("Checking the data");
console.log(response.data.responses);
setdata(response.data.responses);
setloading(false);
})
.catch(error => {
setdata([]);
switch (error.response.status) {
case 403:
console.log("Error code --> " + 403);
props.history.push("/unAuthorizedPage");
break;
default:
console.log("Error String --->" + error);
}
});
};
return (
<>
{loading ? (
<JarvisSpinner size="3x" />
) : (
<div className="JarvisGrid">
<ReactDataGrid
columns={props.columns}
rowGetter={i => data[i]}
rowsCount={data.length}
minHeight={500}
uid={props.uid}
enableRowSelect={null}
enableCellSelect
toolbar={<Toolbar enableFilter="true" />}
onAddFilter={filter => {
setFilters({
filterfield: filter.column.key,
filtervalue: filter.filterTerm
});
}}
onClearFilters={() =>
setFilters({
filterfield: "OrderType",
filtervalue: "Base"
})
}
onGridSort={(sortColumn, sortDirection) => {
setSort({ Field: sortColumn, Direction: sortDirection });
}}
/>
</div>
)}
</>
);
};
function mapStateToProps(state) {
return {
CalendarDates: state.calendarDates
};
}
export default withRouter(connect(mapStateToProps)(JarvisGrid));
Let me know if I need to pass any props to the React-data-grid that I might have missed

Implementing infinite scroll in React with Apollo Client

In my NextJS app, I have a PostList.jsx component that looks like this:
import { useQuery } from '#apollo/react-hooks';
import Typography from '#material-ui/core/Typography';
import { NetworkStatus } from 'apollo-client';
import gql from 'graphql-tag';
import getPostsQuery from '../../apollo/schemas/getPostsQuery.graphql';
import Loading from './Loading';
import Grid from '#material-ui/core/Grid';
import PostPreview from './PostPreview';
import withStyles from '#material-ui/core/styles/withStyles';
import React, { useLayoutEffect } from 'react';
const styles = (theme) => ({
root: {
padding: theme.spacing(6, 2),
width: '100%',
},
});
export const GET_POSTS = gql`${getPostsQuery}`;
export const getPostsQueryVars = {
start: 0,
limit: 7,
};
const PostsList = (props) => {
const { classes } = props;
const {
loading,
error,
data,
fetchMore,
networkStatus,
} = useQuery(
GET_POSTS,
{
variables: getPostsQueryVars,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we'd know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
},
);
const loadingMorePosts = networkStatus === NetworkStatus.fetchMore;
const loadMorePosts = () => {
fetchMore({
variables: {
skip: posts.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
posts: [...previousResult.posts, ...fetchMoreResult.posts]
})
}
})
};
const scrollFunction = () => {
const postsContainer = document.getElementById('posts-container');
if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
console.log('container bottom reached');
}
};
useLayoutEffect(() => {
document.addEventListener('scroll', scrollFunction);
scrollFunction();
// returned function will be called on component unmount
return () => {
document.removeEventListener('scroll', scrollFunction);
};
}, []);
if (error) return <div>There was an error!</div>;
if (loading) return <Loading />;
const { posts, postsConnection } = data;
const areMorePosts = posts.length < postsConnection.aggregate.count;
return (
<Grid item className={classes.root}>
<Grid container spacing={2} direction="row" id="posts-container">
{posts.map((post) => {
return (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} className={`post-preview-container`}>
<PostPreview
title={post.title}
excerpt={post.excerpt}
thumbnail={`https://i.schandillia.com/d/${post.thumbnail.hash}${post.thumbnail.ext}`}
/>
</Grid>
);
})}
</Grid>
{areMorePosts && (
<button onClick={() => loadMorePosts()} disabled={loadingMorePosts}>
{loadingMorePosts ? 'Loading...' : 'Show More'}
</button>
)}
</Grid>
);
};
export default withStyles(styles)(PostsList);
As you can see, this component fetches documents from a database via a GraphQL query using Apollo Client and displays them paginated. The pagination is defined by the getPostsQueryVars object. Here, if you scroll down to the bottom and there still are posts available, you'll get a button clicking which the next set of posts will be loaded.
What I'm keen on doing here is implement some kind of an infinite scroll and do away with the button altogether. So far, I've added a scroll event function to the component using React hooks and can confirm it's triggering as expected:
const scrollFunction = () => {
const postsContainer = document.getElementById('posts-container');
if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
console.log('container bottom reached');
}
};
useLayoutEffect(() => {
document.addEventListener('scroll', scrollFunction);
scrollFunction();
return () => {
document.removeEventListener('scroll', scrollFunction);
};
}, []);
But how do I proceed from here? How do achieve the following once the container bottom is reached AND areMorePosts is true:
Display a <h4>Loading...</h4> right before the last </Grid>?
Trigger the loadMorePosts() function?
remove <h4>Loading...</h4> once loadMorePosts() has finished executing?

Categories