2024-07-16

Mastering React Query and React Table: A Comprehensive Guide with Examples

Table of Contents:

  1. Introduction
  2. React Query
    2.1 Core Concepts
    2.2 Queries
    2.3 Mutations
    2.4 Query Invalidation
    2.5 Prefetching
    2.6 Pagination
    2.7 Infinite Queries
    2.8 Optimistic Updates
    2.9 Devtools
  3. React Table
    3.1 Core Concepts
    3.2 Basic Table
    3.3 Sorting
    3.4 Filtering
    3.5 Pagination
    3.6 Row Selection
    3.7 Column Ordering
    3.8 Grouping
    3.9 Expanding
  4. Conclusion
  5. Introduction

React Query and React Table are powerful libraries that simplify data fetching, state management, and table rendering in React applications. This guide will walk you through the main features of both libraries with practical examples.

  1. React Query

React Query is a data-fetching and state management library for React applications. It provides hooks for fetching, caching, and updating data in your React applications.

2.1 Core Concepts

The main hooks in React Query are:

  • useQuery: For fetching data
  • useMutation: For modifying data
  • useQueryClient: For accessing the QueryClient instance

2.2 Queries

Queries are used to fetch data from an API. Here's a basic example:

import { useQuery } from 'react-query';

function Users() {
  const { isLoading, error, data } = useQuery('users', fetchUsers);

  if (isLoading) return 'Loading...';
  if (error) return 'An error has occurred: ' + error.message;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Fetch function
const fetchUsers = async () => {
  const res = await fetch('https://api.example.com/users');
  return res.json();
};

2.3 Mutations

Mutations are used to create/update/delete data. Here's an example:

import { useMutation, useQueryClient } from 'react-query';

function AddUser() {
  const queryClient = useQueryClient();
  const mutation = useMutation(newUser => {
    return fetch('https://api.example.com/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
    });
  }, {
    onSuccess: () => {
      queryClient.invalidateQueries('users');
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ name: e.target.name.value });
    }}>
      <input type="text" name="name" />
      <button type="submit">Add User</button>
    </form>
  );
}

2.4 Query Invalidation

Query invalidation is used to mark queries as stale and refetch them. Here's how to invalidate queries:

const queryClient = useQueryClient();

// Invalidate every query in the cache
queryClient.invalidateQueries();
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries('todos');

2.5 Prefetching

Prefetching can be used to fetch data before it's needed:

const queryClient = useQueryClient();

queryClient.prefetchQuery('users', fetchUsers);

2.6 Pagination

React Query makes pagination straightforward:

function Users() {
  const [page, setPage] = useState(1);
  const { isLoading, error, data } = useQuery(['users', page], () => fetchUsers(page), {
    keepPreviousData: true,
  });

  // ... render logic
}

2.7 Infinite Queries

For infinite scrolling, use the useInfiniteQuery hook:

function InfiniteUsers() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery('infiniteUsers', fetchUsers, {
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  });

  // ... render logic
}

2.8 Optimistic Updates

Optimistic updates can improve perceived performance:

const queryClient = useQueryClient();

const mutation = useMutation(updateUser, {
  onMutate: async newUser => {
    await queryClient.cancelQueries('user');
    const previousUser = queryClient.getQueryData('user');
    queryClient.setQueryData('user', newUser);
    return { previousUser };
  },
  onError: (err, newUser, context) => {
    queryClient.setQueryData('user', context.previousUser);
  },
  onSettled: () => {
    queryClient.invalidateQueries('user');
  },
});

2.9 Devtools

React Query comes with built-in devtools:

import { ReactQueryDevtools } from 'react-query/devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your app */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

  1. React Table

React Table is a lightweight, extensible datagrids for React.

3.1 Core Concepts

The main hooks in React Table are:

  • useTable: The core hook
  • useSortBy: For sorting
  • useFilters: For filtering
  • usePagination: For pagination
  • useRowSelect: For row selection
  • useColumnOrder: For column ordering
  • useGroupBy: For grouping
  • useExpanded: For expanding rows

3.2 Basic Table

Here's a basic example of a table:

import { useTable } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
  } = useTable({ columns, data });

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps()}>{column.render('Header')}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map(row => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => (
                <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
              ))}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

3.3 Sorting

To add sorting functionality:

import { useTable, useSortBy } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
  } = useTable({ columns, data }, useSortBy);

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                {column.render('Header')}
                <span>
                  {column.isSorted
                    ? column.isSortedDesc
                      ? ' 🔽'
                      : ' 🔼'
                    : ''}
                </span>
              </th>
            ))}
          </tr>
        ))}
      </thead>
      {/* ... tbody */}
    </table>
  );
}

3.4 Filtering

To add filtering:

import { useTable, useFilters } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    state,
    setFilter,
  } = useTable({ columns, data }, useFilters);

  return (
    <>
      <input
        value={state.filters[0]?.value || ''}
        onChange={e => setFilter('columnName', e.target.value)}
        placeholder={'Search...'}
      />
      <table {...getTableProps()}>
        {/* ... table content */}
      </table>
    </>
  );
}

3.5 Pagination

To add pagination:

import { useTable, usePagination } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    page,
    nextPage,
    previousPage,
    canNextPage,
    canPreviousPage,
    pageOptions,
    state: { pageIndex, pageSize },
    prepareRow,
  } = useTable({ columns, data }, usePagination);

  return (
    <>
      <table {...getTableProps()}>
        {/* ... table content using 'page' instead of 'rows' */}
      </table>
      <div>
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          Previous
        </button>
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          Next
        </button>
        <div>
          Page {pageIndex + 1} of {pageOptions.length}
        </div>
      </div>
    </>
  );
}

3.6 Row Selection

To add row selection:

import { useTable, useRowSelect } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    selectedFlatRows,
  } = useTable({ columns, data }, useRowSelect, hooks => {
    hooks.visibleColumns.push(columns => [
      {
        id: 'selection',
        Header: ({ getToggleAllRowsSelectedProps }) => (
          <input type="checkbox" {...getToggleAllRowsSelectedProps()} />
        ),
        Cell: ({ row }) => (
          <input type="checkbox" {...row.getToggleRowSelectedProps()} />
        ),
      },
      ...columns,
    ]);
  });

  return (
    <>
      <table {...getTableProps()}>
        {/* ... table content */}
      </table>
      <pre>
        <code>
          {JSON.stringify(
            {
              selectedRows: selectedFlatRows.map(row => row.original),
            },
            null,
            2
          )}
        </code>
      </pre>
    </>
  );
}

3.7 Column Ordering

To allow column reordering:

import { useTable, useColumnOrder } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    setColumnOrder,
  } = useTable({ columns, data }, useColumnOrder);

  const changeOrder = () => {
    setColumnOrder(['id', 'name', 'age']); // example order
  };

  return (
    <>
      <button onClick={changeOrder}>Change Column Order</button>
      <table {...getTableProps()}>
        {/* ... table content */}
      </table>
    </>
  );
}

3.8 Grouping

To add grouping functionality:

import { useTable, useGroupBy, useExpanded } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
  } = useTable({ columns, data }, useGroupBy, useExpanded);

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps()}>
                {column.canGroupBy ? (
                  <span {...column.getGroupByToggleProps()}>
                    {column.isGrouped ? '🛑 ' : '👊 '}
                  </span>
                ) : null}
                {column.render('Header')}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map(row => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => (
                <td {...cell.getCellProps()}>
                  {cell.isGrouped ? (
                    <>
                      <span {...row.getToggleRowExpandedProps()}>
                        {row.isExpanded ? '👇' : '👉'}
                      </span>{' '}
                      {cell.render('Cell')} ({row.subRows.length})
                    </>
                  ) : cell.isAggregated ? (
                    cell.render('Aggregated')
                  ) : cell.isPlaceholder ? null : (
                    cell.render('Cell')
                  )}
                </td>
              ))}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

3.9 Expanding

To add row expansion:

import { useTable, useExpanded } from 'react-table';

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    state: { expanded },
  } = useTable({ columns, data }, useExpanded);

  return (
    <table {...getTableProps()}>
      {/* ... thead */}
      <tbody {...getTableBodyProps()}>
        {rows.map(row => {
          prepareRow(row);
          return (
            <>
              <tr {...row.getRowProps()}>
                {row.cells.map(cell => (
                  <td {...cell.getCellProps()}>
                    {cell.render('Cell')}
                    {cell.column.id === 'expander' ? (
                      <span {...row.getToggleRowExpandedProps()}>
                        {row.isExpanded ? '👇' : '👉'}
                      </span>
                    ) : null}
                  </td>
                ))}
              </tr>
              {row.isExpanded && (
                <tr>
                  <td colSpan={columns.length}>
                    {/* Additional content for expanded row */}
                  </td>
                </tr>
              )}
            </>
          );
        })}
      </tbody>
    </table>
  );
}

  1. Conclusion

React Query and React Table are powerful libraries that can significantly simplify data management and table rendering in React applications. React Query provides an intuitive way to handle server state, while React Table offers a flexible and feature-rich solution for building complex tables.

By leveraging these libraries, you can create more efficient, maintainable, and user-friendly React applications. Remember to consult the official documentation for the most up-to-date information and advanced usage scenarios.