FullStacking: Pagination + DataLoader
Tags: relay
Published: 2019-09-25 22:06:42

After some challenges, I've finally added pagination in the application.
That required me to add [DataLoader](https://github.com/graphql/dataloader), ModernQueryRenderer and [RefetchContainer](https://relay.dev/docs/en/refetch-container).

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

```react
// 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},
  },
);
```

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

```react
// 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;
}
```

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](https://github.com/entria/graphql-mongoose-loader) to abstract the interactions when loading data from MongoDB.

```javascript
// 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
    }
  };
};
// ...
```

```javascript
// 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,
  });
};
```



```javascript
// 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)},
    },
    // ...
    }
  })
});
```

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