DEV Community

Cover image for FullStacking: Pagination + DataLoader
Mark Kop
Mark Kop

Posted on

4 1

FullStacking: Pagination + DataLoader

After some challenges, I've finally added pagination in the application.
That required me to add DataLoader, ModernQueryRenderer and RefetchContainer.

I still can't explain in details everything I've done, but I'll share the files I've changed.

// EventList.js
import {
  createRefetchContainer, graphql,
} from 'react-relay';
// ...
<View style={styles.container}>
        <FlatList
          data={events.edges}
          renderItem={renderItem}
          keyExtractor={item => item.node.id}
          onEndReached={onEndReached}
          onRefresh={onRefresh}
          refreshing={isFetchingTop}
          ItemSeparatorComponent={() => <View style={styles.separator} />}
          ListFooterComponent={null}
        />
</View>
// ...
const EventListPaginationContainer = createRefetchContainer(
  EventList,
  {
    query: graphql`
      fragment EventList_query on Query  
      @argumentDefinitions(
        count: {type: "Int", defaultValue: 10}
        cursor: {type: "String"}
      ) {
        events(first: $count, after: $cursor)
          @connection(key: "EventList_events") {
          pageInfo {
            hasNextPage
            endCursor
          }
          edges {
            node {
              id
              title
              date
              description
              author
            }
          }
        }
      }
    `,
  },
  graphql`
      query EventListPaginationQuery($count: Int!, $cursor: String) {
        ...EventList_query @arguments(count: $count, cursor: $cursor)
      }
    `,
);
// ...
export default createQueryRendererModern(
  EventListPaginationContainer,
  EventList,
  {
    query: graphql`
      query EventListQuery($count: Int!, $cursor: String) {
        ...EventList_query
      }
    `,
    variables: {cursor: null, count: 5},
  },
);
Enter fullscreen mode Exit fullscreen mode

We've updated the EventList to a display a FlatList component and to use a createQueryRendererModern and a createRefetchContainer.

// createQueryRendererModern.js
import * as React from 'react';
import {Text} from 'react-native';
import {QueryRenderer} from 'react-relay';

import Environment from './Environment';

export default function createQueryRenderer(FragmentComponent, Component, config) {
  const {query, queriesParams} = config;

  class QueryRendererWrapper extends React.Component {
    render() {
      const variables = queriesParams
        ? queriesParams(this.props)
        : config.variables;

      return (
        <QueryRenderer
          environment={Environment}
          query={query}
          variables={variables}
          render={({error, props}) => {
            if (error) {
              return <Text>{error.toString()}</Text>;
            }

            if (props) {
              return <FragmentComponent {...this.props} query={props} />;
            }

            return <Text>loading</Text>;
          }}
        />
      );
    }
  }

  return QueryRendererWrapper;
}
Enter fullscreen mode Exit fullscreen mode

In the server's side we've need to add dataloaders in the app's context and use them to load events from Mongoose. We're also using graphql-mongoose-loader to abstract the interactions when loading data from MongoDB.

// server/app.js
// ...
const graphqlSettingsPerReq = async req => {
  const { currentUser } = await getUser(req.header.authorization);

  const dataloaders = Object.keys(loaders).reduce(
    (acc, loaderKey) => ({
      ...acc,
      [loaderKey]: loaders[loaderKey].getLoader(),
    }),
    {},
  );

  return {
    schema,
    context: {
      currentUser,
      req,
      dataloaders
    }
  };
};
// ...
Enter fullscreen mode Exit fullscreen mode
// EventLoader.js
import DataLoader from 'dataloader';
import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader';
import { ConnectionArguments } from 'graphql-relay';

import EventModel, { IEvent } from './EventModel';


export default class Event {

  constructor(data) {
    this.id = data.id ||  data._id;
    this._id = data._id;
    this.title = data.title;
    this.description = data.description;
    this.author = data.author;
  }
}

export const getLoader = () => new DataLoader(ids => mongooseLoader(EventModel, ids));

const viewerCanSee = () => true;

export const load = async (context, id) => {
  if (!id) {
    return null;
  }

  let data;
  try {
    data = await context.dataloaders.EventLoader.load(id);
  } catch (err) {
      console.log(err)
    return null;
  }

  return viewerCanSee() ? new Event(data, context) : null;
};

export const clearCache = ({ dataloaders }, id) => dataloaders.EventLoader.clear(id.toString());
export const primeCache = ({ dataloaders }, id, data) => dataloaders.EventLoader.prime(id.toString(), data);
export const clearAndPrimeCache = (context, id, data) => clearCache(context, id) && primeCache(context, id, data);


export const loadEvents = async (context, args) => {
    const where = args.search ? { title: { $regex: new RegExp(`^${args.search}`, 'ig') } } : {};
    const event = EventModel.find(where).sort({ createdAt: -1 });
  return connectionFromMongoCursor({
    cursor: event,
    context,
    args,
    loader: load,
  });
};
Enter fullscreen mode Exit fullscreen mode
// QueryType.js
export default new GraphQLObjectType({
  name: "Query",
  description: "The root of all... queries",
  fields: () => ({
    node: nodeField,
    // ...
    },
    events: {
      type: EventConnection.connectionType,
      args: {
        ...connectionArgs,
        search: {
          type: GraphQLString
        }
      },
      resolve: (obj, args, context) => {
        return EventLoader.loadEvents(context, args)},
    },
    // ...
    }
  })
});
Enter fullscreen mode Exit fullscreen mode

This should be enough to enable pagination.
Remember to run yarn update-schema and yarn relay to update the schema and generated files respectively

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️