React Tree Grid State Persistence
The Ignite UI for React State Persistence in React Tree Grid allows developers to easily save and restore the grid state. When the IgrGridState
is applied on the React IgrTreeGrid
, it exposes the GetState
, GetStateAsString
, ApplyState
and ApplyStateFromString
methods that developers can use to achieve state persistence in any scenario.
Supported Features
IgrGridState
directive supports saving and restoring the state of the following features:
Sorting
Filtering
Advanced Filtering
Paging
CellSelection
RowSelection
ColumnSelection
RowPinning
Expansion
GroupBy
Columns
Multi column headers
Columns order
Column properties defined by the IColumnState
interface.
Usage
The getState
method returns the grid state in a IgrGridStateInfo
object, containing all the state info. Additional steps may be required in order to save it.
The GetStateAsString
returns a serialized JSON string, so developers can just take it and save it on any data storage (database, cloud, browser localStorage, etc).
The developer may choose to get only the state for a certain feature/features, by passing in an array with feature names as an argument. Empty array will result to using the default state options.
<IgrTreeGrid >
<IgrGridState ref ={(ref) => { gridState = ref; }}></IgrGridState >
</IgrTreeGrid >
tsx
const state : IgrGridStateInfo = gridState.getState([]);
const stateString : string = gridState.getStateAsString([]);
const sortingFilteringStates : IgrGridStateInfo = gridState.getState(['sorting' , 'filtering' ]);
tsx
ApplyState
- The method accepts a IgrGridStateInfo
object as argument and will restore the state of each feature found in the object or specified features as second argument.
ApplyStateFromString
- The method accepts a serialized JSON string as argument and will restore the state of each feature found in the JSON string or specified features as second argument.
gridState.applyState(gridState, []);
gridState.applyStateFromString(gridStateString, []);
gridState.applyState(sortingFilteringStates, [])
tsx
The Options
object implements the IgrGridStateOptions
interface, i.e. for every key, which is the name of a certain feature, there is the boolean value indicating if this feature state will be tracked. GetState
/GetStateAsString
methods will not put the state of these features in the returned value and ApplyState
/ApplyStateFromString
methods will not restore state for them.
<IgrGridState options ={{ cellSelection: false , sorting: false }}> </IgrGridState >
tsx
The simple to use single-point API's allows to achieve a full state persistence functionality in just a few lines of code. Copy paste the code from below - it will save the grid state in the browser LocalStorage
object every time the user leaves the current page. Whenever the user returns to main page, the grid state will be restored. No more need to configure those complex advanced filtering and sorting expressions every time to get the data you want - do it once and have the code from below do the rest for your users:
<IgrTreeGrid rendered ={restoreGridState} >
<IgrGridState ref ={(ref) => { gridState = ref; }}></IgrGridState >
</IgrTreeGrid >
tsx
useEffect(() => {
restoreGridState();
window.addEventListener('beforeunload' , saveGridState);
return () => {
window.removeEventListener('beforeunload' , saveGridState);
}
}, []);
function saveGridState() {
const state = gridState.getState([]);
window.localStorage.setItem('grid-state' , JSON.stringify(state));
}
function restoreGridState() {
const state = window.localStorage.getItem('grid-state' );
if (state) {
gridState.applyState(JSON.parse(state), []);
}
}
function saveGridState() {
const state = gridState.getStateAsString([]);
window.localStorage.setItem('grid-state' , state);
}
function restoreGridState() {
const state = window.localStorage.getItem('grid-state' );
if (state) {
gridState.applyStateFromString(state, []);
}
}
tsx
Demo
export class EmployeesNestedDataItem {
public constructor (init: Partial<EmployeesNestedDataItem> ) {
Object .assign(this , init);
}
public ID: number ;
public Age: number ;
public Salary: number ;
public Productivity: number ;
public City: string ;
public Country: string ;
public Phone: string ;
public HireDate: string ;
public Name: string ;
public Title: string ;
public Employees: EmployeesNestedDataItem_EmployeesItem[];
}
export class EmployeesNestedDataItem_EmployeesItem {
public constructor (init: Partial<EmployeesNestedDataItem_EmployeesItem> ) {
Object .assign(this , init);
}
public Age: number ;
public Salary: number ;
public Productivity: number ;
public City: string ;
public Country: string ;
public Phone: string ;
public HireDate: string ;
public ID: number ;
public Name: string ;
public Title: string ;
}
export class EmployeesNestedData extends Array <EmployeesNestedDataItem > {
public constructor (items: Array <EmployeesNestedDataItem> | number = -1 ) {
if (Array .isArray(items)) {
super (...items);
} else {
const newItems = [
new EmployeesNestedDataItem(
{
ID : 1 ,
Age : 55 ,
Salary : 80000 ,
Productivity : 90 ,
City : `Berlin` ,
Country : `Germany` ,
Phone : `609-202-505` ,
HireDate : `2008-03-20` ,
Name : `John Winchester` ,
Title : `Development Manager` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 43 ,
Salary : 70000 ,
Productivity : 80 ,
City : `Hamburg` ,
Country : `Germany` ,
Phone : `609-444-555` ,
HireDate : `2011-06-03` ,
ID : 3 ,
Name : `Michael Burke` ,
Title : `Senior Software Developer`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 29 ,
Salary : 60000 ,
Productivity : 80 ,
City : `Munich` ,
Country : `Germany` ,
Phone : `609-333-444` ,
HireDate : `2009-06-19` ,
ID : 2 ,
Name : `Thomas Anderson` ,
Title : `Senior Software Developer`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 31 ,
Salary : 90000 ,
Productivity : 80 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `609-222-205` ,
HireDate : `2014-08-18` ,
ID : 11 ,
Name : `Monica Reyes` ,
Title : `Software Development Team Lead`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 35 ,
Salary : 70000 ,
Productivity : 70 ,
City : `Koln` ,
Country : `Germany` ,
Phone : `609-502-525` ,
HireDate : `2015-09-17` ,
ID : 6 ,
Name : `Roland Mendel` ,
Title : `Senior Software Developer`
})]
}),
new EmployeesNestedDataItem(
{
ID : 4 ,
Age : 42 ,
Salary : 90000 ,
Productivity : 80 ,
City : `Kielce` ,
Country : `Poland` ,
Phone : `609-202-505` ,
HireDate : `2014-01-22` ,
Name : `Ana Sanders` ,
Title : `CEO` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 44 ,
Salary : 80000 ,
Productivity : 80 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `609-202-505` ,
HireDate : `2014-04-04` ,
ID : 14 ,
Name : `Laurence Johnson` ,
Title : `Director`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 25 ,
Salary : 85000 ,
Productivity : 55 ,
City : `Paris` ,
Country : `France` ,
Phone : `609-202-505` ,
HireDate : `2017-11-09` ,
ID : 5 ,
Name : `Elizabeth Richards` ,
Title : `Vice President`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 39 ,
Salary : 88000 ,
Productivity : 88 ,
City : `London` ,
Country : `UK` ,
Phone : `609-202-505` ,
HireDate : `2010-03-22` ,
ID : 13 ,
Name : `Trevor Ashworth` ,
Title : `Director`
})]
}),
new EmployeesNestedDataItem(
{
ID : 18 ,
Age : 49 ,
Salary : 77000 ,
Productivity : 70 ,
City : `Manchester` ,
Country : `UK` ,
Phone : `222-555-577` ,
HireDate : `2014-01-22` ,
Name : `Victoria Lincoln` ,
Title : `Senior Accountant` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 43 ,
Salary : 70000 ,
Productivity : 80 ,
City : `Hamburg` ,
Country : `Germany` ,
Phone : `609-444-555` ,
HireDate : `2011-06-03` ,
ID : 23 ,
Name : `Thomas Burke` ,
Title : `Senior Accountant`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 29 ,
Salary : 60000 ,
Productivity : 80 ,
City : `Munich` ,
Country : `Germany` ,
Phone : `609-333-444` ,
HireDate : `2009-06-19` ,
ID : 22 ,
Name : `Michael Anderson` ,
Title : `Junior Accountant`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 31 ,
Salary : 90000 ,
Productivity : 80 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `609-222-205` ,
HireDate : `2014-08-18` ,
ID : 21 ,
Name : `Roland Reyes` ,
Title : `Accountant Team Lead`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 35 ,
Salary : 70000 ,
Productivity : 70 ,
City : `Koln` ,
Country : `Germany` ,
Phone : `609-502-525` ,
HireDate : `2015-09-17` ,
ID : 24 ,
Name : `Monica Mendel` ,
Title : `Senior Software Developer`
})]
}),
new EmployeesNestedDataItem(
{
ID : 10 ,
Age : 61 ,
Salary : 85000 ,
Productivity : 890 ,
City : `Lyon` ,
Country : `France` ,
Phone : `259-266-887` ,
HireDate : `2010-01-01` ,
Name : `Yang Wang` ,
Title : `Localization Developer` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 31 ,
Salary : 90000 ,
Productivity : 80 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `609-222-205` ,
HireDate : `2014-08-18` ,
ID : 11 ,
Name : `Monica Reyes` ,
Title : `Software Development Team Lead`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 35 ,
Salary : 70000 ,
Productivity : 70 ,
City : `Koln` ,
Country : `Germany` ,
Phone : `609-502-525` ,
HireDate : `2015-09-17` ,
ID : 6 ,
Name : `Roland Mendel` ,
Title : `Senior Software Developer`
})]
}),
new EmployeesNestedDataItem(
{
ID : 35 ,
Age : 35 ,
Salary : 75000 ,
Productivity : 75 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `688-244-844` ,
HireDate : `2014-01-22` ,
Name : `Janine Munoz` ,
Title : `HR` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 43 ,
Salary : 70000 ,
Productivity : 80 ,
City : `Hamburg` ,
Country : `Germany` ,
Phone : `609-444-555` ,
HireDate : `2011-06-03` ,
ID : 3 ,
Name : `Michael Burke` ,
Title : `Senior Software Developer`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 31 ,
Salary : 90000 ,
Productivity : 80 ,
City : `Warasw` ,
Country : `Poland` ,
Phone : `609-222-205` ,
HireDate : `2014-08-18` ,
ID : 11 ,
Name : `Monica Reyes` ,
Title : `Software Development Team Lead`
})]
}),
new EmployeesNestedDataItem(
{
ID : 10 ,
Age : 49 ,
Salary : 95000 ,
Productivity : 80 ,
City : `Krakow` ,
Country : `Poland` ,
Phone : `677-266-555` ,
HireDate : `2010-01-01` ,
Name : `Yang Wang` ,
Title : `Sales Manager` ,
Employees : [
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 29 ,
Salary : 60000 ,
Productivity : 80 ,
City : `Munich` ,
Country : `Germany` ,
Phone : `609-333-444` ,
HireDate : `2009-06-19` ,
ID : 2 ,
Name : `Thomas Anderson` ,
Title : `Senior Software Developer`
}),
new EmployeesNestedDataItem_EmployeesItem(
{
Age : 35 ,
Salary : 70000 ,
Productivity : 70 ,
City : `Koln` ,
Country : `Germany` ,
Phone : `609-502-525` ,
HireDate : `2015-09-17` ,
ID : 6 ,
Name : `Roland Mendel` ,
Title : `Senior Software Developer`
})]
}),
];
super (...newItems.slice(0 ));
}
}
}
ts コピー import React , { useEffect, useRef, useState } from "react" ;
import ReactDOM from "react-dom/client" ;
import {
FilterMode,
IgrActionStrip,
IgrColumn,
IgrGridModule,
IgrGridPinningActions,
IgrGridToolbar,
IgrGridToolbarActions,
IgrGridToolbarHiding,
IgrGridToolbarPinning,
IgrPaginator,
IgrGridState,
IgrGridStateOptions,
GridSelectionMode,
IgrTreeGrid,
} from "@infragistics/igniteui-react-grids" ;
import {
IgrButton,
IgrCheckbox,
IgrCheckboxModule,
IgrCheckboxChangeEventArgs,
IgrIcon,
IgrIconModule,
} from "@infragistics/igniteui-react" ;
import { registerIconFromText } from "igniteui-webcomponents" ;
import "@infragistics/igniteui-react-grids/grids/combined" ;
import "@infragistics/igniteui-react-grids/grids/themes/light/bootstrap.css" ;
import "./index.css" ;
import { EmployeesNestedData } from "./EmployeesNestedData" ;
const mods : any [] = [IgrGridModule, IgrIconModule, IgrCheckboxModule];
mods.forEach((m) => m.register());
const restoreIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/></svg>' ;
const saveIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z"/></svg>' ;
const clearIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>' ;
const forwardIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M647-440H160v-80h487L423-744l57-56 320 320-320 320-57-56 224-224Z"/></svg>' ;
const deleteIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>' ;
const refreshIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>' ;
export default function App() {
const gridData = new EmployeesNestedData();
const [allOptions , setAllOptions ] = useState(true );
const [options , setOption ] = useState<IgrGridStateOptions > ({
cellSelection: true ,
rowSelection: true ,
filtering: true ,
advancedFiltering: true ,
paging: true ,
sorting: true ,
columns: true ,
expansion: true ,
rowPinning: true ,
columnSelection: true ,
});
let grid : IgrTreeGrid ;
function gridRef (ref : IgrTreeGrid ) {
grid = ref;
}
let paginatorRef = useRef<IgrPaginator > (null );
const stateKey = "tree-grid-state" ;
let gridStateRef = useRef<IgrGridState > (null );
useEffect(() => {
registerIconFromText("restore" , restoreIcon, "material" );
registerIconFromText("save" , saveIcon, "material" );
registerIconFromText("clear" , clearIcon, "material" );
registerIconFromText("forward" , forwardIcon, "material" );
registerIconFromText("delete" , deleteIcon, "material" );
registerIconFromText("refresh" , refreshIcon, "material" );
restoreGridState();
window.addEventListener("beforeunload" , saveGridState);
return () => {
window.removeEventListener("beforeunload" , saveGridState);
};
}, []);
function saveGridState() {
const state = gridStateRef.current.getStateAsString([]);
window.localStorage.setItem(stateKey, state);
}
function restoreGridState() {
const state = window.localStorage.getItem(stateKey);
if (state) {
gridStateRef.current.applyStateFromString(state, []);
}
}
function resetGridState() {
paginatorRef.current.page = 0 ;
paginatorRef.current.perPage = 15 ;
paginatorRef.current.totalRecords = gridData.length;
grid.clearFilter(null );
grid.sortingExpressions = [];
grid.deselectAllColumns();
grid.deselectAllRows();
grid.clearCellSelection();
}
function onChange(s: IgrCheckbox, e: IgrCheckboxChangeEventArgs) {
if (s.name === "allFeatures" ) {
setOption({
cellSelection: e.detail.checked,
rowSelection: e.detail.checked,
filtering: e.detail.checked,
advancedFiltering: e.detail.checked,
paging: e.detail.checked,
sorting: e.detail.checked,
columns: e.detail.checked,
expansion: e.detail.checked,
rowPinning: e.detail.checked,
columnSelection: e.detail.checked,
});
for (const key of Object .keys (options )) {
gridStateRef .current .options [key ] = e.detail.checked;
}
} else {
gridStateRef.current.options[s.name] = e.detail.checked;
}
}
function leavePage() {
saveGridState();
window.location.replace("./grids/tree-grid/state-persistence-about" );
}
function clearStorage() {
window.localStorage.removeItem(stateKey);
}
function reloadPage() {
window.location.reload();
}
return (
<div className ="vertical sampleContainer" >
<div className ="container horizontal" >
<IgrButton clicked ={restoreGridState} >
<IgrIcon name ="restore" collection ="material" > </IgrIcon >
<span > Restore</span >
</IgrButton >
<IgrButton clicked ={saveGridState} >
<IgrIcon name ="save" collection ="material" > </IgrIcon >
<span > Save</span >
</IgrButton >
<IgrButton clicked ={resetGridState} >
<IgrIcon name ="clear" collection ="material" > </IgrIcon >
<span > Reset</span >
</IgrButton >
<IgrButton clicked ={leavePage} >
<IgrIcon name ="forward" collection ="material" > </IgrIcon >
<span > Leave</span >
</IgrButton >
<IgrButton clicked ={clearStorage} >
<IgrIcon name ="delete" collection ="material" > </IgrIcon >
<span > Clear</span >
</IgrButton >
<IgrButton clicked ={reloadPage} >
<IgrIcon name ="refresh" collection ="material" > </IgrIcon >
<span > Reload</span >
</IgrButton >
</div >
<div className ="container horizontal" >
<ul >
<li >
Clicking the SAVE button or leaving the page{" " }
<a id ="leaveLink" href ="./grids/tree-grid/state-persistence-about" >
<strong > here</strong >
</a > {" " }
will save grid state to localStorage.
</li >
<li >
Use the control buttons to SAVE / RESTORE / RESET / DELETE / grid
state or LEAVE the page.
</li >
<li >
Select/Deselect checkboxes to control saving / restoring feature
state.
</li >
</ul >
</div >
<div className ="container horizontal" >
<IgrCheckbox name ="allFeatures" change ={onChange} checked ={allOptions} >
<span > All Features</span >
</IgrCheckbox >
<IgrCheckbox
name ="advancedFiltering"
change ={onChange}
checked ={options.advancedFiltering}
>
<span > Adv. Filtering</span >
</IgrCheckbox >
<IgrCheckbox
name ="cellSelection"
change ={onChange}
checked ={options.cellSelection}
>
<span > Cell Selection</span >
</IgrCheckbox >
<IgrCheckbox name ="columns" change ={onChange} checked ={options.columns} >
<span > Columns</span >
</IgrCheckbox >
<IgrCheckbox
name ="columnSelection"
change ={onChange}
checked ={options.columnSelection}
>
<span > Col Selection</span >
</IgrCheckbox >
<IgrCheckbox
name ="expansion"
change ={onChange}
checked ={options.expansion}
>
<span > Expansion</span >
</IgrCheckbox >
<IgrCheckbox
name ="filtering"
change ={onChange}
checked ={options.filtering}
>
<span > Filtering </span >
</IgrCheckbox >
<IgrCheckbox name ="paging" change ={onChange} checked ={options.paging} >
<span > Paging</span >
</IgrCheckbox >
<IgrCheckbox
name ="rowPinning"
change ={onChange}
checked ={options.rowPinning}
>
<span > Row Pinning</span >
</IgrCheckbox >
<IgrCheckbox
name ="rowSelection"
change ={onChange}
checked ={options.rowSelection}
>
<span > Row Selection</span >
</IgrCheckbox >
<IgrCheckbox name ="sorting" change ={onChange} checked ={options.sorting} >
<span > Sorting</span >
</IgrCheckbox >
</div >
<IgrTreeGrid
ref ={gridRef}
data ={gridData}
primaryKey ="ID"
childDataKey ="Employees"
width ="95%"
height ="500px"
autoGenerate ={false}
moving ={true}
allowFiltering ={true}
allowAdvancedFiltering ={true}
filterMode ={FilterMode.ExcelStyleFilter}
columnSelection ={GridSelectionMode.Multiple}
rowSelection ={GridSelectionMode.Multiple}
>
<IgrGridState ref ={gridStateRef} > </IgrGridState >
<IgrGridToolbar >
<IgrGridToolbarActions >
<IgrGridToolbarHiding > </IgrGridToolbarHiding >
<IgrGridToolbarPinning > </IgrGridToolbarPinning >
</IgrGridToolbarActions >
</IgrGridToolbar >
<IgrActionStrip >
<IgrGridPinningActions > </IgrGridPinningActions >
</IgrActionStrip >
<IgrPaginator ref ={paginatorRef} > </IgrPaginator >
<IgrColumn
field ="ID"
header ="ID"
sortable ={true}
filterable ={true}
hidden ={true}
> </IgrColumn >
<IgrColumn
field ="Name"
header ="Name"
sortable ={true}
filterable ={true}
pinned ={true}
> </IgrColumn >
<IgrColumn
field ="Title"
header ="Title"
sortable ={true}
filterable ={true}
> </IgrColumn >
<IgrColumn
field ="Age"
header ="Age"
sortable ={true}
filterable ={true}
groupable ={true}
> </IgrColumn >
<IgrColumn
field ="Phone"
header ="Phone"
sortable ={true}
filterable ={true}
> </IgrColumn >
<IgrColumn
field ="OnPTO"
header ="On PTO"
sortable ={true}
filterable ={true}
groupable ={true}
> </IgrColumn >
</IgrTreeGrid >
</div >
);
}
const root = ReactDOM.createRoot (document.getElementById("root" ));
root.render (<App /> );
tsx コピー
.horizontal {
gap: 10px ;
flex-basis : fit-content;
flex-wrap : wrap;
}
.sampleContainer {
padding : 0.5rem
}
css コピー
Like this sample? Get access to our complete Ignite UI for React toolkit and start building your own apps in minutes. Download it for free.
Limitations