Skip to content

Conversation

@jackdclark
Copy link
Contributor

@jackdclark jackdclark commented Feb 21, 2019

Closes: #41, closes #42, and closes #29

Provides a fetchMore function that will send the same query with the provided options merged into the original options, meaning the consumer doesn't have to manually maintain the state. Removed this as the same can be achieved with refetch now that it accepts options.

useQuery accepts an updateData function as part of the options object that enables control of how the previous & new results are merged, this function can be overridden if necessary when calling refetch as well.

TODO

  • Agree on implementation
  • Test coverage
  • Update documentation

Example usage

// use options.updateResult to append the new page of posts to our current list of posts
const updateData = (prevData, prevData) => ({
  ...data,
  allPosts: [...prevData.allPosts, ...data.allPosts]
});

export default function PostList() {
  // set a default offset of 0 to load the first page
  const [skipCount, setSkipCount] = useState(0);

  const { loading, error, data } = useQuery(
    allPostsQuery,
    { variables: { skip: skipCount, first: 10 } },
    updateData
  );

  if (error) return <div>There was an error!</div>;
  if (loading && !data) return <div>Loading</div>;

  const { allPosts, _allPostsMeta } = data;
  const areMorePosts = allPosts.length < _allPostsMeta.count;

  return (
    <section>
      <ul>
        {allPosts.map(post => (
          <li key={post.id}>
            <a href={post.url}>{post.title}</a>
          </li>
        ))}
      </ul>
      {areMorePosts && (
        <button
          // set the offset to the current number of posts to fetch the next page
          onClick={() => setSkipCount(allPosts.length)}
        >
          Show more
        </button>
      )}
    </section>
  );
}

@jackdclark jackdclark mentioned this pull request Feb 21, 2019
Copy link
Contributor

@bmullan91 bmullan91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like the right approach 👍I've suggested we move the update fn into the options, since we're already passing + extending them via useQuery and useClientRequest.

Should the fn be called update which would match Apollo's name for the same function, or updateResult?

@jackdclark jackdclark marked this pull request as ready for review February 21, 2019 17:11
@jackdclark jackdclark requested a review from Joezo February 21, 2019 17:15
bmullan91
bmullan91 previously approved these changes Feb 21, 2019
Copy link
Contributor

@bmullan91 bmullan91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

src/useQuery.js Outdated
fetchMore: ({ variables, ...rest } = {}) =>
// merge variables so they don't all have to be passed back in,
// just the ones that are changing
queryReq({ ...rest, variables: { ...allOpts.variables, ...variables } })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍


expect(fetchData({ variables: { limit: 20 } })).rejects.toThrow(
'options.updateResult must be a function'
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 excellent tests

@jackdclark
Copy link
Contributor Author

@bmullan91 as discussed earlier I've removed fetchMore in favour of the declarative implementation. If a more imperative style is desired then the same can be achieved by using refetch and passing in new options.

I've also added a pagination guide to the README that includes distinct page & infinite loading examples.

If you could take another look when you get a chance please, hopefully we can get this in next week 😀

@jackdclark jackdclark requested a review from bmullan91 February 22, 2019 17:29
if (typeof revisedOpts.updateResult !== 'function') {
throw new Error('options.updateResult must be a function');
}
result.data = revisedOpts.updateResult(state.data, result.data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the name is updateResult, maybe we should pass the entire result? It might be useful for a developer to change / override the errors in some circumstances.

useClientRequest would then look something like:

let result = await client.request(revisedOperation, revisedOpts);

if (revisedOpts.updateResult) {
  if (typeof revisedOpts.updateResult !== 'function') {
    throw new Error('options.updateResult must be a function');
  }
  result = revisedOpts.updateResult(state, result)
}

The pagination example updateResult:

const updateResult = (prevResult, newResult) => ({
  ...newResult,
  data: {
    ...newResult.data,
    allPosts: [...prevResult.data.allPosts, ...result.data.allPosts]
  }
});

useQuery(MY_QUERY, {
  updateResult
})

It makes updating the data a bit more verbose, but the developer could write a wrapper if it was a really common usecase:

const dataUpdater = (fn) => (prevResult, newResult) => ({
  ...newResult,
  data: fn(prevResult.data, newResult.data)
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, passing the entire result back allows internal properties like loading & error to be changed, or new ones to be added which could be abused or cause tricky bugs. We're going to rename this to updateData as a better description of what it's doing & if we see a use case for changing / overriding errors in the future, we could always introduce an updateErrors option.

bmullan91
bmullan91 previously approved these changes Feb 25, 2019
Copy link
Contributor

@bmullan91 bmullan91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 - some minor comments just

count
}
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should simplify the example - this is could be:

ALL_POSTS_QUERY = `
  query($limit: Int, $skip: Int) {
    allPosts(limit: $limit, skip: $skip) {
      total,
      items {
        id
        title
      }
    }
  }
`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the separation between data & meta is good here? There are redundant parts to this example that could be removed & make it more readable though, how about:

export const ALL_POSTS_QUERY = `
  query allPosts($first: Int!, $skip: Int!) {
    allPosts(first: $first, skip: $skip) {
      id
      title
      url
    }
    _allPostsMeta {
      count
    }
  }
`;

refetch: (options = {}) =>
queryReq({
skipCache: true,
updateData: (_, data) => data,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: We should add a comment here saying why we're doing this

Copy link
Contributor Author

@jackdclark jackdclark Feb 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I've added an explanation & added a note to the README for refetch

Copy link
Contributor

@bmullan91 bmullan91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @jackdclark 🙌

@jackdclark jackdclark changed the title fetchMore util & result updater function Pagination Feb 25, 2019
@jackdclark jackdclark merged commit 4fc0441 into master Feb 25, 2019
@jackdclark jackdclark deleted the pagination-c branch February 25, 2019 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add pagination helper

2 participants