How to implement pagination with GraphQL DataLoader? - javascript

I have the following GraphQL query:
{
events {
name
images {
uri
}
}
}
Currently, I am not able (don't know how) to handle pagination in that images field, because it is fetched using DataLoader.
The eventImagesLoader (DataLoader) implementation is the following:
import DataLoader from 'dataloader';
import { getRepository } from 'typeorm';
import { EventImage } from '../entities/event-image';
export const eventImagesLoader = () =>
new DataLoader<string, EventImage[]>(async (eventIds) => {
const images = await getRepository(EventImage)
.createQueryBuilder('image')
.where('image.eventId IN (:...ids)', { ids: eventIds })
.getMany();
const imagesMap = new Map<string, EventImage[]>();
images.forEach((image) =>
imagesMap.has(image.eventId)
? imagesMap.get(image.eventId)!.push(image)
: imagesMap.set(image.eventId, [image])
);
return eventIds.map((eventId) => imagesMap.get(eventId) || []);
});
The problem that I am facing is to handle the pagination in this scenario, because if you see in the above excerpt of code, I am querying to the database in this way:
const images = await getRepository(EventImage)
.createQueryBuilder('image')
.where('image.eventId IN (:...ids)', { ids: eventIds })
.getMany();
Which translates in a SQL like that:
SELECT * FROM `event_image` AS image WHERE image.id IN ("id:1", "id:2", "id:n");
And I use DataLoader in this case to batch all database queries into one (the above one).
So with that, how am I able to handle pagination?
I would like to do something like that:
{
events {
name
images(page: Int) {
uri
}
}
}

Related

How to obtain the `initialQueryRef` in Relay?

In the Relay docs, to fetch a query you have to first preload it using useQueryLoader, and then later pass the result of this into usePreloadedQuery. However, this first step, useQueryLoader takes two arguments: a query itself, which is easy to obtain, and preloadedQueryReference, which the docs do not explain how to obtain. All they say is " e.g. provided by router", but no router actually supports Relay with hooks so this is not helpful information.
As a simple example of this, I have a single component where I want to preload and then use a query:
import {
usePaginationFragment,
usePreloadedQuery,
useQueryLoader
} from "react-relay";
import graphql from 'babel-plugin-relay/macro';
const MY_QUERY = graphql` some stuff here `;
export default function SomeComponent(props) {
const initialRef = ???????;
const [
queryReference,
loadQuery,
disposeQuery,
] = useQueryLoader(AllHits, initialRef);
const qry = usePreloadedQuery(AllHits, queryReference);
}
However without the initialRef, I can't proceed any further. How do I obtain this?
It seems that the query ref is returned from useQueryLoader or loadQuery. The argument to useQueryLoader (initialRef in the original post) is optional, and might be derived from a previous call to loadQuery done by the router, but it in no way required. An example of using useQueryLoader which loads the query as soon as possible is this:
const some_query = graphql` some query here`;
function Parent(props){
// Note, didn't pass in the optional query ref here
const [
queryRef,
loadQuery,
disposeQuery
] = useQueryLoader(some_query);
// Load immediately
useEffect(() => {
loadQuery(
{count: 20},
{fetchPolicy: 'store-or-network'},
);
}, [loadQuery]);
if (!!queryRef) {
return <Child queryRef={queryRef}/>;
} else {
return "loading...";
}
}
export function Child(props) {
const {queryRef} = props;
const loaded = usePreloadedQuery(some_query, queryRef);
const { data } = useFragment(
graphql`some_fragment`,
loaded
);
return JSON.stringify(data);
}

How to store nuxtjs dynamically generated routes in vuex store

I'm trying to leverage nuxtjs SSG capabilities by creating a static web site where the pages content and navigation are fetched from an API.
I already found my way around on how to dynamically generate the routes by defining a module where I use the generate:before hook to fetch the pages content and routes. When creating the routes I store the page content as the route payload. The following code does just that and works as intended.
modules/dynamicRoutesGenerator.js
const generator = function () {
//Before hook to generate our custom routes
this.nuxt.hook('generate:before', async (generator, generatorOptions) => {
generator.generateRoutes(await generateDynamicRoutes())
})
}
let generateDynamicRoutes = async function() {
//...
return routes
}
export default generator
Now the problem I'm facing is that I have some navigation components that need the generated routes and I was thinking to store them into the vuex store.
I tried the generate:done hook but I don't know how to get the vuex store context from there. What I ended up using was the nuxtServerInit() action because as stated in the docs:
If nuxt generate is ran, nuxtServerInit will be executed for every dynamic route generated.
This is exactly what I need so I'm trying to use it with the following code:
store/index.js
export const actions = {
nuxtServerInit (context, nuxtContext) {
context.commit("dynamicRoutes/addRoute", nuxtContext)
}
}
store/dynamicRoutes.js
export const state = () => ({
navMenuNivel0: {}
})
export const mutations = {
addRoute (state, { ssrContext }) {
//Ignore static generated routes
if (!ssrContext.payload || !ssrContext.payload.entrada) return
//If we match this condition then it's a nivel0 route
if (!ssrContext.payload.navMenuNivel0) {
console.log(JSON.stringify(state.navMenuNivel0, null, 2));
//Store nivel0 route, we could use url only but only _id is guaranteed to be unique
state.navMenuNivel0[ssrContext.payload._id] = {
url: ssrContext.url,
entrada: ssrContext.payload.entrada,
navMenuNivel1: []
}
console.log(JSON.stringify(state.navMenuNivel0, null, 2));
//Nivel1 route
} else {
//...
}
}
}
export const getters = {
navMenuNivel0: state => state.navMenuNivel0
}
The action is indeed called and I get all the expected values, however it seems like that with each call of nuxtServerInit() the store state gets reset. I printed the values in the console (because I'm not sure even if it's possible to debug this) and this is what they look like:
{}
{
"5fc2f4f15a691a0fe8d6d7e5": {
"url": "/A",
"entrada": "A",
"navMenuNivel1": []
}
}
{}
{
"5fc2f5115a691a0fe8d6d7e6": {
"url": "/B",
"entrada": "B",
"navMenuNivel1": []
}
}
I have searched all that I could on this subject and altough I didn't find an example similar to mine, I put all the pieces I could together and this was what I came up with.
My idea was to make only one request to the API (during build time), store everything in vuex then use that data in the components and pages.
Either there is a way of doing it better or I don't fully grasp the nuxtServerInit() action. I'm stuck and don't know how to solve this problem and can't see another solution.
If you made it this far thanks for your time!
I came up a with solution but I don't find it very elegant.
The idea is to store the the API requests data in a static file. Then create a plugin to have a $staticAPI object that expose the API data and some functions.
I used the build:before hook because it runs before generate:before and builder:extendPlugins which means that by the time the route generation or plugin creation happen, we already have the API data stored.
dynamicRoutesGenerator.js
const generator = function () {
//Add hook before build to create our static API files
this.nuxt.hook('build:before', async (plugins) => {
//Fetch the routes and pages from API
let navMenuRoutes = await APIService.fetchQuery(QueryService.navMenuRoutesQuery())
let pages = await APIService.fetchQuery(QueryService.paginasQuery())
//Cache the queries results into staticAPI file
APIService.saveStaticAPIData("navMenuRoutes", navMenuRoutes)
APIService.saveStaticAPIData("pages", pages)
})
//Before hook to generate our custom routes
this.nuxt.hook('generate:before', async (generator, generatorOptions) => {
console.log('generate:before')
generator.generateRoutes(await generateDynamicRoutes())
})
}
//Here I can't find a way to access via $staticAPI
let generateDynamicRoutes = async function() {
let navMenuRoutes = APIService.getStaticAPIData("navMenuRoutes")
//...
}
The plugin staticAPI.js:
import APIService from '../services/APIService'
let fetchPage = function(fetchUrl) {
return this.pages.find(p => { return p.url === fetchUrl})
}
export default async (context, inject) => {
//Get routes and files from the files
let navMenuRoutes = APIService.getStaticAPIData("navMenuRoutes")
let pages = APIService.getStaticAPIData("pages")
//Put the objects and functions in the $staticAPI property
inject ('staticAPI', { navMenuRoutes, pages, fetchPage })
}
The APIService helper to save/load data to the file:
//...
let fs = require('fs');
let saveStaticAPIData = function (fileName = 'test', fileContent = '{}') {
fs.writeFileSync("./static-api-data/" + fileName + ".json", JSON.stringify(fileContent, null, 2));
}
let getStaticAPIData = function (fileName = '{}') {
let staticData = {};
try {
staticData = require("../static-api-data/" + fileName + ".json");
} catch (ex) {}
return staticData;
}
module.exports = { fetchQuery, apiUrl, saveStaticAPIData, getStaticAPIData }
nuxt.config.js
build: {
//Enable 'fs' module
extend (config, { isDev, isClient }) {
config.node = { fs: 'empty' }
}
},
plugins: [
{ src: '~/plugins/staticAPI.js', mode: 'server' }
],
buildModules: [
'#nuxtjs/style-resources',
'#/modules/staticAPIGenerator',
'#/modules/dynamicRoutesGenerator'
]

Fetching data from array and dynamic route display in nextjs

I am learning Nextjs framework and I am having difficulty with dynamic route and how to fetch the data from an array. I followed the basics of Nextjs but this got me stuck.
What I want?
Get the information of this array which is stored as a items.tsx file:
export const projects = [
{
id: '2',
title: 'Italian recipes book',
description: `This a web design and soon-to-be a fully functional and responsive React application`
},
{
id: '1',
title: 'Chat App website',
description: `This is a homepage design and build for a concept project – a chat application. I designed the page
first then built a responsive web page using Webflow.`
},
]
As per nextjs website I was able to fetch the id and created a function to get the project data like so
import { projects } from "../models/projects";
export function getAllItemsIds() {
return projects.map(project => {
return {
params: {
id: project.id,
}
}
})
}
export function getProjectData(id) {
return {
id,
}
}
I believe the function getProjectData(id) is missing the rest of the data and I'm having problems on how to fetch it. I want to show the info of the array in my [id].tsx file according to the parameter (in this case, to the ID 1 or 2).
This is my [id].tsx file:
export async function getStaticPaths() {
const paths = getAllItemsIds()
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const projectData = getProjectData(params.id)
return {
props: {
projectData
}
}
}
const Project = ({ projectData }) => {
return (
<>
<div>
{projectData.id}
{projectData.title}
{projectData.description}
</div>
</>
)
}
export default Project;
As of now I am only able to display my projectData.id and I got a bit confused with all the getStaticPaths() and getStaticProps() thing. I'd really appreciate the help.
I don't know if that's the right way to do it, but I managed to do what I wanted like so:
added the other parameters and mapped the array inside the getProjectData
export function getProjectData(id, title, description ) {
projects.map(data => {
if (data.id === id) {
title = data.title;
description = data.description;
}
});
return {
id,
title,
description
}
}
and in the [id].tsx I updates the getStaticProps like so
export async function getStaticProps({ params, title, description }) {
const projectData = getProjectData(params.id, title, description)
return {
props: {
projectData
}
}
}
It did fetch the data according to the id parameter!

Having Trouble Loading Earlier Messages in React-native GiftedChat Chat App with Firebase

I have been working on a chat app using Gifted-Chat and a Firebase RealTime database (and running it with Expo). At this point, the basic messaging works, but I am trying to enable to app to load earlier messages when the user scrolls up and hits the button that appears (I am aware of the GiftedChat prop for this). Unfortunately, I have been having trouble doing this and am a bit stumped.
There are two separate problems I have been running up against that I am aware of.
Clicking the loadEarlier button gives me an undefined is not a function (near '...this.setState...' runtime error (clearly, something is wrong with the skeleton function I put there).
The bigger issues is that I am still not clear on how to download the n number of messages before the oldest messages currently loaded. I have looked at the GiftedChat example and this post for help, but must confess that I am still lost (the best I can figure is that I need to sort the messages, possibly by timestamp, somehow get the right range, then parse them and prepend them to the messages array in state, but I cannot figure out how to do this, especially the later parts).
The relevant parts of the code for my chat screen are below, as is a screenshot of the structure of my firebase database. I would appreciate any help regarding both of these issues.
// Your run of the mill React-Native imports.
import React, { Component } from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import * as firebase from 'firebase';
// Our custom components.
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { BotButton } from '../components/BotButton';
// Array of potential bot responses. Might be a fancy schmancy Markov
// chain like thing in the future.
import {botResponses} from '../Constants.js';
// Gifted-chat import. The library takes care of fun stuff like
// rendering message bubbles and having a message composer.
import { GiftedChat } from 'react-native-gifted-chat';
// To keep keyboard from covering up text input.
import { KeyboardAvoidingView } from 'react-native';
// Because keyboard avoiding behavior is platform specific.
import {Platform} from 'react-native';
console.disableYellowBox = true;
class Chat extends Component {
state = {
messages: [],
isLoadingEarlier: false,
};
// Reference to where in Firebase DB messages will be stored.
get ref() {
return firebase.database().ref('messages');
}
onLoadEarlier() {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
});
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
}
// Get last 20 messages, any incoming messages, and send them to parse.
on = callback =>
this.ref
.limitToLast(20)
.on('child_added', snapshot => callback(this.parse(snapshot)));
parse = snapshot => {
// Return whatever is associated with snapshot.
const { timestamp: numberStamp, text, user } = snapshot.val();
const { key: _id } = snapshot;
// Convert timestamp to JS date object.
const timestamp = new Date(numberStamp);
// Create object for Gifted Chat. id is unique.
const message = {
_id,
timestamp,
text,
user,
};
return message;
};
// To unsubscribe from database
off() {
this.ref.off();
}
// Helper function to get user UID.
get uid() {
return (firebase.auth().currentUser || {}).uid;
}
// Get timestamp for saving messages.
get timestamp() {
return firebase.database.ServerValue.TIMESTAMP;
}
// Helper function that takes array of messages and prepares all of
// them to be sent.
send = messages => {
for (let i = 0; i < messages.length; i++) {
const { text, user } = messages[i];
const message = {
text,
user,
timestamp: this.timestamp,
};
this.append(message);
}
};
// Save message objects. Actually sends them to server.
append = message => this.ref.push(message);
// When we open the chat, start looking for messages.
componentDidMount() {
this.on(message =>
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, message),
}))
);
}
get user() {
// Return name and UID for GiftedChat to parse
return {
name: this.props.navigation.state.params.name,
_id: this.uid,
};
}
// Unsubscribe when we close the chat screen.
componentWillUnmount() {
this.off();
}
render() {
return (
<View>
<GiftedChat
loadEarlier={true}
onLoadEarlier={this.onLoadEarlier}
isLoadingEarlier={this.state.isLoadingEarlier}
messages={this.state.messages}
onSend={this.send}
user={this.user}
/>
</View>
);
}
}
export default Chat;
For your first issue, you should declare your onLoadEarlier with => function so as to get the current instance this i.e. your code should look like below:
onLoadEarlier = () => {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
}, () => {
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
});
}
Also, setState is asynchronous in nature, so you should rather depend on the second parameter of the setState i.e. the callback to ensure that the next lines of code execute synchronously.
Lastly, if you are using class syntax then you should declare the state in constructor like below:
class Chat extends Component {
constructor (props) {
super (props);
state = {
messages: [],
isLoadingEarlier: false,
};
}
......
onLoadEarlier = () => {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
}, () => {
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
});
}
...
}
For loading the last messages from firebase , I recommend using limitToLast function on your reference. You should afterwards order the results by date before calling append in gifted chat.
For the second question, it should be the same with this question How Firebase on and once differ?
You can using filter feature in Firebase for example using createdAt field to compare with last loaded message to load more.

`updater` not working correctly when using subscriptions in Relay Modern

I'm using Relay Modern in my app and have successfully integrated subscriptions using the requestSubscription function. All works well and the updater function that I use to update the cache gets called correctly with the proper subscription payload.
The subscription is used to add a new item to a list of Link elements. Here's what the subscription looks like:
NewLinkSubscription.js
const newLinkSubscription = graphql`
subscription NewLinkSubscription {
Link {
mutation
node {
id
description
url
createdAt
postedBy {
id
name
}
}
}
}
`
export default (updater, onError) => {
const subscriptionConfig = {
subscription: newLinkSubscription,
variables: {},
updater,
onError
}
requestSubscription(
environment,
subscriptionConfig
)
}
I then have a LinkList component that's rendering all the Link elements. In componentDidMount of that component, I initiate the NewLinkSubscription:
LinkList.js
class LinkList extends Component {
componentDidMount() {
NewLinkSubscription(
proxyStore => {
const createLinkField = proxyStore.getRootField('Link')
const newLink = createLinkField.getLinkedRecord('node')
const viewerProxy = proxyStore.get(this.props.viewer.id)
const connection = ConnectionHandler.getConnection(viewerProxy, 'LinkList_allLinks')
if (connection) {
ConnectionHandler.insertEdgeAfter(connection, newLink)
}
},
error => console.log(`An error occured:`, error),
)
}
render() {
console.log(`LinkList - render `, this.props.viewer.allLinks.edges)
return (
<div>
{this.props.viewer.allLinks.edges.map(({node}) =>
{
console.log(`render node: `, node)
return <Link key={node.id} link={node} viewer={this.props.viewer} />
}
)}
</div>
)
}
}
export default createFragmentContainer(LinkList, graphql`
fragment LinkList_viewer on Viewer {
id
...Link_viewer
allLinks(last: 100, orderBy: createdAt_DESC) #connection(key: "LinkList_allLinks", filters: []) {
edges {
node {
...Link_link
}
}
}
}
`)
Now, here's what's happening when a subscription with a new Link comes in. The updater is called correctly and it seems like the new node gets inserted into the connection correctly (at least ConnectionHandler.insertEdgeAfter(connection, newLink) is called).
This triggers a rerender of the component. When I debug render to inspect the data (props.viewer.allLinks.edges), I can see that a new node indeed was added to the connection - so the list actually does contain one more item now! However, the problem is that this new node is actually undefined which causes the app to crash!
Does anyone spot what I'm missing here?
I was able to make it work, this is how I implemented the updater now:
NewLinkSubscription(
proxyStore => {
const linkField = proxyStore.getRootField('Link')
const newLink = linkField.getLinkedRecord('node')
const viewerProxy = proxyStore.get(this.props.viewer.id)
const connection = ConnectionHandler.getConnection(viewerProxy, 'LinkList_allLinks', {
last: 100,
orderBy: 'createdAt_DESC'
})
if (connection) {
const edge = ConnectionHandler.createEdge(proxyStore, connection, newLink, 'allLinks')
ConnectionHandler.insertEdgeBefore(connection, edge)
}
},
error => console.log(`An error occurred:`, error),
)

Categories

Resources