DEV Community

Discussion on: Creating a React Custom Hook using TDD

Collapse
 
roblevintennis profile image
Rob Levin • Edited

I really liked the approach of this article using TDD and also enjoyed working with the react-hooks helpers. I referenced a bunch of various pagination tutorials and gists on the web and even old stuff I'd written a while back to find a solution I liked. I followed along mostly similar to what you have here but then completely tore down the entire design. I realized that, in my design, usePagination hook only needed to take care of generating the pagination links and nothing else. Totally SRP cohesive. Then, the React component would deal with pretty much only rendering the pagination controls. The consumer would, in fact, take care of the const [currentPage, setCurrentPage] state and simply listen for onPageChanged callback, update the current page, then have a listener in useEffect that would regenerate the paging links based off of the updated current page. There's a lot of code between the three, but I can show the tests for my usePagination as I think you'll find it interesting that it's basically a completely different approach!

import { renderHook, act } from '@testing-library/react-hooks';
import { usePagination } from './usePagination';

describe('generate paging', () => {
  it('should work for smaller totals', () => {
    const { result } = renderHook(() => usePagination({ offset: 2 }));
    act(() => {
      expect(result.current.generate(1, 4)).toStrictEqual([1, 2, 3, 4]);
      expect(result.current.generate(1, 5)).toStrictEqual([1, 2, 3, 4, 5]);
      expect(result.current.generate(1, 6)).toStrictEqual([1, 2, 3, '...', 6]);
      expect(result.current.generate(5, 6)).toStrictEqual([1, 2, 3, 4, 5, 6]);
      expect(result.current.generate(5, 7)).toStrictEqual([1, 2, 3, 4, 5, 6, 7]);
      expect(result.current.generate(6, 8)).toStrictEqual([1, '...', 4, 5, 6, 7, 8]);
    });
  });
  it('should generate pagination with offset of 2', () => {
    const { result } = renderHook(() => usePagination({ offset: 2 }));
    act(() => {
      // Edge case only 1 total pages
      expect(result.current.generate(10, 1)).toEqual([1]);
      expect(result.current.generate(1, 20)).toEqual([1, 2, 3, '...', 20]);
      expect(result.current.generate(2, 20)).toEqual([1, 2, 3, 4, '...', 20]);
      expect(result.current.generate(3, 20)).toEqual([1, 2, 3, 4, 5, '...', 20]);
      expect(result.current.generate(4, 20)).toEqual([1, 2, 3, 4, 5, 6, '...', 20]);
      expect(result.current.generate(5, 20)).toEqual([1, 2, 3, 4, 5, 6, 7, '...', 20]);
      expect(result.current.generate(6, 20)).toEqual([1, '...', 4, 5, 6, 7, 8, '...', 20]);
      expect(result.current.generate(7, 20)).toEqual([1, '...', 5, 6, 7, 8, 9, '...', 20]);
      expect(result.current.generate(8, 20)).toEqual([1, '...', 6, 7, 8, 9, 10, '...', 20]);
      expect(result.current.generate(9, 20)).toEqual([1, '...', 7, 8, 9, 10, 11, '...', 20]);
      expect(result.current.generate(10, 20)).toEqual([1, '...', 8, 9, 10, 11, 12, '...', 20]);
      expect(result.current.generate(11, 20)).toEqual([1, '...', 9, 10, 11, 12, 13, '...', 20]);
      expect(result.current.generate(12, 20)).toEqual([1, '...', 10, 11, 12, 13, 14, '...', 20]);
      expect(result.current.generate(13, 20)).toEqual([1, '...', 11, 12, 13, 14, 15, '...', 20]);
      expect(result.current.generate(14, 20)).toEqual([1, '...', 12, 13, 14, 15, 16, '...', 20]);
      expect(result.current.generate(15, 20)).toEqual([1, '...', 13, 14, 15, 16, 17, '...', 20]);
      expect(result.current.generate(16, 20)).toEqual([1, '...', 14, 15, 16, 17, 18, 19, 20]);
      expect(result.current.generate(17, 20)).toEqual([1, '...', 15, 16, 17, 18, 19, 20]);
      expect(result.current.generate(18, 20)).toEqual([1, '...', 16, 17, 18, 19, 20]);
      expect(result.current.generate(19, 20)).toEqual([1, '...', 17, 18, 19, 20]);
      expect(result.current.generate(20, 20)).toEqual([1, '...', 18, 19, 20]);
      // Test higher page and total
      expect(result.current.generate(999, 1200)).toEqual([
        1,
        '...',
        997,
        998,
        999,
        1000,
        1001,
        '...',
        1200,
      ]);
    });
  });
  it('should generate pagination with offset of 1', () => {
    const { result } = renderHook(() => usePagination({ offset: 1 }));
    act(() => {
      // Edge case only 1 total pages
      expect(result.current.generate(10, 1)).toEqual([1]);
      expect(result.current.generate(1, 20)).toEqual([1, 2, '...', 20]);
      expect(result.current.generate(2, 20)).toEqual([1, 2, 3, '...', 20]);
      expect(result.current.generate(3, 20)).toEqual([1, 2, 3, 4, '...', 20]);
      expect(result.current.generate(4, 20)).toEqual([1, '...', 3, 4, 5, '...', 20]);
      expect(result.current.generate(5, 20)).toEqual([1, '...', 4, 5, 6, '...', 20]);
      expect(result.current.generate(6, 20)).toEqual([1, '...', 5, 6, 7, '...', 20]);
      expect(result.current.generate(7, 20)).toEqual([1, '...', 6, 7, 8, '...', 20]);
      expect(result.current.generate(8, 20)).toEqual([1, '...', 7, 8, 9, '...', 20]);
      expect(result.current.generate(9, 20)).toEqual([1, '...', 8, 9, 10, '...', 20]);
      expect(result.current.generate(10, 20)).toEqual([1, '...', 9, 10, 11, '...', 20]);
      expect(result.current.generate(11, 20)).toEqual([1, '...', 10, 11, 12, '...', 20]);
      expect(result.current.generate(12, 20)).toEqual([1, '...', 11, 12, 13, '...', 20]);
      expect(result.current.generate(13, 20)).toEqual([1, '...', 12, 13, 14, '...', 20]);
      expect(result.current.generate(14, 20)).toEqual([1, '...', 13, 14, 15, '...', 20]);
      expect(result.current.generate(15, 20)).toEqual([1, '...', 14, 15, 16, '...', 20]);
      expect(result.current.generate(16, 20)).toEqual([1, '...', 15, 16, 17, '...', 20]);
      expect(result.current.generate(17, 20)).toEqual([1, '...', 16, 17, 18, '...', 20]);
      expect(result.current.generate(18, 20)).toEqual([1, '...', 17, 18, 19, 20]);
      expect(result.current.generate(19, 20)).toEqual([1, '...', 18, 19, 20]);
      expect(result.current.generate(20, 20)).toEqual([1, '...', 19, 20]);
      // Test higher page and total
      expect(result.current.generate(999, 1200)).toEqual([1, '...', 998, 999, 1000, '...', 1200]);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Here are the relevant scripts if you'd like to see the full approach: Pagination.tsx, usePagination.ts, Storybook consumer story.

A couple of things I'd note:

  • I don't recall ever seeing circular pagination (that's usually something I'd see like tabbing around a modal or tabs, but not pagination). Not sure if the UX is ideal.
  • Keyboard navigation is important. You should be able to tab through the paging controls. Perhaps even better would be to tab "into" controls, then use arrows (Zendesk Garden's pagination does this)
  • Probably the most challenging is large data sets and not having to loop huge data sets. The solution I went with uses currying approach which is completely independent of the size of the data set as it's only worried about the paging controls and where to place ellipses aka gap, and offsets.
Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Thanks for the kind words @roblevintennis :)
Surely there are better ways to implement a Pagination React hook than what I did, and yours does looks like a more robust one. My goal in the article was to focus on the TDD aspect of developing such custom component, so it was never meant to be a complete Pagination solution :)
Having said that, I am not sure how I feel about the Hook supplying the array of links... In my case the hook is much "dumber" in the sense that I leave the buffering (and in your case, the offset ) to be something that the consuming component should take care of.
As for the cyclic nature of the pagination, you're right, this is more of a carousel kinda feature, but heck, why not? ;)
In any case, thanks for sharing your approach!

Collapse
 
roblevintennis profile image
Rob Levin

Gotcha, yeah, there are probably many viable approached — I'd say again that your tutorial on setting up the TDD test bed really really helped and was quite valuable! Not sure if you've ever read Kent Beck's TDD by example? But I have had a long love/hate with unit tests but one case where I feel they shine is when you feel a bit uncertain as to how to go about something and, for me, implementing pagination is quite a challenge indeed!

Good stuff…the dev world is definitely better when we share these ideas! Thanks :)

Thread Thread
 
mbarzeev profile image
Matti Bar-Zeev

Cannot agree more :)