If you’ve ever tried to build a production-grade
React data table with Redux,
you know the drill: pick a table library, bolt on Redux manually,
argue with your state shape for three hours, and eventually ship something
that half-works. Sematable exists precisely to end that loop.
It’s a React table component
built around Redux from the ground up — not an afterthought adapter,
but a first-class Redux-connected table where selection, pagination,
filtering, and sorting all live in your store by default.
This guide covers everything:
sematable installation,
Redux store setup,
column definitions,
filtering,
pagination,
server-side data handling, and real
sematable examples
you can drop straight into a project.
No fluff, no “just check the docs” deflections.
Sematable is an open-source
React table library that treats Redux as infrastructure, not
an optional plugin. Most table libraries — think react-table, AG Grid, or
MUI DataGrid — are self-contained components that manage their own internal
state. That’s fine until you need to share table state across routes, persist
filters through navigation, or sync selections with a global workflow.
At that point, “self-contained” becomes “isolated island,” and you end up
writing Redux middleware just to bridge the gap.
Sematable skips that bridge entirely. Every table instance registers
a named slice inside your Redux store. Filters, sort order, current page,
page size, and selected row IDs are all plain Redux state — readable
via getState(), dispatchable via actions, and serializable
for persistence or time-travel debugging. If you use Redux DevTools
(and you absolutely should), you’ll see every table interaction as a
discrete, inspectable action. It’s the kind of transparency that makes
debugging a pleasure rather than an archaeological dig.
This architectural choice does come with a prerequisite: you need Redux
already in your project. If you’re running a Context-only or Zustand-based
app, sematable isn’t your tool. But for teams already on a
Redux data table stack — especially those using
Redux Toolkit, redux-saga, or redux-observable — the integration
is almost frictionless. Sematable slots into your existing store
like a first-party feature, not a foreign plugin.
Getting sematable installed
takes about five minutes if you already have a Redux store.
Start with the package itself:
npm install sematable
# or
yarn add sematable
The library has peer dependencies on react, react-dom,
redux, and react-redux — all of which you presumably
already have. Once installed, the single mandatory integration step is
registering the sematable reducer in your Redux store
under the exact key 'sematable'. The library reads its own
state from that key by convention, and changing it will silently break everything:
// store.js
import { createStore, combineReducers } from 'redux';
import { sematableReducer } from 'sematable';
const rootReducer = combineReducers({
sematable: sematableReducer,
// ...your other reducers
});
export const store = createStore(rootReducer);
If you’re on Redux Toolkit, the setup is structurally identical —
just add sematable: sematableReducer inside configureStore‘s
reducer map. The reducer key is non-negotiable; the rest of your
store shape remains whatever you want. After this step,
your sematable setup is complete at the infrastructure level.
Everything else happens at the component level.
Sematable uses a higher-order component (HOC) pattern.
You define a plain table component — essentially just a render function
that describes how to display rows — and then wrap it with
sematable(tableName, columns, config).
The HOC injects all table-management props automatically:
rows (the filtered, sorted, paginated slice of data),
selectedRows, headers (column metadata with sort handlers),
and pagination controls. Your inner component stays clean and focused
purely on rendering.
Column definitions are plain JavaScript objects. Each column needs at minimum
a key (matching a property in your data objects) and a
header string. From there you can opt into sorting with
sortable: true, filtering with filterable: true,
and custom rendering with a render function.
The render function receives the raw cell value and the full
row object, so composing complex cells — status badges, action buttons,
linked names — requires no special API, just standard React:
const columns = [
{
key: 'id',
header: 'ID',
sortable: true,
primaryKey: true,
},
{
key: 'username',
header: 'Username',
sortable: true,
filterable: true,
},
{
key: 'status',
header: 'Status',
render: (val) => (
{val}
),
},
{
key: 'actions',
header: '',
render: (_, row) => (
),
},
];
One column must be marked primaryKey: true.
This is sematable’s identity field — it uses the primary key value
to track row selection, deduplication, and state persistence across
re-renders. If your data has no natural unique ID field, add one
before passing data to the component. Trying to work around the
primary key requirement will produce subtle, maddening bugs with
row selection.
Theory covers the architecture. Code covers the gaps.
Here’s a complete, working
sematable example
— a user management table with sorting, filtering, pagination,
and row selection, ready to plug into a Redux-connected React app:
// UsersTable.jsx
import React from 'react';
import sematable, { Table } from 'sematable';
const columns = [
{ key: 'id', header: 'ID', primaryKey: true, sortable: true },
{ key: 'name', header: 'Name', sortable: true, filterable: true },
{ key: 'email', header: 'Email', filterable: true },
{ key: 'role', header: 'Role', sortable: true },
];
const UsersTableInner = ({ rows, headers }) => (
{rows.map((row) => (
{row.id}
{row.name}
{row.email}
{row.role}
))}
);
export default sematable('usersTable', columns, {
defaultPageSize: 10,
showPageSize: true,
showFilter: true,
})(UsersTableInner);
// ParentComponent.jsx
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import UsersTable from './UsersTable';
import { fetchUsers } from './usersSlice';
const UsersPage = () => {
const dispatch = useDispatch();
const users = useSelector((state) => state.users.list);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
Users
);
};
export default UsersPage;
A few things worth noting in this
sematable tutorial.
The table name 'usersTable' is the Redux state key —
it must be unique per table instance in your application.
If you render two tables with the same name, they’ll share state,
which is occasionally intentional but usually catastrophic.
The data prop is the full unfiltered dataset;
sematable handles slicing it for the current page and filter state internally.
You don’t pre-filter before passing it in.
Sematable filtering
operates on two levels: a global text filter that searches across all
filterable: true columns simultaneously, and per-column
filtering if you choose to wire it up manually. The global filter
is enabled by default when you pass showFilter: true
in the config object. Sematable renders a search input above the table
and updates the Redux state on every keystroke — the filtering itself
is synchronous client-side substring matching, case-insensitive by default.
For more granular control, you can dispatch sematable’s built-in filter
actions directly from your own UI elements. This is useful when you want
custom filter dropdowns, date pickers, or range inputs that live outside
the standard search bar. Import filterChange from sematable,
dispatch it with your table name and filter value, and the table state
updates accordingly:
import { filterChange } from 'sematable';
import { useDispatch } from 'react-redux';
const RoleFilter = () => {
const dispatch = useDispatch();
const handleChange = (e) => {
dispatch(filterChange('usersTable', e.target.value));
};
return (
);
};
Because filter state lives in Redux, it persists across route changes
as long as your store isn’t reset. This is a subtle but powerful feature:
a user can navigate away from your users table, come back,
and their filter is still active. Whether that’s the right UX for your app
is your call — but the capability is there without any extra work,
which is more than you can say for most
React table components
that store filter state locally.
Out of the box,
sematable pagination
is client-side: pass your full dataset via data,
configure defaultPageSize, and sematable slices it.
The pagination controls — previous, next, page number buttons,
and an optional page-size selector — render automatically when
showPageSize: true is in your config.
The current page and page size live in the Redux slice for your table,
accessible via selectors if you need to read them from elsewhere
(say, for a “showing X of Y results” status bar).
Server-side pagination is where things get slightly more involved,
but it’s clean once you understand the data contract.
Instead of passing your full dataset, you pass only the current page’s rows
and add a totalCount prop equal to the total number of
records matching the current filter. Sematable uses totalCount
to calculate the correct page count and render pagination correctly
without having all rows loaded. Your job is to re-fetch from the API
whenever the Redux table state changes:
// Server-side pagination pattern
import { getTableState } from 'sematable';
import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
const UsersPageServerSide = () => {
const dispatch = useDispatch();
const tableState = useSelector((state) =>
getTableState(state, 'usersTable')
);
const { data, totalCount } = useSelector(
(state) => state.users
);
useEffect(() => {
if (!tableState) return;
dispatch(
fetchUsers({
page: tableState.page,
pageSize: tableState.pageSize,
filter: tableState.filterValue,
sortKey: tableState.sortKey,
sortDirection: tableState.sortDirection,
})
);
}, [tableState, dispatch]);
return (
);
};
This pattern is what makes sematable genuinely useful in production scenarios.
The table component doesn’t care whether data comes from a local array
or a paginated API — it renders whatever’s in data and
trusts totalCount for pagination math.
Your Redux thunk handles the API call. The concerns stay separated,
the code stays testable, and your
React server-side table
behaves exactly like its client-side counterpart from the user’s perspective.
Row selection is one of sematable’s strongest features — and one of the
main reasons to choose it over a simpler
React data grid solution.
Enable it by adding a selection column to your columns array
(sematable provides a built-in SortableHeader and selection
checkbox setup), and the HOC takes care of tracking which rows are selected.
The selected row IDs — derived from your primaryKey field —
are stored in Redux and accessible via the selectedRows prop
injected into your component.
Reading selection state from outside the table component is equally
straightforward. Use the getTableState selector
(exported by sematable) to pull the full table state object from Redux,
then read selectedRows from it. This is invaluable for
“bulk action” UIs — delete selected, export selected, assign role to selected —
where the action trigger lives in a toolbar component that has no
direct relationship to the table component in the React tree:
// BulkActions.jsx
import { getTableState } from 'sematable';
import { useSelector, useDispatch } from 'react-redux';
import { deleteUsers } from './usersSlice';
const BulkActions = () => {
const dispatch = useDispatch();
const tableState = useSelector((state) =>
getTableState(state, 'usersTable')
);
const selectedIds = tableState?.selectedRows ?? [];
const handleDelete = () => {
if (selectedIds.length === 0) return;
dispatch(deleteUsers(selectedIds));
};
return (
);
};
This architecture demonstrates why Redux integration matters for tables.
In a local-state table, getting selected rows into a sibling component
means lifting state, using a ref, or adding a callback chain through
multiple layers. With sematable’s
Redux table,
you just read the store. The component coupling problem doesn’t exist.
Sematable ships with minimal default styling — intentionally so.
The table structure outputs standard HTML
| {header.title} {header.sortable && ( {header.sortDirection === 'asc' ? '↑' : '↓'} )} | );
|---|
Recent Comments