AdapTable State
AdapTable provides a comprehensive state management architecture that is very flexible and configurable.
The AdapTable state
determines how the AdapTable component instance will look and behave.
It's a single object that encapsulates all the information the component needs:
- columns (size, order, visibility, sorting, etc)
- views (view definitions, current view, grouping, pivoting, aggregations, etc)
- calculated columns
- alerts
- filters
- styled cells
- flashing cells
- quick search state
- and a lot more
Each major functionality in AdapTable has a property in the state object - state.view
for managing views, state.dashboard
for managing the dashboard, state.alert
for defining alerts, etc.
Note
AdapTable State refers to the objects that are used to manage the grid, e.g. views, styles, reports.
It does not include any data in the grid.
A default (initial) state object is passed as a prop to the AdapTable component.
<Adaptable.Provider
primaryKey="id"
adaptableId="Basic State"
defaultState={defaultState}
data={[...]}
>
<Adaptable.UI />
</Adaptable.Provider>
In this example the state contains two views:
- a simple table view with 4 columns
- a grouped table view - grouped by language
By configuring the default state, you control how the user will initially see the AdapTable component. After the component is initialised, the user will interact with it and almost every interaction will update the state object internally (e.g.: changing column order, sorting, creating a new view).
Note
AdapTable comes with a number of ways to manage state, which we'll cover in this page and in dedicated pages for each topic.
By default, changes to the state are persisted as JSON in the browser's local storage, by using the adaptableId
prop as the key for the local storage entry. Of course you'll want to change this behaviour in production, and we'll cover that in the State Persistence section.
Primary Key
One of the mandatory props for the Adaptable.Provider
component is the primaryKey
.
This is a string value used to identify a field from the data source which is guaranteed to contain distinct values for all records.
import { Adaptable } from '@adaptabletools/adaptable-infinite-react';
const dataSource = Promise.resolve([
{ id: 1, name: 'John', age: 21 },
{ id: 2, name: 'Mary', age: 22 },
{ id: 3, name: 'Susan', age: 23 },
])
function App() {
return <Adaptable.Provider
primaryKey="id"
data={dataSource}
defaultState={...}
>
<Adaptable.UI>
</Adaptable.Provider>
}
Note
The primaryKey
is used to uniquely identify a row in the grid.
However, you don't necessarily need to have a column bound to this property.
You need to make sure the primary key values are unique, otherwise AdapTable will behave inconsistently. Many features of AdapTable rely on this uniqueness - for example, row selection, cell selection, editing, cell summary to name a few.
Defining views and columns
A view is a set of columns and associated objects (e.g. sorting, flashing cells, styled cells etc).
The AdapTable state can define multiple views, which can be switched between at run-time, either from the view switcher in the dashboard or via the Settings Panel or even via the view API.
Views can be either Table or Pivot and there is no limit to how many can be provided.
Only one view is active at any given time.
Find Out More
See Guide to using Views in AdapTable for full information on this topic
The state.view property is a mandatory property, as you need to have at least one view. The view contains an array of columns, which are references to the columns made available to the component via state.globalEntities.availableColumns
.
The state.globalEntities.availableColumns
is an object keyed by the column id, which contains all the columns that can be used in the views - each column describes the field it's bound to, the data type, whether it's sortable/pinned/groupable and more.
In turn, each view defines an array of columns, which are objects that refer to the available columns via the id. We call those column references.
import { type AdaptableColDef } from '@adaptabletools/adaptable-infinite-react';
// JavaScript Web frameworks
const availableColumns: Record<string, AdaptableColDef> = {
id: { field: 'id', dataType: 'number' },
name: { field: 'name', label: 'Name', dataType: 'text' },
language: { field: 'language', dataType: 'text' },
license: { field: 'license', dataType: 'text' },
stars: {
field: 'github_stars', editable: true, dataType: 'number'
},
'2xstars': { expression: '[stars] * 2', dataType: 'number' }
};
<Adaptable.Provider
primaryKey="id"
defaultState={{
view: {
currentViewId: 'tableView',
views: [
{
id: 'tableView', label: 'Table View',
columns: [
{ id: 'name' }, // reference the `name` column
{ id: 'stars', editable: false },
{ id: 'language' },
],
}
]
},
globalEntities: {
availableColumns,
},
}}
adaptableId="Adaptable Demo"
data={[...]}
>
</Adaptable.Provider>
The column references in a view allow you to override some of the properties of a column definition (the object specified in state.globalEntities.availableColumns
). For example, in the above example we've overridden the editable
property of the stars
column to false
.
Find Out More
Our page on Working with columns has more information on how to define columns and how to use column references.
Note - Global Entities
The items in Global Entities include objects which will be used across multiple views. This includes columns - which can be defined here and then simply referenced in each view.
State Persistence
After the component is initialised with the defaultState
prop, AdapTable will manage the state internally and will persist it in the browser's local storage by default. For using the controlled state prop, see below.
This behavior can be configured by specifying a custom persistState
function. If you specify a custom persistState
function to change the way the state is persisted (e.g. send it to your application server), you'll also need to pair it with the loadState
function. loadState
will be called when the AdapTable component is initialised and it should fetch the state from the persistence layer (e.g. from your application server) - it will do the exact opposite of persistState
.
Using the adaptableId
The adaptableId
prop should be unique for every Adaptable
instance. The value of this prop will be used as the default key used for persisting state in localStorage
. The default persistence implementation stores the state into the browser localStorage, under this key. As mentioned above, you can customise the persistence by specifying the persistState/loadState
functions. They will be called with context.stateKey
, which has the value of the adaptableId
prop.
<Adaptable.Provider
primaryKey="id"
adaptableId="Basic State"
persistState={async (context) => {
const { state, stateKey } = context;
await fetch(`https://my-server.com/save-state?key=${stateKey}`, {
method: 'POST',
body: JSON.stringify(state),
});
}}
loadState={(context) => {
const { stateKey } = context;
return fetch(`https://my-server.com/load-state?key=${stateKey}`).then((response) =>
response.json(),
);
}}
>
<Adaptable.UI />
</Adaptable.Provider>
This example shows a custom implementation of the persistState
/loadState
props that can be used to send the state to a server.
Deep Dive
Understanding persistState and loadState
Both persistState
and loadState
are called with a context object that contains the following propeties:
stateKey
- the key to be used for the persistence (this will have the value of theadaptableId
prop)adaptableApi
- a reference to the AdaptableAPI that the component exposes.
Additionally, the context param for the persistState
function also has a state
property, which contains the state diff that needs to be persisted.
❗️ ❗️ ❗️ The context object in persistState
gives you access to a state
diff - this is the JSON patch difference between the default state and the current state. This means that by default, only the state changes are sent to the server, instead of the whole state object.
This state diff object will look something like:
[
{
op: 'add',
path: ['view', 'views', '0', 'columns', '1', 'width'],
value: 500,
},
{
op: 'replace',
path: ['view', 'currentViewId'],
value: 'groupedView',
},
];
Listening to state changes
In AdapTable it's possible to listen to state changes - either at a very coarse level, by using the onStateChange
function or very granularly, by using the onStateChangeWithSelectors
function. The following sections will cover and explain both scenarios.
Listening to state changes with onStateChange
Using the onStateChange
prop notifies you of any updates to the state.
<Adaptable.Provider
adaptableId="Basic Demo"
onStateChange={(state) => {
console.log('State changed', state);
}}
>
<Adaptable.UI />
</Adaptable.Provider>
State updates include resizing and reordering columns, sorting the table, changing the current view, grouping, changing the cell/row selection and basically any action that alters the table.
This example logs the state to the console on any change. Open your devtools to see the logs - you can resize columns, change the column ordering, change the current view, etc.
Listening to granular state changes
Using onStateChange
is fine, but it doesn't tell you what has changed. If you want to listen to specific state changes, you can do that by using the onStateChangeWithSelectors
prop.
For example you might be interested when the user changes the current view, when the column order is updated or when the row selection has changed - all this is possible if you use onStateChangeWithSelectors
.
import { Adaptable, withState } from '@adaptabletools/adaptable-infinite-react';
const [currentViewId, setCurrentViewId] = useState(defaultState.view.currentViewId);
function App() {
return (
<Adaptable.Provider
onStateChangeWithSelectors={[
withState({
selector: (state) => state.view.currentViewId,
callback: (currentViewId) => {
setCurrentViewId(currentViewId);
},
}),
]}
data={rowData}
>
<p>
The current view is <b>{currentViewId}</b>
</p>
<Adaptable.UI />
</Adaptable.Provider>
);
}
Deep Dive
Understanding onStateChangeWithSelectors
Using the withState
helper function (import it as a named import from the @adaptabletools/adaptable-infinite-react
package) will give you better typing when passing selector/callback
pairs to the onStateChangeWithSelectors
prop.
You can pass in any number of pairs. Each pair in the array needs
- the
selector
function - can be used to target any part of the AdapTable state - the
callback
function - will be called with the value returned by theselector
function, whenever that part of the state is changed.
Using controlled state
React introduced the concept of controlled components which allows people to use components in a more declarative way and "own" the state of the component in the parent component.
Adaptable fully supports this concept and allows you to use the component in a controlled way. For this, you need to pass in the state
prop instead of the defaultState
prop.
const [state, setState] = useState(initialState);
<Adaptable.Provider
primaryKey="id"
adaptableId="Using Controlled State"
state={state}
onStateChange={setState}
data={[...]}
>
<Adaptable.UI />
</Adaptable.Provider>
Note
When using controlled state in AdapTable, you need to pass in the onStateChange
prop as well, otherwise the state will not be updated and the UI will not reflect the user behavior.
In this example we show you how to use controlled state in order to change the current view and the theme without calling an API.
Note
Using controlled state gives you the ability to update Adaptable without calling any API methods.
If you pass a new state
prop to Adaptable, it will update the UI to reflect the state object. This means that you can remove columns, switch the view, change column order, toggle the theme, add a styled cell (and a lot more) just by updating the state
prop - no API calls required.