DEV Community

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

Posted on

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

Top comments (0)