Add more data to state whilst keeping original (React) - reactjs

I have an app that get data from an API as a page. I've added functionality to get the next page of the data and set it to state.
I want to be able to get the next pages data but rather than replace the state I want to add the next page value.
Here is the code
const TopRatedPage = () => {
const [apiData, setApiData] = useState([]);
const [isLoading, setLoading] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const { results = [] } = apiData;
useEffect(() => {
setLoading(true);
fetchTopRatedMovies(pageNumber).then((data) => setApiData(data));
setLoading(false);
}, [apiData, pageNumber]);
return (
<div className='top-rated-page-wrapper'>
<h1>TopRatedPage</h1>
{isLoading ? <h1>Loading...</h1> : <MovieList results={results} />}
<button
onClick={() => {
setPageNumber(pageNumber + 1);
}}>
MORE
</button>
</div>
);
I've tried setApiData(...apiData,data) but it gives error, apiData is not iterable.
Here is the data returned Object { page: 1, total_results: 5175, total_pages: 259, results: (20) […] }
To clarify I want to be able to allow user to click button that adds more API data to state and display more.

Updated to reflect the shape of the object returned by your api call.
One way to handle this, as demonstrated below, is to replace the page metadata (page, total_results, total_pages) in your component's state with the info from latest api call, but append the results each time.
If you don't care about the paging information you could just drop it on the floor and store only the results, but keeping it around lets you display paging info and disable the 'load more' button when you get to the end.
Displaying the current page and total pages might not make sense given that you're just appending to the list (and thus not really "paging" in the UI sense), but I've included it here just to provide a sense of how you might use that info if you chose to do so.
The key thing in the snippet below as it relates to your original question is the state merging:
const more = () => {
fetchTopRatedMovies(page + 1)
.then(newData => {
setData({
// copy everything (page, total_results, total_pages, etc.)
// from the fetched data into the updated state
...newData,
// append the new results to the old results
results: [...data.results, ...newData.results]
});
});
}
I'm using spread syntax to copy all of the fields from newData into the new state, and then handling the results field separately because we want to replace it with the concatenated results. (If you left the results: [...data.results, ...newData.results] line out, your new state would have the results field from newData. By specifying the results field after ...newData you're replacing it with the concatenated array.
Hope this helps.
/*
Your api returns an object with the following shape.
I don't know what the individual results entries look like,
but for our purposes it doesn't really matter and should be
straightforward for you to tweak the following code as needed.
{
page: 1,
total_results: 5175,
total_pages: 259,
results: [
{
title: 'Scott Pilgrim vs. The World',
year: 2010,
director: 'Edgar Wright'
},
{
title: 'Spiderman: Into the Spider-Verse',
year: 2018,
director: ['Bob Persichetti', 'Peter Ramsey', 'Rodney Rothman']
},
{
title: 'JOKER',
year: 2019,
director: 'Todd Phillips'
},
];
}
*/
const {useState} = React;
// simulated api request; waits half a second and resolves with mock data
const fetchTopRatedMovies = (page = 0) => new Promise((resolve) => {
// 5 is arbitrary
const numPerPage = 5;
// generate an array of mock results with titles and years
const results = Array.from(
{length: numPerPage},
(_, i) => ({
title: `Movie ${(page - 1) * numPerPage + i}`,
year: Math.floor(Math.random() * 20) + 2000
})
);
// 16 is arbitrary; just to show how you can disable
// the 'more' button when you get to the last page
const total_results = 16;
// the return payload
const data = {
page,
total_results,
total_pages: Math.floor(total_results / numPerPage),
results
}
setTimeout(() => resolve(data), 500);
});
const Demo = () => {
const [data, setData] = useState({ page: 0, results: [] });
const more = () => {
fetchTopRatedMovies(page + 1)
.then(newData => {
setData({
// copy everything (page, total_results, total_pages, etc.)
// from the fetched data into the updated state
...newData,
// append the new results to the old results
results: [...data.results, ...newData.results]
});
});
}
// pull individual fields from state
const {page, results, total_results, total_pages} = data;
// are there more pages? (used to disable the button when we get to the end)
const hasMore = page === 0 || (page < total_pages);
return (
<div className='wrapper'>
<div>
{total_pages && (
<div>
<div>Page: {page} of {total_pages}</div>
<div>Total: {total_results}</div>
</div>
)}
<button disabled={!hasMore} onClick={more}>More</button>
<ul>
{results.map(m => (
<li key={m.title}>{m.title} - ({m.year})</li>
))}
</ul>
</div>
<div>
<h3>component state:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
);
};
ReactDOM.render(
<Demo />,
document.getElementById('demo')
);
/* purely cosmetic. none of this is necessary */
body {
font-family: sans-serif;
line-height: 1.6;
}
.wrapper {
display: flex;
background: #dedede;
padding: 8px;
}
.wrapper > * {
flex: 1 1 50%;
background: white;
margin: 8px;
padding: 16px;
}
h3 {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.0/umd/react-dom.production.min.js"></script>
<div id="demo" />

Related

How can i use filter instead of map in MERN app

I want to fetch data from document using nodejs I am using similar method that we use to display product detail page everything is working but map operator gets data with similar names twice how can i prevent that can i use filter instead of map and how....
this is my code for fetching data
return (
<>
{feeds &&
feeds.map((data) => ( // Problem is here
<Link to={`/car-brand/${data?.brand}`}>
<Flex
alignItems={"center"}
justifyContent="center"
flexDirection={"row"}
marginTop={"1rem"}
sx={{
"#media screen and (max-width: 960px)": {
flexDirection: "column",
},
}}
>
<div>
<motion.div
whileHover={{
scale: 1.1,
boxShadow:
"rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px",
}}
style={CardStyle}
>
<img
style={Image}
// src={data.thumbnailImg}
alt="Card image cap"
/>
</motion.div>
</div>
</Flex>
</Link>
))}
</>
);
What i want looks like below
Document
_id = any id
brand = xyz
Document
_id = any id
brand = hfgj
Document
_id = any id
brand = xyz
As u can see above list document 1 and 3 have unique id but brand property is same now what i want is to get only 1 and 2 document bcz 1 and 3 has same value in brand property can i achieve this and how.....?
This is my code for fetching data
const User = require("../models/User");
const Image = require("../models/Image");
const addImage = async (req, res, next) => {
const newImage = new Image({ userId: req.user.id, ...req.body });
try {
const saveImage = await newImage.save();
res.status(200).json("Image uploaded");
} catch (error) {
next(error);
}
};
// GETPRODUCTBYID :-
const getImage = async (req, res) => {
try {
const image = await Image.findById(req.params.id);
res.status(200).json(image);
} catch (error) {
res.status(500).json(error);
}
};
// GET ALL PRODUCTS :-
const getAllImages = async (res) => {
try {
const images = await Image.find();
res.status(200).json(images);
} catch (error) {
res.status(500).json(error);
}
};
// GET IMAGES BY BRAND :-
const getImagesByBrand = async (req, res) => {
const qBrand = req.query.brand;
try {
const images = await Image.find( {brand: qBrand});
res.status(200).json(images);
} catch (error) {
res.status(500).json(error);
}
};
module.exports = Object.freeze({
addImage,
getImage,
getImagesByBrand,
getAllImages,
});
What i am doing is i am using GET IMAGES BY BRAND method to fetch data which gives me all three document now i want only 1 and 2 is it possible?
The map operator should be use in the return method, because you must return the JSX element to display.
There is two things that you can do:
Filter the array before sending to the front
Use the javascript Set object to remove all double object
Here you can find the documentation: https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Set

ApolloClient V3 with React: Caching and re-using result of fetchMore query

I have a small learning project build with React, TS, & Apollo.
As backend I am using https://graphqlzero.almansi.me/
The result I am looking for is:
Fetch Posts by page, 5 per page
Re-use previously fetched data
Here is my container
export const GET_POSTS = gql`
query GetPosts($options: PageQueryOptions!) {
posts(options: $options) {
data {
id
title
body
}
}
}
`
const Posts = (): JSX.Element => {
const [page, setPage] = useState<number>(1)
const { data, fetchMore} = useQuery(GET_POSTS, {
variables: {
options: {
paginate: {
page: 1,
limit: 5,
},
},
},
nextFetchPolicy: "cache-first"
},
)
return (
<div>
<ListOfPosts {...{data,fetchMore,page,setPage }} />
</div>
)
}
and ListOfPosts
const ListOfPosts = ({ data,fetchMore, page, setPage }) => {
const getNextPage = (): void => {
fetchMore({
variables: {
options: {
paginate: {
page: page + 1,
limit: 5,
},
},
},
})
setPage((p: number) => p + 1)
}
const getPrevPage = (): void => {
setPage((p: number) => (p === 0 ? p : p - 1))
}
console.log(data)
return (
<div>
<p>current page{page}</p>
<button type="button" onClick={getPrevPage}>
Get Prev Page
</button>
<button type="button" onClick={getNextPage}>
Get Next Page
</button>
{data &&
data?.posts?.data?.map((post: any) => (
<div key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</div>
)
So if I send a query with page === 1 I get posts from 1 to 5, if page === 2 - posts from 6 to 10 and so on.
The problem is that if for example I send request in next sequence
page === 1 (initially sent by useQuery)
page === 2 ( sending with fetchMore)
page === 3 ( sending with fetchMore)
page === 2 ( sending with fetchMore)
on last request Apollo performs network request, despite data for that that request is already in cache
So my questions actually are:
how to configure Apollo cache to return required data without re-fetching it from a server
How to "invalidate" that data and tell Apollo that I need refresh that particular portion of data?
I think it should be somehow configured in cache typePolicies but haven't found a way to make it work - despite data is in cache( I can track it with browser extension) it is not returned in {data}=useQuery :/
Here is how my cache config looks like.
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
merge(existing, incoming) {
return [ ...existing,...incoming ]
}
}
}
}
}
})
So there are two types of pagination. In the first pagination, the "pages" are somehow known to the user and the user navigates through these via the UI. This seems to be what you want to do:
I would propose to keep it simple: Change the state variables, pass them into useQuery and render the results. Apollo will run the query again with the new page value every time the variables change. If Apollo has seen the same variables before (you go back to the previous page), Apollo can return the result from cache.
export const GET_POSTS = gql`
query GetPosts($options: PageQueryOptions!) {
posts(options: $options) {
data {
id
title
body
}
}
}
`
const Posts = (): JSX.Element => {
const [page, setPage] = useState<number>(1)
const { data } = useQuery(GET_POSTS, {
variables: {
options: {
paginate: {
page,
limit: 5,
},
},
},
nextFetchPolicy: "cache-first"
})
return (
<div>
<ListOfPosts {...{data,page,setPage }} />
</div>
)
}
const ListOfPosts = ({ data, page, setPage }) => {
const getNextPage = (): void => setPage((p: number) => p + 1);
const getPrevPage = (): void =>
setPage((p: number) => (p === 0 ? p : p - 1))
return (
<div>
<p>current page{page}</p>
<button type="button" onClick={getPrevPage}>
Get Prev Page
</button>
<button type="button" onClick={getNextPage}>
Get Next Page
</button>
{data &&
data?.posts?.data?.map((post: any) => (
<div key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</div>
)
}
Merging the cache is only relevant when you want to continuously show results after another. Things like "load more" or "infinite scrolling". This means you want to continue adding to your result list. You would add more and more posts to the view and the old pages don't disappear. This is what fetchMore is designed for. If you want to do that have a look at the docs and this part specifically. Your problem is that you are currently mixing both approaches, which probably leads to weird results.

How do I display a placeholder element at current insertion point during drag and drop operation?

I have a list that I can sort with drag and drop using react, and it works fine. The way it works is onDragEnter, the items get replaced. What I want to do though, is show a placeholder element once the dragging item is hovering over available space. So the final placement would happen in onDragEnd. I have two functions that handle dragging:
const handleDragStart = (index) => {
draggingItem.current = index;
};
const handleDragEnter = (index) => {
if (draggingWidget.current !== null) return;
dragOverItem.current = index;
const listCopy = [...rows];
const draggingItemContent = listCopy[draggingItem.current];
listCopy.splice(draggingItem.current, 1);
listCopy.splice(dragOverItem.current, 0, draggingItemContent);
if (draggingItem.current === currentRowIndex) {
setCurrentRowIndex(dragOverItem.current);
}
draggingItem.current = dragOverItem.current;
dragOverItem.current = null;
setRows(listCopy);
};
and in react jsx template, I have this:
{rows.map((row, index) => (
<div
key={index}
draggable
onDragStart={() => handleDragStart(index)}
onDragEnter={() => handleDragEnter(index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={handleDragEndRow}
>
...
</div>
Can anyone come with any tips as to how I might solve this?
To display a placeholder indicating where you are about to drop the dragged item, you need to compute the insertion point according to the current drag position.
So dragEnter won't do, dargOver is best suited to do that.
When dragging over the first half of the dragged overItem, the placeholder insertion point will be before the dragged over item, when dragging over the second half, it will be after. (see getBouldingClientRect, height/2 usages, of course if dragging horizontally width will need to be accounted for).
The actual insertion point (in the data, not the UI), if drag succeeds, will depend on if we're dropping before or after the initial position.
The following snippet demonstrate a way of doing that with the following changes in your initial code:
Avoided numerous refs vars by putting everything in state, especially because changing these will have an effect on the UI (will need rerender)
Avoided separate useState calls by putting all vars in a common state variable and a common setState modifier
Avoided unnecessary modifications of the rows state var, rows should change only when drag ends as it's easier to reason about it => the placeholder is not actually part of the data, it serves purpose only in the ui
Avoided defining handler in the render code onEvent={() => handler(someVar)} by using dataset key data-drag-index, the index can retrieved after using this key: const index = element.dataset.dragIndex. The handler can live with the event only which is automatically passed.
Avoided recreating (from the children props point of view) these handlers at each render by using React.useCallback.
The various css class added show the current state of each item but serves no functionnal purpose.
StateDisplay component also serves no purpose besides showing what happens to understand this answer.
Edit: Reworked and fixed fully working solution handling all tested edge cases
const App = () => {
const [state,setState] = React.useState({
rows: [
{name: 'foo'},
{name: 'bar'},
{name: 'baz'},
{name: 'kazoo'}
],
draggedIndex: -1,
overIndex: -1,
overZone: null,
placeholderIndex: -1
});
const { rows, draggedIndex, overIndex, overZone, placeholderIndex } = state;
const handleDragStart = React.useCallback((evt) => {
const index = indexFromEvent(evt);
setState(s => ({ ...s, draggedIndex: index }));
});
const handleDragOver = React.useCallback((evt) => {
var rect = evt.target.getBoundingClientRect();
var x = evt.clientX - rect.left; // x position within the element.
var y = evt.clientY - rect.top; // y position within the element.
// dataset variables are strings
const newOverIndex = indexFromEvent(evt);
const newOverZone = y <= rect.height / 2 ? 'top' : 'bottom';
const newState = { ...state, overIndex: newOverIndex, overZone: newOverZone }
let newPlaceholderIndex = placeholderIndexFromState(newOverIndex, newOverZone);
// if placeholder is just before (==draggedIndex) or just after (===draggedindex + 1) there is not need to show it because we're not moving anything
if (newPlaceholderIndex === draggedIndex || newPlaceholderIndex === draggedIndex + 1) {
newPlaceholderIndex = -1;
}
const nonFonctionalConditionOnlyForDisplay = overIndex !== newOverIndex || overZone !== newOverZone;
// only update if placeholderIndex hasChanged
if (placeholderIndex !== newPlaceholderIndex || nonFonctionalConditionOnlyForDisplay) {
newState.placeholderIndex = newPlaceholderIndex;
setState(s => ({ ...s, ...newState }));
}
});
const handleDragEnd = React.useCallback((evt) => {
const index = indexFromEvent(evt);
// we know that much: no more dragged item, no more placeholder
const updater = { draggedIndex: -1, placeholderIndex: -1,overIndex: -1, overZone: null };
if (placeholderIndex !== -1) {
// from here rows need to be updated
// copy rows
updater.rows = [...rows];
// mutate updater.rows, move item at dragged index to placeholderIndex
if (placeholderIndex > index) {
// inserting after so removing the elem first and shift insertion index by -1
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex - 1, 0, rows[index]);
} else {
// inserting before, so do not shift
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex, 0, rows[index]);
}
}
setState(s => ({
...s,
...updater
}));
});
const renderedRows = rows.map((row, index) => (
<div
key={row.name}
data-drag-index={index}
className={
`row ${
index === draggedIndex
? 'dragged-row'
: 'normal-row'}`
}
draggable
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
{row.name}
</div>
));
// there is a placeholder to show, add it to the rendered rows
if (placeholderIndex !== -1) {
renderedRows.splice(
placeholderIndex,
0,
<Placeholder />
);
}
return (
<div>
{renderedRows}
<StateDisplay state={state} />
</div>
);
};
const Placeholder = ({ index }) => (
<div
key="placeholder"
className="row placeholder-row"
></div>
);
function indexFromEvent(evt) {
try {
return parseInt(evt.target.dataset.dragIndex, 10);
} catch (err) {
return -1;
}
}
function placeholderIndexFromState(overIndex, overZone) {
if (overZone === null) {
return;
}
if (overZone === 'top') {
return overIndex;
} else {
return overIndex + 1;
}
}
const StateDisplay = ({ state }) => {
return (
<div className="state-display">
{state.rows.map(r => r.name).join()}<br />
draggedIndex: {state.draggedIndex}<br />
overIndex: {state.overIndex}<br />
overZone: {state.overZone}<br />
placeholderIndex: {state.placeholderIndex}<br />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
.row { width: 100px; height: 30px; display: flex; align-items: center; justify-content: center; }
.row:nth-child(n+1) { margin-top: 5px; }
.row.normal-row { background: #BEBEBE; }
.row.placeholder-row { background: #BEBEFE; }
.row.normal-row:hover { background: #B0B0B0; }
.row.placeholder-row:hover { background: #B0B0F0; }
.row.dragged-row { opacity: 0.3; background: #B0B0B0; }
.row.dragged-row:hover { background: #B0B0B0; }
.state-display { position: absolute; right: 0px; top: 0px; width: 200px; }
<html><body><div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.0/umd/react-dom.production.min.js"></script></body></html>

Rendering multiple EditorJS components

I'm using EditorJS on a React page to allow people to write in a block-based editor. However, I also want to build sections, where a user can have multiple sections and each section can support an EditorJS component
I'm running into an issue when I add a new section, and want to render an empty EditorJS component for this new section (and keep the data from the old section and EditorJS instance). Instead of an empty instance, it copies over the information from the old instance and assigns it to the new Section. Type definitions are below
types.d.ts
interface User {
...
sections: Section[],
}
interface Section {
id: string,
name: string,
content: ContentBlock,
}
interface ContentBlock {
id: string,
threads: Thread[],
content: OutputData, //this is the EditorJS saved data
}
I'm wondering if EditorJS is keeping some sort of global state that it's applying to every instance of itself in my application. Does anyone have experience with spinning up multiple editorJS instances?
For reference, I have two components: Page.tsx and Section.tsx. Relevant code is below
//Page.tsx
const Page: React.FC = () => {
const [allSections, setAllSections] = useState<Section[]>([]);
const [currSectionID, setCurrSectionID] = useState("");
const addNewSection = (type: string) => {
const newID = uuidv4();
const newSection: Section = {
id: newID,
name: "",
content: emptyContentBlock,
};
setAllSections(arr => [...arr, newSection]);
setCurrSectionID(newID);
};
const updateContentForSection = (contentBlock: ContentBlock, sectionID: string) => {
const newSectionArray = [...allSections];
newSectionArray.forEach((section: Section) => {
if (section.id === sectionID) {
section.content = contentBlock
}
});
setAllSections(newSectionArray);
};
return (
<Section
sectionID={currSectionID}
sections={allSections}
pageActions = {{
addNewSection: addNewSection,
updateContentForSection: updateContentForSection,
}}
/>
)
}
//Section.tsx
const Section: React.FC<SectionInput> = (props) => {
const currSection = props.sections.filter(section => section.id === props.sectionID)[0];
const blocks = currSection? currSection.content.content : [];
const [editorInstance, setEditorInstance] = useState<EditorJS>();
const saveEditorData = async() => {
if (editorInstance) {
const savedData = await editorInstance.save();
console.log(`saving data to section ${props.sectionID}`, savedData);
props.pageActions.updateContentForSection({content: savedData, id: props.sectionID, threads: threads}, props.sectionID);
}
}
}
return (
<div>
<button
className={`absolute top-0 right-12 mt-2 focus:outline-none`}
onClick={() => {
props.pageActions.addNewSection()
}}
>
Add Section
</button>
<EditorJs
key="0"
holder="custom"
data={blocks}
autofocus={true}
instanceRef={(instance: EditorJS) => {
setEditorInstance(instance)
}}
onChange={saveEditorData}
tools={EDITOR_JS_TOOLS}
>
<div
id="custom"
>
</div>
</EditorJs>
</div>
)
So according to this github thread, the answer is actually straightforward. Use a unique ID for each editorJS ID for each editor you want to have in the DOM. The code, then, becomes something like this
<EditorJs
key={`${sectionID}`}
holder={`custom${sectionID}`}
data={blocks}
autofocus={true}
instanceRef={(instance: EditorJS) => {
setEditorInstance(instance)
}}
onChange={saveEditorData}
tools={EDITOR_JS_TOOLS}
>
<div
id={`custom${sectionID}`}
>
</div>
</EditorJs>

Fetch more content as user is scrolling down

I have a page with a search input, once the user click on submit results come up.
There can be a lot of results and I don't want to load them all at once, how can I fetch more data into the page using Lodash throttle on mouse move?
This is my react component:
const getContacts = async (searchString) => {
const { data: contactsInfo} = await axios.get(`api/Contats/Search?contactNum=${searchString}`);
return contactsInfo;
};
export default class Home extends React.Component {
state = {
contactsInfo: [],
searchString: '',
};
handleSubmit = async () => {
const { searchString } = this.state;
const contactsInfo = await getContacts(searchString);
this.setState({ contactsInfo });
};
onInputChange = e => {
this.setState({
searchString: e.target.value,
});
};
onMouseMove = e => {
};
render() {
const { contactsInfo, searchString, } = this.state;
return (
<div css={bodyWrap} onMouseMove={e => this.onMouseMove(e)}>
<Header appName="VERIFY" user={user} />
{user.viewApp && (
<div css={innerWrap}>
<SearchInput
searchIcon
value={searchString || ''}
onChange={e => this.onInputChange(e)}
handleSubmit={this.handleSubmit}
/>
{contactsInfo.map(info => (
<SearchResultPanel
info={info}
isAdmin={user.isAdmin}
key={info.id}
/>
))}
</div>
)}
<Footer />
</div>
);
}
}
I supposed that, using getContacts() you retrieve ALL the contacts, and then you just want to show them at some rate, like showing the first 20, then when you reach the last one, another 20s appear.
Just asking because this is really different from "let's fetch the first 20 contacts, show them, and when the user reaches the last one, fetch another 20s".
So, if the first assumption I've made it's correct, I can raccomend you to use the Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
This is really useful in case like yours (it's even written in the documentation "Lazy-loading of images or other content as a page is scrolled.").
The idea is that you should add this Intersection Observer, and start the observation on the last image: this observator will run a callback as soon as the last image appears on the screen (you can even decide the percentage of the image that must be on the screen).
For example, you can say that, as soon as 1px of the image appear on the screen, you add another 20s images!
Notice that, once another 20s images are shown, you must unobserve the currect observed image, and observe the new last image!
I can also suggest to not put the observer on the last image, but maybe on the third last.
EDIT: I'm not sure this answers your question. It does if I consider the title "Fetch more content as user is scrolling down", but it does not actually use mouseover (even though I think this implementation is the best one for your goal).
EDIT2: There it goes, I've added the fiddle, and here there is the codepen: https://codepen.io/Gesma94/pen/OqpOQb
Note that I've simulated the contacts with divs of different color. What is going on is that, when the third last contact (div) appear on the screen, new contacts are added in the state. Right now the contacts are just empty objects, but you can run a fetch or doing whatever you want inside fetchMoreContent(). Is this clear enough? :) I've commented the code too.
/* Just a function that create a random hex color. */
function randomColor() {
let randomColor = '#';
const letters = '0123456789ABCDEF';
for (let i = 0; i < 6; i++) {
randomColor += letters[Math.floor(Math.random() * 16)];
}
return randomColor;
}
class Home extends React.Component {
contactList = null; // Ref to the div containing the contacts.
contactObserved = null; // The contact which is observed.
intersectionObserver = null; // The intersectionObserver object.
constructor(props) {
super(props);
this.contactList = React.createRef();
this.state = {
loading: true,
contactsToShow: 0,
contacts: []
};
}
componentDidMount() {
/* Perform fetch here. I'm faking a fetch using setTimeout(). */
setTimeout(() => {
const contacts = [];
for (let i=0; i<100; i++) contacts.push({});
this.setState({loading: false, contacts, contactsToShow: 10})}, 1500);
}
componentDidUpdate() {
if (!this.state.loading) this.handleMoreContent();
}
render() {
if (this.state.loading) {
return <p>Loading..</p>
}
return (
<div ref={this.contactList}>
{this.state.contacts.map((contact, index) => {
if (index < this.state.contactsToShow) {
const color = contact.color || randomColor();
contact.color = color;
return (
<div
className="contact"
style={{background: color}}>
{color}
</div>
);
}
})}
</div>
);
}
handleMoreContent = () => {
/* The third last contact is retrieved. */
const contactsDOM = this.contactList.current.getElementsByClassName("contact");
const thirdLastContact = contactsDOM[contactsDOM.length - 3];
/* If the current third last contact is different from the current observed one,
* then the observation target must change. */
if (thirdLastContact !== this.contactObserved) {
/* In case there was a contact observed, we unobserve it and we disconnect the
* intersection observer. */
if (this.intersectionObserver && this.contactObserved) {
this.intersectionObserver.unobserve(this.contactObserved);
this.intersectionObserver.disconnect();
}
/* We create a new intersection observer and we start observating the new third
* last contact. */
this.intersectionObserver = new IntersectionObserver(this.loadMoreContent, {
root: null,
threshold: 0
});
this.intersectionObserver.observe(thirdLastContact);
this.contactObserved = thirdLastContact;
}
}
loadMoreContent = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
let contactsCounter = this.state.contacts.length;
let contactsToShow = this.state.contactsToShow + 10;
if (contactsToShow > contactsToShow) contactsToShow = contactsToShow;
this.setState({contactsToShow});
}
})
}
}
ReactDOM.render(<Home />, document.getElementById('root'));
#import url(https://fonts.googleapis.com/css?family=Montserrat);
body {
font-family: 'Montserrat', sans-serif;
}
.contact {
width: 200px;
height: 100px;
border: 1px solid rgba(0,0,0,0.1);
}
.contact + .contact {
margin-top: 5px;
}
<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>

Resources