Chapter 21

Tables, grids and search

A table people can search looks like a lot of work. It is a short list of decisions. Pick the pattern, start from a skeleton, and then see how interactive tables and on-demand search tables work inside.

Pega 24 – 26

Decide

Which pattern do I need?

Answer five questions. You get a pattern, the few settings to fill in first, what to add later only if you need it, and what Pega has to provide.

Pattern pickerTry it: answer the five questions
When should rows appear?
How many rows can there be?
Where should filtering happen?
Must you record what people were shown?
What can a row do?

Use

Interactive data table

A table that loads at once and filters as you type

The rows load as soon as the grid appears, with no Search step. Filters narrow what is already loaded, so typing never calls the data page.

Fill these settings first

  • dataPageName
  • columnsJson (plain names)
  • filtersJson (plain names)
  • rowsPerPage
  • clientSideFiltering = on (the default)

Add later, only if you need it

  • useOpenCase + openCaseColumnKeys (a column that opens a case)
  • badgeColumnKey + successBadgeValues / infoBadgeValues / errorBadgeValues (coloured status)
  • aggregationDataPage + aggregationKey (count tiles above the filters)
  • autoRefreshSeconds, showRefreshButton, recordsDisplayMode (infinite scroll)

Pega needsA data page that returns the rows (a List Data Page) · Rows that carry the case handle (and class) so only real cases become links

The easy wayStart with only the data page name and a plain list of columns. Add tiles, badges, counters or export later, one setting at a time. Each is optional and the grid stays plain without them.

Start

A starter skeleton.

Choose how rows arrive, how they are filtered and paged, and what a click does. The skeleton changes with each choice, along with what Pega needs and the traps to watch.

Starter generatorTry it: choose how rows arrive, are filtered and paged
Where do rows come from?
Filtering
Paging
A click on a row

Pega needs

  • A data page that returns the rows
  • Rows that carry the case handle and class
MyTable/index.tsx (illustrative skeleton)
import { useEffect, useMemo, useState } from 'react'; // Settings the author fills in. Everything has a default, and columns is just a plain list of names.type Props = {  getPConnect: () => any;  dataPageName: string;  columns?: string;        // 'CaseID, Name, Status'  rowsPerPage?: number;}; export default function MyTable(props: Props) {  const { getPConnect, columns = '', dataPageName, rowsPerPage = 10 } = props;  const pConn = getPConnect();  const cols = columns.split(",").map((c) => c.trim()).filter(Boolean);   const [rows, setRows] = useState<any[]>([]);  const [error, setError] = useState<string | null>(null);  const [query, setQuery] = useState('');  const [page, setPage] = useState(1);   // 1. Ask for rows. Never fetch yourself: PCore adds sign-in, context and caching.  useEffect(() => {    async function load() {      try {        const res = await (window as any).PCore?.getDataApiUtils?.()?.getData(          dataPageName, {  }, pConn.getContextName());        // 2. Accept every answer shape, or a data page that did answer looks empty        setRows(res?.data?.data ?? res?.data ?? (Array.isArray(res) ? res : []));        setError(null);      } catch {        setError('The data could not be loaded.');   // short, calm, no raw error text      }    }    load();  }, [dataPageName]);   // 3. Filter the rows already loaded (a text box matches part of any shown column)  const filtered = useMemo(() => {    const needle = query.trim().toLowerCase();    return needle ? rows.filter((r) => cols.some((c) => String(r[c] ?? "").toLowerCase().includes(needle))) : rows;  }, [rows, query, columns]);   // 4. Cut into pages, in the browser  const shown = filtered.slice((page - 1) * rowsPerPage, page * rowsPerPage);   // 5. What a click on a row does  const open = (row: any) => {    // Only a real case can be opened; a data-view row has a key but no case to open    if (row.pzInsKey && row.pxObjClass) pConn.getActionsApi().openWorkByHandle(row.pzInsKey, row.pxObjClass);  };   if (error) return <p role="alert">{error}</p>;   return (    <div>      <input aria-label="Search" value={query} onChange={(e) => { setQuery(e.target.value); setPage(1); }} />      <table>        <thead><tr>{cols.map((c) => <th key={c}>{c}</th>)}</tr></thead>        <tbody>          {shown.map((row, i) => (            <tr key={row.pzInsKey ?? i} onClick={() => open(row)}>              {cols.map((c) => <td key={c}>{String(row[c] ?? "")}</td>)}            </tr>          ))}        </tbody>      </table>      <button disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</button>      <button disabled={page >= Math.ceil(filtered.length / rowsPerPage)} onClick={() => setPage(page + 1)}>Next</button>    </div>  );}
  • Browser-side filtering and paging suit up to a few hundred rows. Beyond that, use the server.
  • This is the shape, not the finished thing: add the busy state, the error boundary and the guarded PCore access from the playbook.

Keep it simple

Ten rules, and adding a setting.

The same habits show up in every component that stayed easy to change. Then a worked example of adding one setting safely.

Simple rulesTry it: read the rules, then step through the recipe

Ten rules that keep it simple

  1. 1Plain names first, JSON for exceptionsA column or filter is just its property name. The heading, the input type and the label come from the data page. Write JSON only when you must override something.
  2. 2Show only what the author choseColumns come only from the Columns setting. Nothing else from the data page leaks onto the screen.
  3. 3Fetch, normalise, renderGet the rows, map each into your own shape, then map those into the screen. The screen never depends on the class structure.
  4. 4One setting, one hookA column setting lives in the columns hook, a filter setting in the filters hook. Changing one thing then touches one file.
  5. 5Pure functions for the logicSorting, paging, parsing and formatting are plain functions: same input, same output, no Pega. They are the easiest to test and to trust.
  6. 6Accept every answer shapeRows can arrive as { data: { data } }, { data } or a bare array. Read all three.
  7. 7Let one part fail without the restA dropdown whose data page fails stays empty and says so. The other filters and the search carry on.
  8. 8Do not guessNever reformat a value that only looks like a date, and never pick a parameter name when several fit. Ask for JSON instead.
  9. 9Calm messages, safe logsShow a short sentence with a code. Log the safe cause. Never show or log the raw error.
  10. 10Try it with a fake Pega firstStorybook with a mocked PCore lets you build and test the whole thing before touching a server. Then test once on the real one.

Adding a setting, step by step

Example: let the author hide the result count above a table.

  1. Add the setting: a name, a label, help text, a format (BOOLEAN here) and a default. Add the default to defaultConfig too.

Three places always describe the same setting and must agree: config.json (what the author sees), types.ts (what the developer reads) and index.tsx (what actually runs).

Under the hood

How interactive tables and search tables really work.

What a finished interactive table or search table does in order, what it protects against, and the workarounds that exist because something once broke. Pick a component on the left.

Tables and search, insideTry it: pick a component

Server-side table

Page, sort and filter on the server so big lists stay fast.

What happens, in order

  1. 1Turn every filled filter into a condition (T1, T2…), joined with AND, the way Pega’s own tables send a search.
  2. 2Send page number, page size and the conditions in one data page request.
  3. 3Move the page number at once, keep the old rows with a progress indicator, and roll back if the fetch fails.
  4. 4Optionally write one column of the visible rows to a property so a paired component can follow.

What it guards against

  • The server does the filtering, so paging and filtering stay consistent with each other.
  • Text filters match part of the value ignoring capitals; every other type matches exactly.
  • Only a case is a link. A data view row has a key and a class too, but Pega cannot open it as a case, so its cell stays plain text instead of a link that goes nowhere.

Workarounds, and why they exist

  • Finding a case’s class takes a fallback chain: the class inside the handle, then the row’s own object class, then the data page’s metadata.
  • A column that already holds the whole handle (it contains a space) is used as it is; otherwise the handle is built from class and id. The link opens in the same tab with openWorkByHandle.
  • Headings are sort controls drawn like Pega’s own list headings: an arrow on hover, kept on the sorted column and turned over for descending.

Your turn

Questions and comments

Stuck on something, or have a thought? Write it here. To talk about one part, use the Comment button under that section's heading.

3–20 letters, digits, _ or -. Kept in this browser so you are not asked again.

0/1000

A hands-on guide by Sanjeev Kujur. Independent, and not affiliated with or endorsed by Pegasystems.

Covering Pega Infinity versions 24.2, 25.1, and 26.1 · Updated 5 Nov 2026