I'm attempting to make a infinite scroll pagination in my React-Relay frontend, but without a success.
At this moment, I have this React component ...
class List extends Component {
state = { loading: false };
componentDidMount() {
window.onscroll = () => {
if (!this.state.loading
&& (window.innerHeight + window.scrollY)
>= document.body.offsetHeight) {
this.setState({loading: true}, () => {
this.props.relay.setVariables({
count: this.props.relay.variables.count + 5
}, (readyState) => {
if (readyState.done) {
this.setState({ loading: false });
}
});
});
}
}
}
render() {
return (
<div>
{this.props.viewer.products.edges.map(function(product, i) {
return (<Item key={i} product={product.node} />);
})}
</div>
);
}
};
... wrapped in Relay container
export default Relay.createContainer(List, {
initialVariables: {
count: 5
},
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
products(first: $count) {
total
edges {
node {
${Item.getFragment('product')}
}
}
}
}
`,
}
});
The logic behind this seems quite easy. If you reach a certain scrollY position, set to count variable a new inceremented value, which extends your list of Edges.
But this concept will lead to a situation, where in the beginning I query the database for first 5 records, but lately while I keep scrolling a will query for N+5 records. In the end I will query the database for a whole table (thousands of records)! Which is quite unacceptable.
So I'm trying to implement a cursors, but I don't know how to just fetch data from a connection and extend the result. This approach returns me a "paginated" list.
Also, the above code gives me this error, when I scroll down
resolveImmediate.js?39d8:27 Uncaught Invariant Violation: performUpdateIfNecessary: Unexpected batch number (current 19, pending 18)
I will be very thankful for any help or example!
Problem solved!
About my concern of fetching N+5 records over and over again with each scroll, it seems that Relay is quite intelligent, because it automatically "paginate" my connection by querying cursor and managing the after arg in my connection.
And with little help from graphql-sequelize on my backend, it simply turns into this query
SELECT ... FROM product ORDER BY product.id ASC LIMIT 5 OFFSET 10
Which actually solves my first problem about the performance. :)
So my Relay.QL query has underhood transpiled into this
query ListPage_ViewerRelayQL($id_0:ID!) {
node(id:$id_0) {
...F1
}
}
fragment F0 on Product {
id,
title,
price
}
fragment F1 on Viewer {
_products4tSFq4:products(after:"YXJyYXljb25uZWN0aW9uJDUkNA==",first:5) {
edges {
cursor,
node {
id,
...F0
}
},
pageInfo {
hasNextPage,
hasPreviousPage
}
},
id
}
and when I ran this query on my GraphQL backend, it returned me empty result
{
"data": {
"node": null
}
}
Then I immediately realized where is the problem and why my Relay crashes down.
The problem is with refetching the viewer. I did not properly implemented nodeDefinitions and when the viewer ID hit idFetcher and typeResolver, it failed and returned a null. After that on the client side, Relay was unable to finish the refetching of my connection and crashed down!
After small repairment and fixes on the Backend-side, my infinite scroll works like a charm! :)
Related
I have one query and one subscription, what I am trying to do is add my data to previous query so that it shows the full list.
I have one query which is returning me list of students and I am rendering that on UI like below
function Test(props) {
const { loading, data: dta } = useQuery(GETSTUDENTS);
const { data: d } = useSubscription(GETSUBSTUDENTS, {
onSubscriptionData: ({ subscriptionData: { data } }) => {
let fname = data.getSubStudent.fname;
let lname = data.getSubStudent.lname;
dta.getStudents.push({ fname, lname });
},
});
return (
<div className="">
{dta &&
dta.getStudents.map((li) => {
<div>
<p>{li.fname}</p>
<p>{li.lname}</p>
</div>;
})}
</div>
);
}
export default Test;
But the main issue is the above one is not updating the cache so when I change the routes and come bqack again it takes the previous data only.
So What I wnat to know na what is the best way to do this, I have check subscribeToMore also but did not get idea How to implement that and how it works with hooks.
I am getting some data from subscription and on that basis I want to change some other part so can I use refetchQueries I did not found any good tutorial which uses hooks (react-apollo-hooks) using qraphql
First, you can just use the pooling option of the useQuery instead of subscription,
I suggest you check it.
From Apollo docs:
"In the majority of cases, your client should not use subscriptions to
stay up to date with your backend. Instead, you should poll
intermittently with queries, or re-execute queries on demand when a
user performs a relevant action."
Apollo subscription
If you still want to use the subscription I think you should use the subscribeToMore and to update your cache policy inside the apollo cache file:
const cache = new InMemoryCache({
typePolicies: {
Agenda: {
fields: {
tasks: {
merge(existing = [], incoming: any[]) {
return [...existing, ...incoming];
},
},
},
},
},
});
You can read more about it here: merge cahce
And check that video: youtube apollo cache
Edit: Moved the getMetadata() call out of render, as suggested by #Robin Zigmond. Still having trouble getting the proper value from getMetadata() returned, but that's different from my original question. Updating code just for reference.
Quick background: I have a fair amount of experience with shell scripting and stuff like Perl and PHP, but javascript and especially the libraries on top of it like React feel very foreign to me. Forcing myself to develop this with React to teach myself something new, so if you see any bad practices, feel free to suggest improvements!
That said, I'm trying to write a simple app that:
Prompts for search parameter
Runs query against 3rd party service that returns json results
For each returned element, run additional query to get more details
Display tabular results
I have 1, 2, and 4 generally worked out, but struggling with 3. Here's what I have so far, with less important code snipped out:
class VGSC_Search extends React.Component {
constructor() {
super();
this.state = {
submitted: false,
platform: '',
games: [],
metadata: [],
files: [],
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({platform: event.target.value});
}
handleSubmit(event) {
this.setState({submitted: true});
<SNIP - vars>
fetch(encodeURI(searchurl + query + fields + sort + rows))
.then(result => result.json())
.then(data => this.setState({
games: data.response.docs.map(game => ({
identifier: game.identifier,
title: game.title,
creator: game.creator,
year: game.year,
uploader: this.getMetadata(game.identifier),
}))
}));
event.preventDefault();
}
getMetadata(id) {
<SNIP - vars>
fetch(encodeURI(metadataurl + id + metadatainfo))
.then(result => result.json())
.then(data => this.setState({metadata: data.response}));
}
renderResults() {
const {games} = this.state;
const {metadata} = this.state;
return (
<SNIP - table header>
<tbody>{games.map(game =>
<tr key={game.identifier}>
<td><a href={'https://archive.org/details/' + game.identifier}>{game.title}</a></td>
<td>{game.creator}</td>
<td>{game.year}</td>
<td>{game.uploader}</td>
</tr>
)}</tbody>
</table>
);
}
render(){
return (
<div>
<SNIP - form>
<br/>
{this.state.submitted && this.renderResults()}
</div>
);
}
}
My problem is with that Uploader field, which should run getMetadata() on the given identifier and return the name of the uploader. I'm having trouble figuring out how to reference the result, but my biggest problem is actually that my browser keeps running getMetadata() on all displayed items in an endless loop. Eg, from the Developer Tools log:
XHR GET https://archive.org/metadata/4WheelThunderEuropePromoDiscLabel/metadata/uploader [HTTP/1.1 200 OK 105ms]
XHR GET https://archive.org/metadata/AirJapanCompleteArtScans/metadata/uploader [HTTP/1.1 200 OK 279ms]
XHR GET https://archive.org/metadata/AeroWings-CompleteScans/metadata/uploader [HTTP/1.1 200 OK 287ms]
XHR GET https://archive.org/metadata/BioCodeVeronicaLEDCT1210MNTSCJ/metadata/uploader [HTTP/1.1 200 OK 279ms]
XHR GET https://archive.org/metadata/Biohazard2ValuePlusDreamcastT1214MNTSCJ/metadata/uploader [HTTP/1.1 200 OK 282ms]
XHR GET https://archive.org/metadata/4WheelThunderEuropePromoDiscLabel/metadata/uploader [HTTP/1.1 200 OK 120ms]
XHR GET https://archive.org/metadata/AirJapanCompleteArtScans/metadata/uploader [HTTP/1.1 200 OK 120ms]
<SNIP>
The first search returns 5 results, and getMetadata() is run correctly on those five results, but note that it starts repeating. It'll do that endlessly until I reload the page.
I'm guessing that has something to do with running getMetadata() inside a function that's being rendered, but I'm not sure why, and having trouble thinking of a good alternate way to do that.
Can anyone explain why I'm seeing this behavior, and (hopefully) offer a suggestion on how to properly implement this?
Thanks!
The infinite loop happens because you're running getMetadata inside render (or rather inside a function that's called from render) - and that results in changing state, which causes a rerender, and so on in an endless loop.
It's bad practice in any situation to call any function which cause any "side effects" from within render - render should simply determine the output given the component's props and state. Typically data-fetching like you're doing is done inside componentDidMount and/or componentDidUpdate. However in this case, where you appear to need to fetch additional data based on the first response in handleSubmit, it seems that you need to call getMetadata from within the final .then callback of that function.
After your latest edit, I can see the problem with the approach you tried here. this.getMetadata doesn't actually return anything. To fix it, you can return the Promise returned by fetch:
getMetadata(id) {
<SNIP - vars>
return fetch(encodeURI(metadataurl + id + metadatainfo))
.then(result => result.json())
.then(data => this.setState({metadata: data.response}));
}
and then use asyc/await inside handleSubmit, with Promise.all to manage the array:
fetch(encodeURI(searchurl + query + fields + sort + rows))
.then(result => result.json())
.then(async data => {
const games = await Promise.all(data.response.docs.map(async game => ({
identifier: game.identifier,
title: game.title,
creator: game.creator,
year: game.year,
uploader: await this.getMetadata(game.identifier),
})));
this.setState({ games });
});
I have a React component, that includes the availability flag of Internet connectivity. UI elements have to be dynamically changed according to state real-time. Also, functions behave differently with the changes of the flag.
My current implementation polls remote API using Axios in every second using interval and updates state accordingly. I am looking for a more granular and efficient way to do this task to remove the 1-second error of state with the minimum computational cost. Considered online if and only if device has an external Internet connection
Current implementation :
class Container extends Component {
constructor(props) {
super(props)
this.state = {
isOnline: false
};
this.webAPI = new WebAPI(); //Axios wrapper
}
componentDidMount() {
setInterval(() => {
this.webAPI.poll(success => this.setState({ isOnline: success });
}, 1000);
}
render() {
return <ChildComponent isOnline={this.state.isOnline} />;
}
}
Edited:
Looking for a solution capable of detecting external Internet connectivity. The device can connect to a LAN which doesn't have an external connection. So, it is considered offline. Considers online if and only if device has access to external Internet resources.
You can use https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event
window.addEventListener('offline', (event) => {
console.log("The network connection has been lost.");
});
and https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event
for checking when you're back online
window.addEventListener('online', (event) => {
console.log("You are now connected to the network.");
});
Method one: Using legacy browser API - Navigator.onLine
Returns the online status of the browser. The property returns a boolean value, with true meaning online and false meaning offline. The property sends updates whenever the browser's ability to connect to the network changes. The update occurs when the user follows links or when a script requests a remote page. For example, the property should return false when users click links soon after they lose internet connection.
You can add it to your component lifecycle:
Play with the code below using Chrome dev tools - switch "Online" to "Offline" under the Network tab.
class App extends React.PureComponent {
state = { online: window.navigator.onLine }
componentDidMount() {
window.addEventListener('offline', this.handleNetworkChange);
window.addEventListener('online', this.handleNetworkChange);
}
componentWillUnmount() {
window.removeEventListener('offline', this.handleNetworkChange);
window.removeEventListener('online', this.handleNetworkChange);
}
handleNetworkChange = () => {
this.setState({ online: window.navigator.onLine });
}
render() {
return (
<div>
{ this.state.online ? 'you\'re online' : 'you\'re offline' }
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
However, I think this isn't what you want, you wanted a real-time connection validator.
Method two: Checking internet connection by using it
The only solid confirmation you can get if the external internet connectivity is working is by using it. The question is which server you should call to minimize the cost?
There are many solutions on the internet for this, any endpoint that responds with a quick 204 status is perfect, e.g.:
calling to Google server (for it being the most battle-tested (?) )
calling its cached JQuery script endpoint (so even if the server is down, you should still be able to get the script as long as you have a connection)
try fetching an image from a stable server (e.g.: https://ssl.gstatic.com/gb/images/v1_76783e20.png + date timestamp to prevent caching)
IMO, if you are running this React app on a server, it makes the most sense to call to your own server, you can call a request to load your /favicon.ico to check the connection.
This idea (of calling your own server) has been implemented by many libraries, such as Offline, is-reachable, and is widely used across the community. You can use them if you don't want to write everything by yourself. (Personally I like the NPM package is-reachable for being simple.)
Example:
import React from 'react';
import isReachable from 'is-reachable';
const URL = 'google.com:443';
const EVERY_SECOND = 1000;
export default class App extends React.PureComponent {
_isMounted = true;
state = { online: false }
componentDidMount() {
setInterval(async () => {
const online = await isReachable(URL);
if (this._isMounted) {
this.setState({ online });
}
}, EVERY_SECOND);
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return (
<div>
{ this.state.online ? 'you\'re online' : 'you\'re offline' }
</div>
);
}
}
I believe what you have currently is already fine, just make sure that it is calling the right endpoint.
Similar SO questions:
Detect the Internet connection is offline?
Detect network connection in React Redux app - if offline, hide component from user
https://stackoverflow.com/Questions/3181080/How-To-Detect-Online-Offline-Event-Cross-Browser
Setup a custom hook
Setup a hook with the online, offline events. then update a state and return it. This way you can use it anywhere in your app with an import. Make sure you clean up with the return function. If you don't you will add more and more event listeners each time a component using the hook mounts.
const onlineHook = () => {
const {isOnline, setOnline} = React.useState();
React.useEffect(() => {
const goOnline = function(event){
setOnline(true);
});
const goOffline = function(event){
setOnline(false);
});
window.addEventListener('offline', goOffline);
window.addEventListener('online', goOnline);
return () => {
window.removeEventListener('offline', goOffline);
window.removeEventListener('online', goOnline);
}
}, [])
return isOnline
}
To use this just import the above hook and call it like this.
const isOnline = onlineHook(); // true if online, false if not
You can create a component to share between all subcomponents
used:
import React, { useState, useEffect } from "react";
export default function NetworkChecker() {
const [networkStatus, setNetworkStatus] = useState(true)
useEffect(() => {
window.addEventListener('offline', (event) => {
setNetworkStatus(false)
});
window.addEventListener('online', (event) => {
setNetworkStatus(true)
});
return function cleanupListener() {
window.removeEventListener('online', setNetworkStatus(true))
window.removeEventListener('offline', setNetworkStatus(false))
}
},[])
if (networkStatus) {
return <div className={"alert-success"}>Online</div>
} else {
return <div className={"alert-danger"}>Offline</div>
}
}
I have this code:
Template.messageList.onCreated(function onCreated() {
this.state = new ReactiveDict();
this.state.setDefault({
limit: 5
});
this.autorun(() => {
this.subscribe('messages', {limit: this.state.get('limit')});
});
});
Template.messageList.helpers({
messages () {
const instance = Template.instance();
return Messages.find({}, MessagesFilter.common({limit: instance.state.get('limit')}));
}
});
Template.messageList.events({
'click .ui.button.more' (event, instance) {
const limit = instance.state.get('limit');
instance.state.set('limit', limit + 5);
}
});
It's intended to be a infinite scroll, but it presents weird events. Right now, the elements on page are reloaded when the limit changes.
Looks like meteor is re-subscribing (which is expected by the documentation) but this is reloading all the documents, it deletes the messages and then reload the with the new limit value.
How can I prevent it? I want a infinite scroll, but the way it behaves right now looks impossible.
Here's a video of what's happening.
http://recordit.co/mlsGThOdHp
Here's the repository:
https://github.com/Sornii/sayito/tree/infinite-scroll
I changed
this.autorun(() => {
this.subscribe('messages', {limit: this.state.get('limit')});
});
To
Tracker.autorun(() => {
this.subscribe('messages', {limit: this.state.get('limit')});
});
And now it works the way I want to.
I just have a question to find out if this is possible:
So what I am doing is when I submit a post I wait for it to complete then update the user object in firebase to insert a time-stamp.
This is fine and works but when the time-stamp is inserted it is causing other subscriptions that are subscribed to changes in the user object to update.
What I want to do it update the user object without causing other subscribers to be updated.
Here is where I am updating the timestamp:
I tried commenting out this line of code which stops the data duplication issue on screen but I need this code to run as I need to update the timestamp when a post is submitted.
this.af.database.object('users/' + x[0].uid + '/lastPostAt').set(timestamp)
.then(x => { this.dialogsService.showSuccessDialog('Post Submitted'); });
Here is where I am subscribing to all the posts:
subscribeAllPosts(): Observable<Post[]> {
return this.af.database.list('/posts')
.map(Post.fromJsonList);
}
Here is where I am creating the array of posts in my constructor to display via a loop in the html:
this.activeItem = this.items[0];
this.postsService.subscribeAllPosts()
.subscribe(posts => {
let container = new Array<PostContainer>();
for (let post of posts) {
this.getEquippedItemsForUsername(post.username).subscribe(
x => {
try {
container.push(new PostContainer(post, x[0].equippedItems));
} catch (ex) { }
}
);
}
this.postContainers = container;
});
In the inner subscription it gets the equippedItems for the user of the post:
getEquippedItemsForUsername(username: string) {
return this.usersService.subscribeUserByUsername(username);
}
Which in turns calls:
subscribeUserByUsername (username: string) {
return this.af.database.list('users' , {
query: {
orderByChild: 'username',
equalTo: username
}
});
}
In the HTML it loops through the postContainer[]:
<li *ngFor="let item of postContainers">
So all that works the issue as stated is that if I do not have the following commented out then the posts will be duplicated more and more as the posts are submitted. If I refresh the app then the posts will show the correct non duplicated posts until another post is submitted.
this.af.database.object('users/' + x[0].uid + '/lastPostAt').set(timestamp)
.then(x => { this.dialogsService.showSuccessDialog('Post Submitted'); });
EDIT: Solved by splitting up logic.