Our blazing fast Grid component built with pure JavaScript


Post by Yamo93 »

When the React BryntumGrid component is mounted, the data (which is managed in Mobx State Tree) is filled in the config. However, when that state changes, the grid is not updated.

How can I force such an update? I tried reloading the dataset with store.loadData() but the view isn't updated.


Post by saki »

There shouldn't be anything else needed just binding the data property to your state. The following App.js can replace Grid basic example and it works as expected. The grid starts populated and is cleared after 3s:

/**
 * Application
 */
import React, { Fragment, useState, useCallback, useRef } from 'react';
import {
    BryntumDemoHeader,
    BryntumThemeCombo,
    BryntumGrid,
    BryntumButton,
    BryntumNumberField
} from '@bryntum/grid-react';
import { Toast } from '@bryntum/grid/grid.umd';

import { useGridConfig, rowHeightConfig } from './AppConfig';
import './App.scss';

const App = props => {
    const grid = useRef(null);

    const [rowHeight, setRowHeight] = useState(50);
    const [selectedRow, setSelectedRow] = useState(null);

    const handleCellButtonClick = useCallback(record => {
        Toast.show('Go ' + record.team + '!');
    }, []);

    const gridConfig = useGridConfig(handleCellButtonClick);
    const [data, setData] = useState(gridConfig.data)

    delete gridConfig.data;

    const handleAddRow = useCallback(() => {
        grid.current.instance.store.add({
            team: 'New team',
            division: '?'
        });
    }, []);

    const handleRemoveRow = useCallback(() => {
        if (selectedRow) {
            selectedRow.remove();
        }
    }, [selectedRow]);

    const handleSelectionChange = useCallback(({ selected, mode }) => {
        if (mode === 'row') {
            setSelectedRow(selected.length ? selected[0] : null);
        }
    }, []);

    const getTeam = useCallback(() => {
        return selectedRow ? selectedRow.team : 'None';
    }, [selectedRow]);

    setTimeout(()=>{
        setData([])
    }, 3000);

    return (
        <Fragment>
            <BryntumDemoHeader
                title="React Basic Grid demo"
                href="../../../../../#example-frameworks-react-javascript-basic"
                children={<BryntumThemeCombo />}
            />
            <div className="demo-toolbar align-right">
                <BryntumNumberField
                    {...rowHeightConfig}
                    value={rowHeight}
                    onChange={({ value }) => setRowHeight(value)}
                />
                <div className="spacer"></div>
                <label className="selected-row">
                    Selected team: <span>{getTeam()}</span>
                </label>
                <BryntumButton
                    cls="b-green b-raised"
                    icon="b-fa-plus"
                    tooltip="Add Team"
                    onClick={handleAddRow}
                />
                <BryntumButton
                    cls="b-red b-raised"
                    icon="b-fa-trash"
                    tooltip="Remove Team"
                    disabled={!selectedRow}
                    onClick={handleRemoveRow}
                />
            </div>
            <BryntumGrid
                ref={grid}
                {...gridConfig}
                data={data}
                rowHeight={rowHeight}
                onSelectionChange={handleSelectionChange}
            />
        </Fragment>
    );
};

export default App;

Post by Yamo93 »

I'm using a tree grid, and when I'm updating the array, the column renderer is running again, but the UI isn't updating. It's like the UI and the data are out of sync.

Also, I'm getting this in the console:

BryntumGridComponent development warning!
"columns" is a static config option for component constructor only. No runtime changes are supported!
Please check integration guide: https://www.bryntum.com/docs/grid/#guides/integration/react.md

I don't really get what that is supposed to mean.

Try this to reproduce the issue:

import React, { useState, useEffect, useRef, ReactElement } from 'react';
import { BryntumGrid } from '@bryntum/grid-react';

const Prototyping = (): ReactElement => {
    const [customGridData, setCustomGridData] = useState([]);
    const userGridRef = useRef(null);

useEffect(() => {
    setCustomGridData([
        {
            username: 'User123',
            isAdmin: true,
        },
        {
            username: 'User124',
        },
        {
            username: 'User125',
            isAdmin: true,
        },
        {
            username: 'User126',
        },
        {
            username: 'User127',
            isAdmin: true,
        },
        {
            username: 'User128',
        },
        {
            username: 'User129',
        },
        {
            username: 'User130',
        },
    ]);
}, []);

const deleteUser = (data): void => {
    const users = [...userGridRef.current.gridInstance.store.data];
    const userIndex = users.findIndex(user => user.username === data.record.data.username);
    users.splice(userIndex, 1);
    setCustomGridData(users);
};
const toggleAdmin = (data): void => {
    const users = [...userGridRef.current.gridInstance.store.data];
    const userIndex = users.findIndex(user => user.username === data.record.data.username);
    users[userIndex].isAdmin = !users[userIndex].isAdmin;
    users.splice(userIndex, 1, users[userIndex]);
    setCustomGridData(users);
};

const customGridConfig = {
    data: customGridData,
    treeFeature: true,
    autoHeight: true,
    columns: [
        {
            type: 'tree',
            field: 'username',
            text: 'Username',
            flex: 1,
            editor: false,
        },
        {
            field: 'isAdmin',
            text: 'Is Admin',
            flex: 1,
            editor: false,
            renderer: (data): any => (
                <>
                {data.record.data.isAdmin ? <p>Yes</p> : <p>No</p>}
                {<button onClick={(): void => toggleAdmin(data)}>Toggle Admin</button>}
                </>
            ),
        },
        {
            text: 'Delete user',
            field: '',
            flex: 1,
            renderer: (data): any => {
                return <button onClick={(): void => deleteUser(data)}>Delete</button>;
            },
        },
    ],
};

return (
    <BryntumGrid ref={ userGridRef } {...customGridConfig } />
);
};

export default Prototyping;

Try to toggle the admin status of a user. Note that the array is correctly updated, but the update is not shown in the UI. In other words, "Yes" doesn't turn into "No" and vice versa.

Last edited by Yamo93 on Fri Apr 23, 2021 10:41 am, edited 1 time in total.

Post by saki »

Re warning about columns: columns property is config-only what means that it can be set only once on the grid instantiation. Any runtime attempts to change columns are ignored at runtime with the above warning. The array passed as config is transformed to ColumnStore so if you need to alter columns you need to treat this property as ColumnStore.

Re data: Normally, we do not pass the whole data object to the grid when only one record changes but we just update the underlying record. Therefore, your admin toggle button can look simple like this:

renderer: data => (
    <>
        {data.record.data.isAdmin ? <p>Yes</p> : <p>No</p>}
        {<button onClick={() => data.record.isAdmin = !data.record.isAdmin}>Toggle Admin</button>}
    </>
)

Would this work for you?


Post by Yamo93 »

Okay, thanks for the explanation. I managed to update the UI by updating the grid store manually. Updating that record wasn't as easy as in your example, because I had to recursively find that record in the tree structure and update the fields.

The conclusion is that the BryntumGrid component can't sync the data with the UI on a record level, but only on an array level. So if a property in a record changes, that isn't reflected in the UI automatically.

This will be cumbersome though, because it means that I have to manage two states: the Mobx/React state and the Grid store state.

I really hope that this will be dealt with in a newer version of your React wrapper, so that the data is automatically in-sync with the UI.


Post by pmiklashevich »

Hello,

Okay, thanks for the explanation. I managed to update the UI by updating the grid store manually. Updating that record wasn't as easy as in your example, because I had to recursively find that record in the tree structure and update the fields.

There are methods to search in the tree: https://www.bryntum.com/docs/grid/#guides/data/treedata.md#retrieving-nodes

The conclusion is that the BryntumGrid component can't sync the data with the UI on a record level, but only on an array level. So if a property in a record changes, that isn't reflected in the UI automatically.

When you change a record field it always updates the grid row (if store events are not suspended).

This will be cumbersome though, because it means that I have to manage two states: the Mobx/React state and the Grid store state.

The Bryntum Grid has the fully functional data layer called the store. You can think of the store as of a state manager provided by Bryntum. You can load/save the data from/to the server directly. You can instantiate a new store globally, load data in it, and reuse in many components. If you're going to have another data layer (like component state), then you'll have to sync the data manually. In our examples we show how to load the data to the store manually. So the array of "local" data is set to the store. Technically, when you pass an array of objects, the grid store creates an array of records (see Model), it sets the Id, default fields, does some preparation. So after you load the data you can think of it as of records, not just simple objects. And then when the react state is updated, it sets a new array of data to the store. There is a config to treat the data as completely new dataset, or try to merge the data with the current dataset: https://www.bryntum.com/docs/grid/#Core/data/Store#config-syncDataOnLoad

const toggleAdmin = (data): void => {
    const users = [...userGridRef.current.gridInstance.store.data];
    const userIndex = users.findIndex(user => user.username === data.record.data.username);
    users[userIndex].isAdmin = !users[userIndex].isAdmin;
    users.splice(userIndex, 1, users[userIndex]);
    setCustomGridData(users);
};

Back to the code you provided, there is a problem with it. You tried to avoid mutation of the array by having a copy of the array elements at the first line. But this is an array of objects, and objects should be immutable too!

    const toggleAdmin = (data) => {
        const users = [...userGridRef.current.gridInstance.store.data];
        const userIndex = users.findIndex(user => user.name === data.record.name);
        // DO NOT MUTATE THE OBJECT
        // users[userIndex].isAdmin = !users[userIndex].isAdmin;
        const userObj = {
            ...users[userIndex],
            isAdmin : !users[userIndex].isAdmin
        };
        users.splice(userIndex, 1, userObj);
        setCustomGridData(users);
    };

As a side note, column renderer argument (renderData) is an object that contains many other cell/UI related info, which you don't need to pass to the toggle handler. I would recommend to pass only record. And whenever it is possible use getter instead of accessing fields via "data", so:

//record.data.isAdmin => record.isAdmin

This is how you can fix your solution:

    const columns = [
        {
            type   : 'tree',
            field  : 'name',
            text   : 'name',
            flex   : 1,
            editor : false
        },
        {
            field    : 'isAdmin',
            text     : 'Is Admin',
            flex     : 1,
            editor   : false,
            renderer : (renderData) => {
                return (
                    <>
                        {renderData.record.isAdmin ? <p>Yes</p> : <p>No</p>}
                        {<button onClick={() => toggleAdmin(renderData.record)}>Toggle Admin</button>}
                    </>
                )
            }
        }
    ];

const toggleAdmin = (record) => {
    const users = [...userGridRef.current.gridInstance.store.data];
    const userIndex = users.findIndex(user => user.name === record.name);
    // DO NOT MUTATE THE OBJECT
    // users[userIndex].isAdmin = !users[userIndex].isAdmin;
    const userObj = {
        ...users[userIndex],
        isAdmin : !users[userIndex].isAdmin
    };
    users.splice(userIndex, 1, userObj);
    setCustomGridData(users);
};

But this is not recommended approach. It's better to use setter on the record field to change data, and if you need, update the component state. Note, store.data holds "the raw original data", when store.records or allRecords hold an array of records to be used by the store. You can take a copy of the record data to pass to the component state.

    const toggleAdmin = (record) => {
        // This will update the data in the Grid Store
        record.isAdmin = !record.isAdmin;

    // This will sync the Grid Store with the Component State
    const users = userGridRef.current.gridInstance.store.records.map(r => ({ ...r.data }));
    setCustomGridData(users);
};

More over, if you edit a cell value, you might want to sync it with the component state too. For that you can add a listener to update the state:

    useEffect(() => {
        userGridRef.current.gridInstance.store.on({
            update() {
                // This will sync the Grid Store with the Component State
                const users = userGridRef.current.gridInstance.store.records.map(r => ({ ...r.data }));
                setCustomGridData(users);
            }
        })
    }, []);

And therefore simplify the toggle handler to:

const toggleAdmin = (record) => {
        // This will update the data in the Grid Store
        record.isAdmin = !record.isAdmin;
    };

As a side note, changing the record field will already update the grid view. But when you set the new array to the component state, since it's bound to the store data, the grid data will be reloaded. Make sure syncDataOnLoad is true (default).

Best regards,
Pavel

Pavlo Miklashevych
Sr. Frontend Developer


Post by pmiklashevich »

Also this code should work, but there is a bug in our code. Ticket here: https://github.com/bryntum/support/issues/2757

const toggleAdmin = (record) => {
    const users = userGridRef.current.gridInstance.store.records.map(r => ({ ...r.data }));
    const userIndex = users.findIndex(user => user.id === record.id);
    const userObj = {
        ...users[userIndex],
        name    : 'asd',
        isAdmin : !users[userIndex].isAdmin
    };
    users.splice(userIndex, 1, userObj);
    setCustomGridData(users);
};

Pavlo Miklashevych
Sr. Frontend Developer


Post by Yamo93 »

First of all, thanks for the extensive answer. Appreciate it 👌

I didn't know about the store methods. They seem pretty cool! Thanks.

Yeah, regarding the function that mutated the objects - it was just some quick test code from my side, lol.

I see. I hope that bug gets fixed in the near future.

Thanks again, this was helpful!


Post Reply