DEV Community

Cover image for How to Build a Reusable React Data Grid with TypeScript
Vishal Porwal
Vishal Porwal

Posted on Edited on

How to Build a Reusable React Data Grid with TypeScript

As a React application grows, data grids often become one of the most duplicated parts of the codebase. Orders, customers, invoices, inventory, and audit logs may all need tables, but each screen can end up implementing formatting, editing, validation, and loading behavior differently.

A reusable React Grid Layout component solves this by separating what a screen displays from how the grid behaves.

Instead of configuring every grid from scratch, developers define columns and provide data. The shared component handles the common behavior consistently.

  1. Define the API Before Writing the Component
    The first step is deciding what the component should expose.
    A common mistake is wrapping a grid library and passing every underlying configuration option directly through React props. That creates a wrapper, but it does not necessarily create a consistent component.
    A better API exposes the capabilities most screens actually need:
    columns — field names, headers, data types, and editing rules
    data or url — local records or a remote data source
    groupBy — an optional grouping field
    title, height, and emptyText — presentation settings
    onSelectionChange — a callback for selected rows
    onCellEdit — a callback for handling edits
    A small set of imperative methods, such as reloading data or clearing filters
    The goal is to make common use cases simple while keeping advanced configuration available when necessary.

  2. Use Typed Column Definitions
    Column definitions are a natural place to standardize how data is displayed.
    For example, a team might support text, number, currency, date, and boolean types. Each type can define its own formatting and editing behavior.

type ColumnType =
  | "text"
  | "number"
  | "currency"
  | "date"
  | "boolean";

interface GridColumn {
  field: string;
  header: string;
  type?: ColumnType;
  width?: number;
  editable?: boolean;
  required?: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Now screens can describe business data instead of implementation-specific widget settings.
A currency column automatically receives the correct number format. A date column uses the application's standard date display. An editable required field receives consistent validation rules.
These decisions belong in one shared column-mapping function rather than being repeated across every screen.

  1. Build the Reusable DataGrid Component
    The component translates the public API into the configuration expected by the underlying grid.
    A typical implementation needs to manage:
    Column mapping and data types.
    Local or remote data loading.
    Selection and editing callbacks.
    Component references for supported imperative operations.
    Updates when data changes.
    Error handling when an edit cannot be saved.
    When using React hooks, avoid unnecessary configuration rebuilds. Memoize structural configuration where appropriate, and update the existing data store when only the records change—provided the underlying grid supports that approach.
    This can help preserve user context, such as scroll position and column widths, while new data is loaded.

  2. Make Editing Reliable
    Inline editing is easy to demonstrate but requires careful behavior in a production application.
    A robust editing flow should:
    Capture the edited field, new value, and previous value.
    Validate the change.
    Send the update to the appropriate data source.
    Commit the change when saving succeeds.
    Restore the previous value when saving fails.
    Communicate errors clearly to users.

For example, the shared component could expose an asynchronous callback:

<DataGrid
  columns={orderColumns}
  data={orders}
  onCellEdit={async ({ row, field, value }) => {
    await saveOrder(row.id, {
      [field]: value,
    });
  }}
/>
Enter fullscreen mode Exit fullscreen mode

The underlying implementation must explicitly define how a rejected promise affects the grid's record state. That behavior should be tested rather than assumed.
Centralizing this logic helps ensure that all screens follow the same editing and error-handling rules.

  1. Support Local and Remote Data
    A reusable component should accommodate the data patterns used by the application.
    For smaller datasets, local data may be sufficient. For larger datasets, a remote source may need server-side paging, sorting, and filtering.
    These approaches should share the same public API wherever practical, while the implementation handles their differences.
    For remote data, document the expected API response format, total-record count, filtering parameters, sorting parameters, and error behavior. Server-side grouping and summaries may also require explicit API support.

  2. Use the Component Across Multiple Screens
    Once the shared component exists, individual screens should become easier to maintain.
    An orders screen might define editable customer, amount, date, and status columns. A customer screen might use read-only name, country, and account-value columns.
    Both screens reuse the same component, so formatting, empty states, selection, and other shared behavior remain consistent.
    This also creates one place to improve accessibility, fix bugs, and update common behavior.

  3. Choose the Underlying Grid Carefully
    Building a reusable wrapper does not mean you must build every grid feature yourself.
    If the application needs advanced editing, grouping, summaries, remote sorting, or paging, evaluate whether an existing library can provide those capabilities.
    For example, Sencha's ReExt integrates Ext JS components into React applications. It is an option for teams that want enterprise-oriented grid functionality alongside charts, forms, and other UI components.
    The right choice depends on licensing, technical requirements, the existing stack, and how much functionality your team wants to maintain internally.

  4. Test and Document the Shared Component
    A reusable grid becomes a critical piece of application infrastructure, so its behavior deserves dedicated testing.
    Consider testing:
    Column type mapping and formatting.
    Required-field validation.
    Successful and failed edits.
    Selection callbacks.
    Local data updates.
    Remote loading and pagination.
    Grouping and summaries, if supported.
    Keyboard accessibility and responsive behavior.
    Document the supported props, column types, examples, and extension points. A small demonstration page or Storybook collection can help developers learn the component without reading its implementation.

Final Thoughts

A reusable React data grid is more than a wrapper around a table. It establishes consistent conventions for data display, editing, validation, and maintenance across the application.
Start with a small API, use typed column definitions, centralize shared behavior, and test the features that every screen depends on. Adopt a dedicated grid library when its capabilities make more sense than maintaining those features yourself.
For a complete TypeScript walkthrough using ReExt and the Ext JS grid, read the original Sencha guide: How to Build a Reusable React Grid Component.

Top comments (0)