For about 7 hours now, Am working with infinite Scrolling using react redux.
This code works very well by displaying 20 database records as user scroll-down the page. But am currently face with two issues.
1.) I cannot get the application to display a message "No more records" once record gets finished displaying from database.
I have tried
get finished() {
console.log(this.props.users_scroll.length);
if (this.row >= this.props.users_scroll.length ) {
return (<li key={'done'}>No More Message to Load.</li>);
}
return null;
}
but console give values undefined for this line of code
console.log(this.props.users_scroll.length);
I have also tried
get finished() {
console.log(this.loadMore.length);
if (this.row >= this.loadMore.length ) {
return (<li key={'done'}>No More Message to Load.</li>);
}
return null;
}
but console give values 0 for this line of code console.log(this.loadMore.length); as a result the application will immediately
shows No more records whereas they are still about 15 records in the database.
2.) When all the 20 records from database gets display, there is still continuous Call from server/database as long as the user
keeps on scrolling down the Page. I have tried this but it does not stop the unecessary Server/database Calls. it seems that this.users_scroll
is either empty or undefined
if(this.users_scroll !='' ){
loadMore();
}
Here is the code
import React, { Component, Fragment } from "react";
import { render } from "react-dom";
import request from "superagent";
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { userActions } from '../_actions';
class InfinitescrollPage extends React.Component {
constructor(props) {
super(props);
// Sets up our initial state
this.state = {
users_scroll: [],
};
//set parameters
this.row = 0;
this.rowperpage = 5;
this.cText = "";
this.loadMore = this.loadMore.bind(this);
// Binds our scroll event handler
window.onscroll = () => {
const {
loadMore,
state: {
},
} = this;
// Checks that the page has scrolled to the bottom
if (
window.innerHeight + document.documentElement.scrollTop
=== document.documentElement.offsetHeight
) {
//if(this.users_scroll !='' ){
loadMore();
//}
}
};
}
componentWillMount() {
// Loads some users on initial load
this.props.dispatch(userActions.getAll_Infinitescroll(this.row));
}
loadMore() {
this.cText = "content is Loading ...";
this.row+=this.rowperpage;
setTimeout(()=>{
this.len =this.props.dispatch(userActions.getAll_Infinitescroll(this.row));
this.cText = "";
},100);
}
get finished() {
//alert('database rows lengths ' +this.props.users_scroll.length);
//if (this.row >= this.loadMore.length ) {
if (this.row >= this.props.users_scroll.length ) {
return (<li key={'done'}>No More Message to Load.</li>);
}
return null;
}
render() {
//const {} = this.state;
const {user_scroll, users_scroll } = this.props;
return (
<div>
<h1>Infinite Users!</h1>
<p>Scroll down to load more!!</p>
{users_scroll.items2 && users_scroll.items2.map(user_scroll => (
<Fragment key={user_scroll.username}>
<hr />
<div style={{ display: 'flex' }}>
<img
alt={user_scroll.username}
src='http://localhost/apidb_react/2.png'
style={{
borderRadius: '50%',
height: 72,
marginRight: 20,
width: 72,
}}
/>
<div>
<h2 style={{ marginTop: 0 }}>
#{user_scroll.uuid}
</h2>
<p>Name: {user_scroll.name}</p>
<p>Email: {user_scroll.email}</p>
<p>Counting: {user_scroll.rcount}</p>
</div>
</div>
</Fragment>
))}
{this.finished}
{this.cText}
<input type="text" className="form-control" name="this.row" id="this.row" value={this.row} onChange={this.handleChange} />
<hr />
</div>
);
}
}
const container = document.createElement("div");
document.body.appendChild(container);
render(<InfinitescrollPage />, container);
users.service
function getAll_Infinitescroll(us) {
const request = {
method: 'POST',
body: JSON.stringify({us});
};
return fetch(`/users.php`, request).then(handleResponse)
.then(users_scroll => {
if (users_scroll) {
console.log(users_scroll);
}
return users_scroll;
});
}
This has been resolved. I need to get the row counts from the database and the compare it with the passing row data.
1.) To display No more message it becomes
this.count='20';
or
this.count=this.props.dispatch(userActions.getRowCount());
get finished() {
if (this.row >= this.count ) {
return (<li key={'done'}>No More Message to Load.</li>);
}
return null;
}
2.) To prevent unnecessary scrolling when there is no more data to display
loadMore() {
if (this.row != this.count ) {
this.cText = "content is Loading ...";
this.row+=this.rowperpage;
setTimeout(()=>{
this.len =this.props.dispatch(userActions.getAll_Infinitescroll(this.row));
this.cText = "";
},100);
}
}
Related
I am encountering a browser infinite looping issue in my NextJS project when I add the SSRKeycloakProvider component from #react-keycloak/ssr npm package. This infinite loop only happens with a specific application the Reports page.
My investigations have led me to believe it is related to the way cookies are handled with keycloak integration, and in the Reports page I am also using windows.replaceState() javascript function.
Everytime the loop starts, this is the message I get from the application:
[Function: setReportq]
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
at Overflow (/home/ubuntu/growth-admin/node_modules/rc-overflow/lib/Overflow.js:42:32)
at SelectSelector (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Selector/MultipleSelector.js:36:18)
at div
at Selector (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Selector/index.js:38:35)
at Trigger (/home/ubuntu/growth-admin/node_modules/rc-trigger/lib/index.js:79:36)
at SelectTrigger (/home/ubuntu/growth-admin/node_modules/rc-select/lib/SelectTrigger.js:74:25)
at div
at BaseSelect (/home/ubuntu/growth-admin/node_modules/rc-select/lib/BaseSelect.js:67:18)
at Select (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Select.js:66:18)
at InternalSelect (/home/ubuntu/growth-admin/node_modules/antd/lib/select/index.js:55:31)
at div
at div
at div
at Col (/home/ubuntu/growth-admin/node_modules/antd/lib/grid/col.js:59:33)
at FormItemInput (/home/ubuntu/growth-admin/node_modules/antd/lib/form/FormItemInput.js:44:25)
at div
at Row (/home/ubuntu/growth-admin/node_modules/antd/lib/grid/row.js:56:34)
at FormItem (/home/ubuntu/growth-admin/node_modules/antd/lib/form/FormItem.js:101:20)
at form
at Form (/home/ubuntu/growth-admin/node_modules/rc-field-form/lib/Form.js:33:19)
at SizeContextProvider (/home/ubuntu/growth-admin/node_modules/antd/lib/config-provider/SizeContext.js:19:23)
at InternalForm (/home/ubuntu/growth-admin/node_modules/antd/lib/form/Form.js:66:27)
at div
at Report (webpack-internal:///./common/components/DominoReport/index.js:152:3)
at div
at appClassicstyle__ContentWrapper (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:19220)
at div
at appClassicstyle__AppWrapper (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:19220)
at AppClassic (webpack-internal:///./common/components/Admin/report.js:27:3)
at div
at main
at Basic (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:78:25)
at Content (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at section
at BasicLayout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:93:34)
at Layout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at section
at BasicLayout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:93:34)
at Layout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at exports.ThemeProvider (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:24917)
at Growth (webpack-internal:///./containers/Admin/growth.js:81:3)
at AppClassic (webpack-internal:///./pages/report.js:19:3)
at ThemeProvider (/home/ubuntu/growth-admin/node_modules/#material-ui/styles/ThemeProvider/ThemeProvider.js:48:24)
at KeycloakProvider (/home/ubuntu/growth-admin/node_modules/#react-keycloak/core/lib-commonjs/provider.js:72:51)
at SSRKeycloakProvider (/home/ubuntu/growth-admin/node_modules/#react-keycloak/ssr/lib-commonjs/SSRKeycloakProvider.js:64:28)
at CustomApp (webpack-internal:///./pages/_app.tsx:63:3)
at StylesProvider (/home/ubuntu/growth-admin/node_modules/#material-ui/styles/StylesProvider/StylesProvider.js:57:24)
at ae (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:13296)
at AppContainer (/home/ubuntu/growth-admin/node_modules/next/dist/server/render.js:293:29)
The above message mentions the parseCookies function (in _app.tsx:63:3) ,
the pages/index.tsx line 20 which is basically this line:
const parsedToken: ParsedToken | undefined = keycloak?.tokenParsed
,the windows.replaceState() function setting the URL params in the Growth component, and the specific reportq() function which is also in the Growth.
Here is my _app.tsx, I believe the parseCookies function is of interest here:
import React, { useEffect } from "react"
import App from 'next/app'
import { SSRKeycloakProvider, SSRCookies } from '#react-keycloak/ssr'
import cookie from 'cookie'
import type { IncomingMessage } from 'http'
import { ThemeProvider } from '#material-ui/core/styles';
import theme from '../theme';
const KC_URL = process.env.NEXT_PUBLIC_KC_URL;
const KC_REALM = process.env.NEXT_PUBLIC_KC_REALM
const KC_CLIENT_ID = process.env.NEXT_PUBLIC_KC_CLIENT_ID
const keycloakCfg = {
realm: KC_REALM,
url: KC_URL,
clientId: KC_CLIENT_ID
}
interface InitialProps {
cookies: unknown
}
export default function CustomApp({ Component, pageProps, cookies }) {
const initOptions = {
onLoad: 'login-required',
checkLoginIframe: false
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakCfg}
persistor={SSRCookies(cookies)}
initOptions={initOptions}
>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</SSRKeycloakProvider>
);
}
// I think the cookies and this function have to do with the issue
function parseCookies(req?: IncomingMessage) {
if (!req || !req.headers) {
return {}
}
return cookie.parse(req.headers.cookie || '')
}
CustomApp.getInitialProps = async (appContext) => {
// Your static props paths
const staticPropsPaths = [
"/paper/[paperId]/[paperName]",
"/hubs"
]
if (process.browser || !staticPropsPaths.includes(appContext.router.route)) {
const appProps = await App.getInitialProps(appContext)
return { ...appProps, cookies: parseCookies(appContext?.ctx?.req) }
}
}
And here is my index.tsx:
import type { NextPage } from 'next'
import { useKeycloak } from '#react-keycloak/ssr'
import type { KeycloakInstance, KeycloakTokenParsed } from 'keycloak-js'
import Growth from '../containers/Admin/growth'
type ParsedToken = KeycloakTokenParsed & {
email?: string
preferred_username?: string
given_name?: string
family_name?: string
}
const Home = ({ query }) => {
const { keycloak } = useKeycloak<KeycloakInstance>()
const parsedToken: ParsedToken | undefined = keycloak?.tokenParsed
const loggedinState = keycloak?.authenticated ? (
<span className="text-success">logged in</span>
) : (
<span className="text-danger">NOT logged in</span>
)
const welcomeMessage =
keycloak?.authenticated || (keycloak && parsedToken)
? `Welcome back ${parsedToken?.preferred_username ?? ''}!`
: 'Welcome ! Please login to continue.'
return <Growth query={query} page={'home'} />
}
Home.getInitialProps = async ({ query, res }) => {
return { query }
}
export default Home
the Growth code is below: and I believe the line of interest in the Growth component:
window.history.replaceState('state', 'Growth ', `${BASE_URL}${page}${reportq}`)
The Growth Component:
const Growth = ({ query, page }) => {
const { keycloak, initialized } = useKeycloak()
const router = useRouter()
let [p, setP] = useState(page)
let reportq;
let prospectq;
// I believe this function is involved
const setReportq = (params) => {
window.sessionStorage.setItem("reportq", params)
}
const setProspectq = (params) => {
window.sessionStorage.setItem("prospectq", params)
}
useEffect(() => {
if (typeof window !== 'undefined') {
reportq = window.sessionStorage.getItem('reportq');
if (!reportq)
reportq = '';
prospectq = window.sessionStorage.getItem('prospectq');
if (!prospectq)
prospectq = '';
}
if (typeof window !== 'undefined' && page == 'report') {
const keys = Object.keys(query);
let params = "?";
for (let i = 0; i < keys.length; i++) {
params += `${keys[i]}=${encodeURIComponent(query[keys[i]])}&`
}
// console.log("eval reportq vs query", { reportq, params })
if (!reportq && keys && keys.length > 0) {
console.log("growth line 78 setting reportq")
setReportq(params)
}
else {
if (params != reportq) {
console.log("growth line 81 updating url with ", reportq)
window.history.replaceState('state', 'Growth ', `${BASE_URL}${page}${reportq}`)
}
}
}
})
const key = query.key;
const setPage = (page) => {
setP(page)
setTimeout(() => {
const newUrl = `${BASE_URL}${page == 'home' ? '' : page}${page == 'report' ? reportq : page == 'prospect' ? prospectq ? prospectq : `?key=${key}` : `?key=${key}`}`
router.push(newUrl);
}, 1);
}
return (
<ThemeProvider theme={theme}>
<>
<Head>
<title>Growth</title>
<meta name="robots" content="noindex" />
<meta name="theme-color" content="#2563FF" />
</Head>
<Layout>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
}}
>
<Menu theme="dark" mode="inline" defaultSelectedKeys={[page]}>
<Menu.Item key="report" onClick={() => setPage('report')}>
Reports
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout" style={{ marginLeft: 200 }}>
{p == 'report' && <Report query={query} setReportq={setReportq} />}
</Layout>
</Layout>
</>
</ThemeProvider >);
}
Growth.getInitialProps = async ({ query, res }) => {
return { query }
}
export default Growth;
Change onLoad: 'login-required' to onLoad: 'check-sso',
I'm implementing an infinite scroll with Apollo and React. Everything works fine. When I navigate away from Feed and then back to Feed I'm getting this weird error:
TypeError: Cannot read property 'fetchMore' of undefined
Has anyone else had experience with this issue? I found this but there doesn't seem to yet be any solutions. One of the answers mentions a partial solution "checking the route before executing fetchMore" but I don't know what means.
I'm still in the middle of development so I haven't had a chance to clean this component up yet, but here it is:
import React, { useEffect, useRef } from 'react';
import { useQuery } from '#apollo/client';
import PostUpdateOrShow from '../posts/types/showOrUpdate/PostUpdateOrShow.js'
import Cookies from 'js-cookie';
import Queries from '../../graphql/queries';
import InfiniteScroll from './util/Infinite_Scroll.js';
const { FETCH_USER_FEED, FETCH_TAG_FEED } = Queries;
const Feed = ({
user, tag
}) => {
let fetchMoreDiv = useRef(null);
let cursorId = useRef(null);
useEffect(() => {
var scroll = document.addEventListener('scroll', function(event) {
fetchMoreDiv.current = document.querySelector('#fetchMore')
var el = fetchMoreDiv.current.getBoundingClientRect()
var elTop = el.top
var elBottom = el.bottom
var innerHeight = window.innerHeight
if (elTop >= 0 && elBottom <= innerHeight) {
fetchMore({
query: gqlQuery,
variables: {
query: query,
cursorId: cursorId.current
},
})
}
})
return () => {
document.removeEventListener('scroll', scroll)
}
})
var gqlQuery
var query
if (user) {
gqlQuery = FETCH_USER_FEED
query = user.blogName
} else if (tag) {
gqlQuery = FETCH_TAG_FEED
query = tag.title.slice(1)
} else {
gqlQuery = FETCH_USER_FEED
query = Cookies.get('currentUser')
}
let { loading, error, data, fetchMore } = useQuery(gqlQuery, {
variables: {
query: query,
cursorId: null
},
})
if (loading) return 'Loading...';
if (error) return `Error: ${error}`;
const { fetchUserFeed, fetchTagFeed } = data
cursorId.current = fetchUserFeed ? fetchUserFeed[fetchUserFeed.length - 1]._id :
fetchTagFeed[fetchTagFeed.length - 1]._id
if (tag) {
return(
<div>
<div>
{fetchTagFeed.map((post, i) => {
return (
<div
className='post'
key={post._id}
>
<PostUpdateOrShow
post={post}
/>
</div>
)
})}
</div>
<InfiniteScroll
fetchMoreDiv={fetchMoreDiv}
/>
<div
id='fetchMore'
>
</div>
</div>
)
} else {
return(
<div>
<div>
{fetchUserFeed.map((post, i) => {
return (
<div
className='post'
key={post._id}
>
<PostUpdateOrShow
post={post}
/>
</div>
)
})}
</div>
<InfiniteScroll
fetchMoreDiv={fetchMoreDiv}
/>
<div
id='fetchMore'
>
</div>
</div>
)
}
}
export default Feed;
Apollo client config:
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
fetchLikesRepostsAndComments: {
merge: (existing = [], incoming = []) => {
return incoming
}
},
fetchUserFeed: {
keyArgs: ['query'],
merge: (existing = [], incoming = []) => {
const elements = [...existing, ...incoming].reduce((array, current) => {
return array.map(i => i.__ref).includes(current.__ref) ? array : [...array, current];
}, []);
return elements
},
}
}
}
}
}),
errorLink
})
I believe the issue is that you are using fetchMore within useEffect hook.
Try to rethink your code to avoid this. Using fetchMore outside the hook would work flawlessly.
I have a button that changes state when clicked. I would like a style (resultInfo) to show up on another element when the button state is false. I tried to use useEffect [checkAnswer] to update the other element's inline style, but upon the button's state change, the other element's style is not updated. How come the following doesn't work? Thanks.
// the Continue button that also shows the user if they are correct or wrong
import React, { useEffect, useContext, useState } from 'react';
import { PracticeContext } from '../contexts/PracticeContext';
import ModuleFinished from './ModuleFinished';
// Enables the user to check their answer, shows the result, and provides an element to proceed to the next question
const ModulePracticeAnswerResult = ( {questionNumber, answer, attempt} ) => {
const { setShowModuleFinished } = useContext(PracticeContext);
const { questionIndex, setQuestionIndex } = useContext(PracticeContext);
const { selectedPracticeEnd } = useContext(PracticeContext);
const [checkAnswer, setCheckAnswer] = useState(false);
// create the user selected module for practice
useEffect(() => {
setCheckAnswer(false); // answer state is reverted when question state is updated
}, [questionIndex]);
// navigate to the next question
function progress(e) {
e.preventDefault();
if (checkAnswer === false) {
setCheckAnswer(true);
return;
}
if (selectedPracticeEnd === true) {
// there are no more questions - don't progress any further
if (checkAnswer) {
setShowModuleFinished(true);
}
return;
}
// if checkAnswer is true, user has already answers and received feedback. progress to next question
setQuestionIndex(questionNumber + 1);
}
let resultInfo = { display: 'none' };
useEffect(() => {
// when check answer button has been pressed, its state changes to false until the continue button is pressed
if (checkAnswer === false) {
// display the result of the answer
if (answer === attempt) {
resultInfo = { display: 'block', background: '#4CAF50' }
}
else {
resultInfo = { display: 'block', background: 'rgb(255, 52, 86)' }
}
return;
}
resultInfo = { display: 'none' }; // user hasn't checked the answer yet
}, [checkAnswer]);
return (
<div className="module-practice-answer-result">
<div className="result-info-container">
<div style={resultInfo}>
<p>{ 'result message here...' }</p>
</div>
<button
className={ checkAnswer === false ? 'answer-button answer-button-default' : 'answer-button answer-button-continue' }
onClick={progress} disabled={attempt.length < 1 ? 'disabled' : ''}>
{ checkAnswer === false ? 'Check' : 'Continue' }
</button>
</div>
<ModuleFinished />
</div>
);
}
export default ModulePracticeAnswerResult;
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>
Background I am trying to create a container for a collection of elements, each of which can be removed from the collection. When an element is removed, I want to animate its exit, and I am trying to achieve this using React Motion.
Here's a diagram of the above:
Problem I thought of using React Motion's TransitionMotion component, and got stuck trying to write a function that needs to be passed to it. Here is my — incorrect — code:
class Container extends Component {
state = {
elementStyles: {}
}
componentDidMount() {
this.getElementStyles();
}
componentDidUpdate() {
this.getElementStyles();
}
getElementStyles() {
if (!this.props.children.length || this.hasElementStyles()) return;
// this assumes all elements passed to the container are of equal dimensions
let firstChild = this.refs.scroller.firstChild;
let elementStyles = {
width: firstChild.offsetWidth,
height: firstChild.offsetHeight,
opacity: 1
};
this.setState({
elementStyles
});
}
hasElementStyles() {
return !isEmpty(this.state.elementStyles); // lodash to the rescue
}
willLeave() {
return { width: spring(0), height: spring(0), opacity: spring(0) }
}
getChildrenArray() {
return Children.toArray(this.props.children); // that's React's util function
}
getModifiedChild(element, props) {
if (!element) return;
return React.cloneElement(
element,
{style: props.style}
);
}
getInitialTransitionStyles() {
let elements = this.getChildrenArray();
let result = elements.map((element, index) => ({
key: element.key,
style: this.state.elementStyles
}));
return result;
}
render() {
if (this.hasElementStyles()) {
return (
<TransitionMotion
willLeave={this.willLeave}
styles={this.getInitialTransitionStyles()}
>
{ interpolatedStyles => {
let children = this.getChildrenArray();
return <div ref="scroller" className="container">
{ interpolatedStyles.map((style, index) => {
return this.getModifiedChild(children[index], style);
})
}
</div>
}}
</TransitionMotion>
);
} else {
return (
<div ref="scroller" className="container">
{ this.props.children }
</div>
);
}
}
}
Notice this line inside the map function in the TransitionMotion component: return this.getModifiedChild(children[index], style). It is wrong, because once an element is removed from the collection, this.props.children will change, and indices of the children array will no longer correspond to the indices of the styles array calculated from those children.
So I what I need is either some clever way to track the props.children before and after an element has been removed from the collection (and I can't think of one; the best I could come up with was using a find function on the array return this.getModifiedChild(children.find(child => child.key === style.key), style);, but that will result in so many loops within loops I am scared even to think about it), or to use some completely different approach, which I am not aware of. Could you please help?
This solution's delete animation slides the deleted item (and the rest of the "cards" in the row) horizontally left beneath the card on the left (using it's lower z-index).
The initial render sets up the < Motion/> tag and the cards display as a row.
Each card shares a delete button method which setStates the index of the array element and starts a timer that slices the array. The setState triggers the render which animates the cards before the timer does the slice.
class CardRow extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
cardToRemove: 99,
};
}
findCard = (_id) => {
return this.myArray.find((_card) => {
return _card.id === _id;
});
};
removeCardClick = (evt) => {
const _card = this.findCard(evt.target.id);
setTimeout(() => { // wait for the render...
this.myArray.splice(this.myArray.indexOf(_card), 1);
this.setState({cardToRemove: 99});
}, 200);
this.setState({cardToRemove: this.myArray.indexOf(_card)});
};
render() {
let items = this.myArray.map((item, index) => {
let itemLeft = 200 * index; // card width 200px
if (this.state.cardToRemove <= index) {
itemLeft = 200 * (index - 1);
}
// .cardContainer {position: fixed}
return <div key={item.id}>
<Motion style={{left: spring(itemLeft)}}>
{({left}) =>
<div className="cardContainer" style={{left: left}}>
<div className="card">
<button className="Button" id={item.id} onClick={this.removeCardClick}>Del</button>
</div>
</div>
}
</Motion>
</div>;
});
items = items.reverse();
return <div>
{items}
</div>;
}
}
That's about it! Thanks and enjoy,