Meteor / React App - Very Slow with Lots of Data - performance

I built an application and it was fast and cool until I added a bunch of data to test it. I started with Contacts collection only and added 28,000 records. Now my Contacts Page takes at least 30 to 60 seconds to load and sometimes even crashes the browser.
I understand that 28,000 records is not that much, plus I can load other data that is related to Contacts, like Charges that can be approximately 80,000 of records based on my logic where each Contact can have 2-3 records of Charges. So loading the page with Contacts and Charges only (there might be more other data), would need to get approximately 110,000 records be loaded until I let to render a page. Here is the code example (similar but my page is much more complicated):
import React, { PropTypes } from "react";
import { createContainer } from "meteor/react-meteor-data";
import { Contacts } from '../api/contacts.js';
import { Charges } from '../api/charges.js';
class SingleContactRow extends React.Component {
renderCharges() {
const {contact, charges} = this.props;
let contactCharges = [];
charges.forEach(function(charge) {
if (charge.contactId === contact._id) {
contactCharges.push(charge);
}
});
return contactCharges.map((charge, i) => (
<div key={charge._id}>
Charge #{i}: {charge.name} _ ${charge.amount}
</div>
));
}
render() {
const {contact} = this.props;
return (
<div><b>{contact.name}</b></div>
<div>{this.renderCharges()}</div>
);
}
}
class ContactsComponent extends React.Component {
renderContacts() {
const {contacts, charges} = this.props;
return contacts.map((contact) => (
<SingleContactRow key={appointment._id} contact={contact} charges={charges} />
));
}
render() {
const {loadingContacts, loadingCharges} = this.props;
if (!loadingContacts && !loadingCharges) {
return(
<div>
{this.renderContacts()}
</div>
);
} else {
return(
<div>Loading ... </div>
);
}
}
}
ContactsComponent.propTypes = {
contacts: PropTypes.array.isRequired,
charges: PropTypes.array.isRequired,
};
export default createContainer(({ params }) => {
const contactsHandle = Meteor.subscribe('contacts');
const loadingContacts = !contactsHandle.ready();
const chargesHandle = Meteor.subscribe('charges');
const loadingCharges = !chargesHandle.ready();
return {
loadingContacts,
contacts: Contacts.find({}, { sort : { createdAt : -1 } }).fetch(),
loadingCharges,
charges: Charges.find({}).fetch(),
};
}, ContactsComponent);
What I do above:
I subscribe to data
I wait until all of the data gets loaded, and in the meanwhile render to a user "Loading ..." message
Then I render Contacts
Then loop through all of the Charges and push into an Array the charges that belong to that particular Contact (I see problem in here)
Return Charges for each Contact
If I had 30,000 records of Contacts and 90,000 records of Charges this page would take at least a minute or two to render. How to improve this situation?

Related

Combining useEffect, useContext, and/or useSWR to minimise renders on static pages with dynamic content

I suspect I'm overcomplicating things, and there's a much simpler solution here, but I'm new to NextJS ... and am clearly doing it wrong.
I'm generating contest landing pages from a headless CMS using NextJS ("the contest page"). Any given user might enter multiple contests, and go to any one/many of the contest pages.
The requirement is, the contest page displays how many "points" the user has in the contest, and there are components where if the user takes certain actions (e.g., clicking on them) he gets more points.
Which buttons are displayed and how many points each is worth are defined in the CMS.
So, my current thinking (which isn't working) is:
a tuple of [userId, contestId] is stored in localStorage for each contest entered (they enter by submitting a form on a different landing page, which redirects to one of these that I'm currently building)
the contest page is wrapped in a context provider, so the action components know to which [userId, contestId] they should be adding points in the database; and so the display component knows the current point value
SWR queries for the point value, so it can do its magic of "fast/lightweight/realtime"
Where I currently am:
// /pages/contests/[contest].js
export default function ContestEntryPage({ data }) {
return (
<ContestProvider>
<PageLandingPage data={data} />
</ContestProvider>
);
}
And then the landing page component:
// /components/PageLandingPage.js
const fetcher = async ({pageId, contestId, clientId}) => {
const res = await fetch({
pathname: '/api/getpoints',
query: {
pageId,
contestId,
clientId
}
});
return res.json();
}
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest();
let registration, points;
if (data?.contestPage?.id) {
registration = // complicated parsing to extract the right contest from localStorage
const { data: fetchedData, error } = useSWR({
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId
}, fetcher);
points = fetchedData;
}
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
dispatchContest({
payload: {
contestId: registration.contestId,
clientId: registration.clientId,
points: points
},
type: 'update'
})
}, [data, points, registration, dispatchContest])
return (
<div>
Awesome content from the CMS
</div>
)
}
export default PageLandingPage
As written above, it gets stuck in an infinite re-rendering loop. Previously, I had the SWR data fetching inside of the useEffect, but then it complained about calling a hook inside of useEffect:
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest();
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
const registration = // get from localStorage
const { data: points, error } = useSWR({
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId
}, fetcher);
dispatchContest({
payload: {
contestId: registration.contestId,
clientId: registration.clientId,
points: points
},
type: 'update'
})
}, [data, dispatchContest])
Obviously, I haven't even gotten to the point of actually displaying the data or updating it from sub-components.
What's the right way to combine useEffect, useContext, and/or useSWR to achieve the desired outcome?
Though I suspect it's not relevant, for completeness' sake, here's the useContext code:
import { createContext, useContext, useReducer } from 'react';
const initialState = {
contestId: null,
clientId: null
};
const ContestContext = createContext(initialState);
function ContestProvider({ children }) {
const [contestState, dispatchContest] = useReducer((contestState, action) => {
return {
...contestState,
...action.payload
}
}, initialState);
return (
<ContestContext.Provider value={{ contestState, dispatchContest }}>
{children}
</ContestContext.Provider>
);
}
function useContest() {
const context = useContext(ContestContext);
if (context === undefined) {
throw new Error('useContest was used outside of its provider');
}
return context;
}
export { ContestProvider, useContest }

Render Contentful Reference (many) Array on Gatsby

I am pretty new to using Contentful and their Reference (many) field type. I have one reference type that pulls in many product names. in GraphQL I can see all my product name displaying, but when I try and render it on Gatsby I am not seeing anything display (productName:array). Here is my GraphQL
{
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
and here is my code...
import React from 'react';
import { graphql, StaticQuery } from 'gatsby';
const Products = () => (
<StaticQuery
query={graphql`
query MyQuery {
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
`}
render={data => (
<div>
{data.allContentfulAppetizerMenuSection.nodes.map(({ menuItemReferences }, i) => (
<div key={i}>
{menuItemReferences.productName}
</div>
))}
</div>
)}
/>
)
export default Products;
Any help will be much appreciated.
Try:
const Products = () => {
const data = useStaticQuery(graphql`
query {
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
`);
return <div>
{data.allContentfulAppetizerMenuSection.nodes.map(({ menuItemReferences }, i) => {
return <div key={i}>
{menuItemReferences.productName}
</div>;
})}
</div>;
};
export default Products;
Note the usage of useStaticQuery hook and the return statement in your loop.
The refactor upon useStaticQuery results in a cleaner and reusable code but the idea it's exactly the same, if you are more familiar with the old StaticQuery, you can keep it.
If the error persists, try to debug inside the loop.

Loading text shown on fetching pagination requests

I have a react component using react-apollo fetching a list. The server is using relay style connection.
My question is when I fetch for next page, it shows the "Loading..." and then the page cursor moves back to the top, shown in the following.
https://imgur.com/a/ImfQPVJ
I want to have a better UX that no "Loading..." text shown, and after fetching, just append the newly fetched result to the back of the list.
This is the code (remove non-relevant code):
class Links extends Component {
fetchNextPage = (pageInfo, fetchMore) => ev => {
const { linksOrder } = this.props;
const after = pageInfo.endCursor;
const queryVars = getQueryVarsFromParam(linksOrder, after);
fetchMore({
variables: queryVars,
updateQuery: (previousResult, { fetchMoreResult }) => {
const { allLinks: { nodes: newLinks, pageInfo: newPageInfo }} = fetchMoreResult;
// Handle no new result
if (newLinks.length === 0) return previousResult;
// With new result, we append to the previous result list
let finalResult = previousResult;
finalResult.allLinks.pageInfo = newPageInfo;
finalResult.allLinks.nodes = finalResult.allLinks.nodes.concat(newLinks);
return finalResult;
}
})
}
render() {
const { linksOrder, classes } = this.props;
const { linkGqlCursorAfter: after } = this.state;
const queryVars = getQueryVarsFromParam(linksOrder, after);
return(<Query query={LINKS_QUERY_GQL} variables={queryVars}>
{({ loading, error, data, fetchMore }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const { allLinks: { nodes: links, pageInfo }} = data;
return(
<React.Fragment>
<Grid container spacing={8} className={ classes.linksList } >
{ links.map((link, ind) => (
<Grid item key={link.id}><Link ind={ind} link={link}/></Grid>
)) }
{ pageInfo.hasNextPage ? (
<Grid item key={ "fetchNextPage" } style={{ alignSelf: "center" }}>
<Fab onClick={ this.fetchNextPage(pageInfo, fetchMore) }
size="small" color="secondary"><AddIcon /></Fab>
</Grid>)
: null }
</Grid>
</React.Fragment>
)
} }
</Query>)
}
}
How could I achieve that? Another approach I could think of is not to use <Query> tag at all, and retrieve the Apollo client itself via HOC in onClick hander, fetch the result, and add the result back to the links object by updating the component state.
Then, this begs the question of why we want to use <Query> <Mutation>, when we can always get the Apollo client and handle the query interaction better ourselves?
Since you already have the data you fetched from previous pages in your cache you can render that conditionally in your if (loading) while waiting for new data to be fetched.
if(loading) {
return data.allLinks ?
<>
<LinksComponent {/*pass in the old data here...*/}/>
<Spinner/>
</> : <Spinner/>
}
If you already have data.allLinks you will display that data in a LinksComponent even when new data is being fetched. The Spinner component will be displayed under the LinksComponent while Loading is true. If you don't have any data fetched, you will just display the Spinner component.

Relay Modern node does not contain fragment properties

I have the following setup in my React Project:
export default class OverviewScreen extends React.Component<any, any> {
public render() {
return (
<QueryRenderer
environment={environment}
query={OverviewScreenQuery}
render={this.queryRender}/>
);
}
protected queryRender({error, props}): JSX.Element {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return (
<div>
<div>
<ActivityOfferList viewer={props.viewer} title="Titel"/>
<ActivityTypeListsFragment viewer={props.viewer}/>
</div>
</div>
);
}
return <div>Loading...</div>;
}
}
const OverviewScreenQuery = graphql`
query OverviewScreenQuery {
viewer {
...HorizontalOfferList_viewer
...ActivityTypeLists_viewer
}
}`;
class ActivityTypeLists extends React.Component<IHorizontalOfferListProps, any> {
public render() {
return (
<div>
{this.props.viewer.allActivityTypes.edges.map((typeEdge) => {
let typeNode = typeEdge.node;
return this.getCardListForActivityType(typeNode);
})}
</div>
);
}
private getCardListForActivityType(typeNode: any) {
console.log(typeNode);
return (
<CardList key={typeNode.__id} title={typeNode.title}>
{typeNode.activities.edges.map(({node}) => {
return (
<RelayPicturedTypeActivityCard key={node.__id} offer={node} activityType={typeNode}/>
);
})}
</CardList>
);
}
}
export const ActivityTypeListsFragment = createFragmentContainer(ActivityTypeLists, graphql`
fragment ActivityTypeLists_viewer on Viewer {
allActivityTypes(first: 5) {
edges {
node {
...PicturedTypeActivityCard_offer
}
}
}
}
`);
export class PicturedTypeActivityCard extends React.Component<any, any> {
public render() {
return (
<PicturedCard title={this.props.offer.title} subtitle={this.props.activityType.title} width={3}/>
);
}
}
export const RelayPicturedTypeActivityCard = createFragmentContainer(PicturedTypeActivityCard, graphql`
fragment PicturedTypeActivityCard_offer on ActivityType {
title
activities(first: 4) {
edges {
node {
id
title
}
}
}
}
`);
Which should work and give me the correct result from the graphcool relay endpoint.
The Network call to the relay endpoint is indeed correct and I receive all the ActivityTypes and their activities and titles from my endpoint.
But somehow in the function getCardListForActivityType() the typeNode only contains the __id of the node as data and no title at all:
If I insert title and activities directly instead of using
...PicturedTypeActivityCard_offer
then the data also gets passed down correctly. So something with the Fragment must be off.
Why is it that the network call is complete and uses the fragment correctly to fetch the data, but the node object never gets the fetched data?
This is indeed correct behavior.
Your components must, individually, specify all their own data dependencies, Relay will only pass to the component the data it asked for. Since your component is not asking any data, it's receiving an empty object.
That __id you see is used internally by Relay and you should not rely on it (that is why it has the __ prefix).
Basically, the prop viewer on ActivityTypeLists component will have exactly the same format than the query requested on the ActivityTypeLists_viewer fragment, without any other fragments from other components that you are referencing there.
This is known as data masking, see more in the following links:
https://facebook.github.io/relay/docs/en/thinking-in-relay.html#data-masking
https://facebook.github.io/relay/docs/en/graphql-in-relay.html#relaymask-boolean

ReactJS pass props to child via redux ajax

I have a reactjs component with redux which passes asynchronously props to child component.
In child component I try to catch the data in componentDidMount but somehow does not work either, however the child component is getting rendered.
This is my parent component
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as slidesActions from '../../actions/slidesActions';
import Slider from '../Partials/Slider'
import _ from 'underscore';
class HomePage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.actions.getSlides()
}
componentWillMount() {
const {slides} = this.props;
}
render() {
const {slides} = this.props;
return (
<div className="homePage">
<Slider columns={1} slides={slides} />
</div>
);
}
}
function mapStateToProps(state) {
return {
slides: state.slides
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(slidesActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
here comes my child component where I try to get passed slides props but is empty
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
//TODO: work on logic
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
//here I try set a state variable on slides
let {slides} = this.props
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next)
this.hammer.off('swiperight', this.prev)
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
//TODO: this should go to slides actions
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
and here I have my actions for slider
import * as types from './actionTypes';
import axios from 'axios';
import _ from 'underscore';
//TODO: this should be accessed from DataService
if (process.env.NODE_ENV === 'development') {
var slidesEndPoint = 'http://dev.server/wp-json/wp/v2/slides';
} else {
var slidesEndPoint = 'http://prod.server/wp-json/wp/v2/slides';
}
export function getSlides () {
return dispatch => {
dispatch(setLoadingState()); // Show a loading spinner
axios.get(slidesEndPoint)
.then(function (response) {
dispatch(setSlides(response.data))
dispatch(doneFetchingData(response.data))
})
/*.error((response) => {
dispatch(showError(response.data))
})*/
}
}
function setSlides(data) {
return {
type: types.SLIDES_SUCCESS,
slides: data
}
}
function setLoadingState() {
return {
type: types.SHOW_SPINNER,
loaded: false
}
}
function doneFetchingData(data) {
return {
type: types.HIDE_SPINNER,
loaded: true,
slides: data
}
}
function showError() {
return {
type: types.SHOW_ERROR,
loaded: false,
error: 'error'
}
}
Reason is, componentDidMount will get called only once, just after the initial rendering, since you are fetching the data asynchronously so before you get the data Slider component will get rendered.
So You need to use componentwillreceiveprops lifecycle method.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
componentWillReceiveProps:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Write it like this:
componentWillReceiveProps(nextProps){
if(nextProps.slides){
let {slides} = nextProps.props
let slidesLength = slides.length;
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
this.setState({slidesLength})
}
}
As far as I understand, you are doing an axios call to fetch the data and then set it in the reducer which you are returning later. Also initially reducer data is empty . Now since componentDidMount is called only once, and initially no data may have been there you are not seeing any values. Use a componentWillReceiveProps function
componentWillReceiveProps(nextProps) {
//here I try set a state variable on slides
let {slides} = nextProps
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}

Resources