useState Hooks not working with enzyme mount() - javascript

The setWishlists hook in this component seems to not run, even though everything before and after it in the promise chain runs. It just doesn't change wishlists. In my test's setup: handleGetWishlists is passed through a jest mock so that it can still be used in the component while allowing jest to spy on it. The implementation is still passed through so that Mock Service Worker an provide the data instead of mocking fetch.
My repo on the relevant branch is here
Relevant section:
//HomePage.js
import React, { useEffect, useState } from 'react';
import { makeStyles } from '#material-ui/core/styles';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
FormGroup,
FormControlLabel,
Checkbox,
} from '#material-ui/core/';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
const useStyles = makeStyles(() => ({
homeContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
heading: {},
accordion: {
width: '50%',
},
}));
const HomePage = ({ handleGetWishlists }) => {
const classes = useStyles();
const [wishlists, setWishlists] = useState([]);
useEffect(() => {
handleGetWishlists()
.then((res) => res.json())
.then((data) => {
console.log('DATA BEFORE SET', data); //--> DATA BEFORE SET [...somedata...]
return data;
})
.then(setWishlists) // --> console.error()
.then(() => console.log('WISHLIST AFTER SET', wishlists)); // --> WISHLIST AFTER SET []
console.log('END OF USE EFFECT');
}, []);
return (
<div className={classes.homeContainer}>
{wishlists.map((wishlist, index) => {
return (
<Accordion key={`Accordian${index}`} className={classes.accordion}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
>
<Typography className={classes.heading}>
{`${wishlist.name} by ${wishlist.author}`}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FormGroup>
{wishlist.items.map((item, index) => {
return (
<FormControlLabel
key={`WishlistItemCheckbox${index}`}
control={<Checkbox />}
label={item.name}
/>
);
})}
</FormGroup>
</AccordionDetails>
</Accordion>
);
})}
</div>
);
};
export default HomePage;
//HomePage.test.js
import React from 'react';
import { mount } from 'enzyme';
import { handleGetWishlists } from '../../client/client';
import HomePage from './HomePage';
import { Accordion } from '#material-ui/core/';
describe('HomePage', () => {
let component;
const mockHandleGetWishlists = jest.fn();
beforeEach(async () => {
mockHandleGetWishlists.mockImplementation(handleGetWishlists);
component = mount(<HomePage handleGetWishlists={mockHandleGetWishlists} />);
});
afterEach(() => {
mockHandleGetWishlists.mockReset();
});
it('should load wishlists', async () => {
expect(mockHandleGetWishlists).toBeCalled();
mockHandleGetWishlists()
.then((res) => res.json())
.then(console.log);
expect(component.exists(Accordion)).toBeTruthy();
});
});
//client.jsconst
client = {
//...
handleGetWishlists: () =>
fetch('http://localhost:3001/wishlist', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}),
//...
};
module.exports = client;

The variable wishlists will contain the updated value only in the next run of useEffect(), not yet in the same run, so console.log('WISHLIST AFTER SET', wishlists) still shows the old value.
Your useEffect is (intentionally) only called once, so you can not console.log the updated wishlist inside the useEffect. You just never have it there.
However, I would expect your wishlists.map() should correctly use the updated wishlists, assuming that the data returned from handleGetWishlists() is correct.
If you want to console.log the updated wishlists, you need to use a 2nd useEffect(), like this:
useEffect(() => {
console.log( 'WISHLIST AFTER SET', wishlists );
}, [ wishlists ]);

When setWishlist is used as a callback in the promise chain, its context changes from your react component state to that of the Promise itself. Calling setWishlist directly from the handler should set wishlist as expected.
handleGetWishlists()
.then((res) => res.json())
.then((data) => {
console.log('DATA BEFORE SET', data); //--> DATA BEFORE SET [...somedata...]
setWishlist(data);
})

Related

Redux Toolkit: How to make a GET request after POST or PUT request is successful?

I made a simple To Do list application to learn Redux Toolkit and after getting it working without a backend, decided to add a simple Flask server to practice working with Redux Toolkit's createAsyncThunk
I was successfully able to create 3 different async thunks for when a user fetches their to-dos, posts a new to-do, or puts a to-do. Here is my toDoSlice:
import { createSlice, current, createAsyncThunk } from '#reduxjs/toolkit';
import axiosPrivate from '../../api/axios';
const initialState = {
toDos: []
}
export const getToDos = createAsyncThunk('toDo/getToDos', async (user) => {
const response = await axiosPrivate.get(`/to-dos/${user?.user?.firebase_uid}`, { headers: { 'Authorization': user?.idToken }, withCredentials: true });
return response.data
})
export const addToDo = createAsyncThunk('toDo/addToDo', async ({ user, newToDo }) => {
const response = await axiosPrivate.post('/to-dos/new', { userId: user?.user?.id, firebaseUid: user?.user?.firebase_uid, task: newToDo?.task }, { headers: { 'Authorization': user?.idToken }, withCredentials: true });
const data = response.data
return {data, user}
})
export const updateToDo = createAsyncThunk('toDo/updateToDo', async ({ user, toDoId, updateAttributes }) => {
const response = await axiosPrivate.put('/to-dos/update', { userId: user?.user?.id, firebaseUid: user?.user?.firebase_uid, toDoId: toDoId, updateAttributes}, { headers: { 'Authorization': user?.idToken }, withCredentials: true })
const data = response.data
return {data, user}
})
export const toDosSlice = createSlice({
name: 'toDos',
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(getToDos.fulfilled, (state, action) => {
state.toDos = action.payload?.toDos
})
.addCase(addToDo.fulfilled, (action) => {
getToDos(action?.payload?.user); // This does not change anything
})
.addCase(updateToDo.fulfilled, (action) => {
getToDos(action?.payload?.user); // This does not change anything
})
}
})
export const { deleteToDo } = toDosSlice.actions;
export const selectToDos = (state) => state.toDos;
export default toDosSlice.reducer;
The problem I am having, is that after a user edits their toDo by marking it complete, I am unsure of where and how to properly fetch the to-dos from the backend. I know that I could technically set the state of the toDo using the redux state and validating if the POST or PUT was successfully, although would like to learn how it is properly done with a GET request thereafter.
My ToDoList component where users can DELETE or PUT their ToDos is as follows:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Stack from '#mui/material/Stack';
import Divider from '#mui/material/Divider';
import Box from '#mui/material/Box';
import Grid from '#mui/material/Grid';
import List from '#mui/material/List';
import ListItem from '#mui/material/ListItem';
import ListItemButton from '#mui/material/ListItemButton';
import IconButton from '#mui/material/IconButton';
import DeleteIcon from '#mui/icons-material/Delete';
import ListItemIcon from '#mui/material/ListItemIcon';
import ListItemText from '#mui/material/ListItemText';
import Checkbox from '#mui/material/Checkbox';
import Typography from '#mui/material/Typography';
import { getToDos, updateToDo, selectToDos } from '../toDoSlice';
import useAuth from '../../../hooks/useAuth';
function ToDoList() {
const dispatch = useDispatch();
const { user } = useAuth();
const toDos = useSelector(selectToDos);
useEffect(() => {
dispatch(getToDos(user));
}, [dispatch, user])
const handleChange = (toDoId, updateAttributes) => {
dispatch(updateToDo({ user, toDoId, updateAttributes }));
// dispatch(getToDos(user)); // This sometimes causes the GET request to occur before the PUT request, which I don't understand
}
return (
<Stack spacing={2}>
<Divider sx={{ marginTop: 5 }} />
<Grid xs={4} item>
{toDos?.toDos?.length ?
<Box>
<List>
{toDos?.toDos?.map((toDo) => (
<ListItem
key={toDo?.id}
secondaryAction={
<IconButton
edge="end"
onClick={() => handleChange(toDo?.id, { 'deleted': !toDo?.deleted })}
>
<DeleteIcon />
</IconButton>
}
>
<ListItemButton onClick={() => handleChange(toDo?.id, { 'completed': !toDo?.completed })}>
<ListItemIcon>
<Checkbox
name={toDo.task}
checked={toDo.completed}
edge='start'
/>
</ListItemIcon>
<ListItemText
primary={toDo.task}
sx={{ textDecoration: toDo.completed ? 'line-through' : null }}
primaryTypographyProps={{
style: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
: <Typography align='center' variant="h6" component="div" mt={2}>No To-Dos</Typography>
}
</Grid >
</Stack >
)
}
export default ToDoList
How do I perform a GET request after the POST or PUT operations? Where should I then put the dispatch(getToDos(user))? The comments in my code show the results of the methods I've already tried
After reading through the Redux-Toolkit docs again and looking through other SO posts, I learned of the proper way to perform asyncThunk calls in series.
Instead of performing them in my slice, I moved the calls to my component and used Promises to execute them. In order to catch the error from the request and have access to it inside the component, you have to use Toolkit's rejectWithValue parameter inside the asyncThunk.
Here's an example:
export const loginUser = createAsyncThunk('user/loginUser', async (loginData, { rejectWithValue }) => {
try {
const response = await axios.post('/login', loginData);
return response.data
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
})
By adding { rejectWithValue } to the parameter and return rejectWithValue(err.response.data), you can access the response in the component, like this:
const handleSubmit = () => {
if (loginData?.email && loginData?.password) {
dispatch(loginUser(loginData))
.unwrap()
.then(() => {
// perform further asyncThunk dispatches here
})
.catch(err => {
console.log(err) // the return rejectWithValue(err.response.data) is sent here
});
}
}

Next.js localStorage not defined even using useEffect

I know, there is a lot of similar questions although I could not find a solution to my problem. It is the first time I am using Next.js and TypeScrypt.
I am simulating a login with REQRES storing the token in the localStorage as shown below:
import {
FormControl,
FormLabel,
Input,
Heading,
Flex,
Button,
useToast,
} from '#chakra-ui/react';
import { useRouter } from 'next/router';
import { useState } from 'react';
import LStorage from '../utils/localStorage/index';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleEmail = (e: any) => setEmail(e.target.value);
const handlePassword = (e: any) => setPassword(e.target.value);
const router = useRouter();
const toast = useToast();
const success = () => toast({
title: 'Login Successfull',
description: 'You will be redirected now.',
status: 'success',
duration: 1200,
isClosable: true,
});
const failure = (error: string) => toast({
title: 'Login unsuccessfull',
description: error,
status: 'error',
duration: 3000,
isClosable: true,
});
const login = async () => {
const res = await fetch('/api', {
method: 'POST',
body: JSON.stringify({
email,
password,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
});
const json = await res.json();
console.log(json);
if (json.error) {
failure(json.error);
setEmail('');
setPassword('');
} else {
LStorage.set('userToken', json.token);
LStorage.set('userInfo', email);
success();
setTimeout(() => {
router.push('/users');
}, 1500);
}
};
return (<div>
<Flex justifyContent="center">
<Heading my="5">Login</Heading>
</Flex>
<FormControl>
<FormLabel htmlFor="email">Email:</FormLabel>
<Input id="email" type="email" onChange={handleEmail} value={email}/>
<FormLabel htmlFor="password">Password:</FormLabel>
<Input id="password" type="password" onChange={handlePassword} value={password}/>
</FormControl>
<br />
<Button onClick={login}>Login</Button>
</div>);
};
export default Login;
which seem to work fine. Although when trying to get the userInfo from localStorage at the _app.tsx component I get the localStorage not defined, looking for the error I found out the solution below inside the useEffect.
import '../styles/globals.sass';
import { ChakraProvider } from '#chakra-ui/react';
import type { AppProps } from 'next/app';
import { useState, useEffect } from 'react';
import NavBar from '../components/NavBar';
import MainLayout from '../layouts/mainLayout';
import theme from '../styles/theme';
import LStorage from '../utils/localStorage/index';
function MyApp({ Component, pageProps }: AppProps) {
const [userInfo, setUserInfo] = useState<string | null>(null);
const logout = () => {
LStorage.remove('userToken');
LStorage.remove('userInfo');
setUserInfo(null);
};
useEffect(() => {
if (typeof window !== 'undefined') {
if (LStorage.get('userInfo')) {
setUserInfo(LStorage.get('userInfo'));
}
}
console.log('i am here');
}, []);
return (
<ChakraProvider theme={theme}>
<NavBar user={userInfo} logout={logout} />
<MainLayout>
<Component {...pageProps}/>
</MainLayout>
</ChakraProvider>
);
}
export default MyApp;
I understood that the first run will be on the server-side and that is why I got the error, nevertheless, using the useEffect should fix it. The thing is the useEffect does not even run unless I refresh the page... What am I missing??!??
The Login.js is a page inside page folder and the NavBar is a component inside components folder in the root.
import {
Flex, Spacer, Box, Heading, Button,
} from '#chakra-ui/react';
import Link from 'next/link';
import { FC } from 'react';
interface NavBarProps {
user: string | null;
logout: () => void;
}
const NavBar: FC<NavBarProps> = ({ user, logout }: NavBarProps) => (
<Flex bg="black" color="white" p="4">
<Box p="2">
<Heading size="md">
<Link href="/">My Sanjow App</Link>
</Heading>
</Box>
<Spacer />
{user && (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/users">Users</Link>
</Heading>
</Box>
)}
{user ? (
<Button
variant="ghost"
pr="4"
onClick={logout}
>
<Heading size="md">
<Link href="/">Logout</Link>
</Heading>
</Button>
) : (
<Box pt="2" pr="4">
<Heading size="md">
<Link href="/login">Login</Link>
</Heading>
</Box>
)}
</Flex>
);
export default NavBar;
The utils/localStorage/index
const lsType = {
set: 'setItem',
get: 'getItem',
remove: 'removeItem',
};
const ls = (type: string, itemName: string, itemData?: string): void | string => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-undef
const LS = window.localStorage;
if (type === lsType.set && itemData) {
LS[type](itemName, itemData);
return;
}
return LS[type](itemName);
}
};
export default {
set(itemName: string, itemData: string): void {
ls(lsType.set, itemName, itemData);
},
get(itemName: string): string {
return ls(lsType.get, itemName) as string;
},
remove(itemName: string): void {
ls(lsType.remove, itemName);
},
};
You are running the effect only once by passing the [] empty array, pass the props that you expect to change instead of a blank array.
via the docs:
If you want to run an effect and clean it up only once (on mount and >unmount), you can pass an empty array ([]) as a second argument. This tells >React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
Generally speaking, managing userInfo only via localStorage is not a good idea, since you might want to re-render the application when user logs in or logs out, or any other change to the user data (i.e. change the username), and React is not subscribed to changes done to localStorage.
Instead, React has an instrument for runtime data management like that, it's called React Context. That context (let's call it UserContext) could be initializing from localStorage, so that the case when you refresh the page for example. But after that initial bootstrapping all state management should go thru the context. Just don't forget to update both context and localStorage every time you login/logout.
I hope this is just enough to give you the right direction.

How to fetch data before render functionnal component in react js

Here Below my code I would like to retrieve all data before starting the render of my component, is there any way to do that in react ? I guess it's maybe a simple code line but as I'm new in coding I still don't know all react components behavior. Thanks for your answer.
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
// import material ui
import CircularProgress from "#mui/material/CircularProgress";
import Box from "#mui/material/Box";
// import config file
import { SERVER_URL } from "../../configEnv";
const Products = ({ catList }) => {
// catList is data coming from app.js file in format Array[objects...]
console.log("catList ==>", catList);
const [isLoading, setIsLoading] = useState(true);
const [dataSku, setDataSku] = useState([]);
console.log("datasku ==>", dataSku);
const tab = [];
useEffect(() => {
// Based on the catList tab I fetch additionnal data linked with each object of catList array
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
// The result I receive from the call is an array of objects that I push inside the Tab variable
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}, [catList]);
return isLoading ? (
<Box sx={{ display: "flex" }}>
{console.log("there")}
<CircularProgress />
</Box>
) : (
<div className="products-container">
<div>LEFT BAR</div>
<div>
{dataSku.map((elem) => {
return (
<div>
<h2>{elem.name}</h2>
</div>
);
})}
</div>
</div>
);
};
export default Products; ```
#Jessy use your loading state to fetch data once,
In your useEffect, check for loading,
useEffect(() => {
if(loading) {
catList.slice(0, 2).forEach(async (element) => {
const { data } = await axios.post(`${SERVER_URL}/products`, {
product_skus: element.product_skus,
});
tab.push({ name: element.name, content: data });
setDataSku(tab);
console.log("tab ==>", tab);
setIsLoading(false);
});
}
}, [catList]);`
I finally managed to displayed all results by adding this condition on the isLoading
if (tab.length === catList.length) {
setIsLoading(false);
}
Many thanks guys for your insight :)

Making an axios get request and using React useState but when logging the data it still shows null

When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.
Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?
import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';
import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';
const Details = () => {
const [ countryData, setCountryData ] = useState(null);
let country;
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [])
return (
<>
<Container className="details">
<Row>
<Col sm={6}>
<DetailImage />
</Col>
<Col sm={6}>
<DetailInfo
name={countryData.name}
population={countryData.population}
region={countryData.region}
subRegion={countryData.subRegion}
capital={countryData.capital}
topLevelDomain={countryData.topLevelDomain}
currencies={countryData.currencies}
language={countryData.language}
/>
</Col>
</Row>
</Container>
</>
)
}
export default Details;
The child component below......
import React from 'react';
const DetailInfo = (props) => {
const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
return (
<>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
)
}
export default DetailInfo;
Ultimately, the problem comes down to not handling the intermediate states of your component.
For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.
const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
status: "loading",
data: null,
error: null
});
useEffect(() => {
let unsubscribed = false;
fetchData()
.then((response) => {
if (unsubscribed) return; // unsubscribed? do nothing.
setDataInfo({
status: "fetched",
data: response.data,
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
console.error('Failed to fetch remote data: ', err);
setDataInfo({
status: "error",
data: null,
error: err
});
});
return () => unsubscribed = true;
}, []);
switch (dataInfo.status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {dataInfo.error.message}
</div>
);
}
// render data using dataInfo.data
return (
/* ... */
);
If this looks like a lot of boiler plate, there are useAsyncEffect implementations like #react-hook/async and use-async-effect that handle it for you, reducing the above code to just:
import {useAsyncEffect} from '#react-hook/async'
/* ... */
const {status, error, value} = useAsyncEffect(() => {
return fetchData()
.then((response) => response.data);
}, []);
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {error.message}
</div>
);
}
// render data using value
return (
/* ... */
);
Because state only update when component re-render. So you should put console.log into useEffect to check the new value:
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL).then(res => {
console.log(res.data[0]);
setCountryData(res.data[0]);
});
}, []);
useEffect(() => {
console.log(countryData);
}, [countryData]);
useState does reflecting its change immediately.
I think that it would be probably solved if you set countryData to second argument of useEffect.
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [countryData])
The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.
import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";
const Details = () => {
const [countryData, setCountryData] = useState({});
let country = "USA";
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
console.log(country);
queryURL += country;
console.log(queryURL);
axios
.get(queryURL)
.then((res) => {
console.log(res.data[0]);
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData);
});
}, []);
return (
<Container className="details">
<Row>
<Col sm={6}>
<DetailInfo
name={countryData?.name}
population={countryData?.population}
region={countryData?.region}
subRegion={countryData?.subRegion}
capital={countryData?.capital}
language={countryData?.language}
/>
</Col>
<Col sm={6}></Col>
</Row>
</Container>
);
};
export default Details;
Checkout my Codesandbox here for an example.

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