Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dealing with ag grid react and rendering a grid of checkboxes

I'm messing with ag-grid, react-apollo, and everything seems to be working fine. The goal here is to click a check box and have a mutation / network request occur modifying some data. The issue i'm having is that it redraws the entire row which can be really slow but im really just trying to update the cell itself so its quick and the user experience is better. One thought i had was to do a optimistic update and just update my cache / utilize my cache. What are some approach you guys have taken.

Both the columns and row data are grabbed via a apollo query.

Heres some code:

CheckboxRenderer

import React, { Component } from "react";
import Checkbox from "@material-ui/core/Checkbox";
import _ from "lodash";

class CheckboxItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: false
    };
    this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
  }

  componentDidMount() {
    this.setDefaultState();
  }

  setDefaultState() {
    const { data, colDef, api } = this.props;
    const { externalData } = api;
    if (externalData && externalData.length > 0) {
      if (_.find(data.roles, _.matchesProperty("name", colDef.headerName))) {
        this.setState({
          value: true
        });
      }
    }
  }

  updateGridAssociation(checked) {
    const { data, colDef } = this.props;
    // const { externalData, entitySpec, fieldSpec } = this.props.api;
    // console.log(data);
    // console.log(colDef);
    if (checked) {
      this.props.api.assign(data.id, colDef.id);
      return;
    }
    this.props.api.unassign(data.id, colDef.id);
    return;
  }

  handleCheckboxChange(event) {
    const checked = !this.state.value;
    this.updateGridAssociation(checked);
    this.setState({ value: checked });
  }

  render() {
    return (
      <Checkbox
        checked={this.state.value}
        onChange={this.handleCheckboxChange}
      />
    );
  }
}

export default CheckboxItem;

Grid itself:

import React, { Component } from "react";
import { graphql, compose } from "react-apollo";
import gql from "graphql-tag";
import Grid from "@material-ui/core/Grid";
import _ from "lodash";
import { AgGridReact } from "ag-grid-react";
import { CheckboxItem } from "../Grid";
import "ag-grid/dist/styles/ag-grid.css";
import "ag-grid/dist/styles/ag-theme-material.css";

class UserRole extends Component {
  constructor(props) {
    super(props);
    this.api = null;
  }

  generateColumns = roles => {
    const columns = [];
    const initialColumn = {
      headerName: "User Email",
      editable: false,
      field: "email"
    };
    columns.push(initialColumn);
    _.forEach(roles, role => {
      const roleColumn = {
        headerName: role.name,
        editable: false,
        cellRendererFramework: CheckboxItem,
        id: role.id,
        suppressMenu: true,
        suppressSorting: true
      };
      columns.push(roleColumn);
    });
    if (this.api.setColumnDefs && roles) {
      this.api.setColumnDefs(columns);
    }
    return columns;
  };

  onGridReady = params => {
    this.api = params.api;
    this.columnApi = params.columnApi;
    this.api.assign = (userId, roleId) => {
      this.props.assignRole({
        variables: { userId, roleId },
        refetchQueries: () => ["allUserRoles", "isAuthenticated"]
      });
    };

    this.api.unassign = (userId, roleId) => {
      this.props.unassignRole({
        variables: { userId, roleId },
        refetchQueries: () => ["allUserRoles", "isAuthenticated"]
      });
    };
    params.api.sizeColumnsToFit();
  };

  onGridSizeChanged = params => {
    const gridWidth = document.getElementById("grid-wrapper").offsetWidth;
    const columnsToShow = [];
    const columnsToHide = [];
    let totalColsWidth = 0;
    const allColumns = params.columnApi.getAllColumns();
    for (let i = 0; i < allColumns.length; i++) {
      const column = allColumns[i];
      totalColsWidth += column.getMinWidth();
      if (totalColsWidth > gridWidth) {
        columnsToHide.push(column.colId);
      } else {
        columnsToShow.push(column.colId);
      }
    }
    params.columnApi.setColumnsVisible(columnsToShow, true);
    params.columnApi.setColumnsVisible(columnsToHide, false);
    params.api.sizeColumnsToFit();
  };

  onCellValueChanged = params => {};

  render() {
    console.log(this.props);
    const { users, roles } = this.props.userRoles;
    if (this.api) {
      this.api.setColumnDefs(this.generateColumns(roles));
      this.api.sizeColumnsToFit();
      this.api.externalData = roles;
      this.api.setRowData(_.cloneDeep(users));
    }
    return (
      <Grid
        item
        xs={12}
        sm={12}
        className="ag-theme-material"
        style={{
          height: "80vh",
          width: "100vh"
        }}
      >
        <AgGridReact
          onGridReady={this.onGridReady}
          onGridSizeChanged={this.onGridSizeChanged}
          columnDefs={[]}
          enableSorting
          pagination
          paginationAutoPageSize
          enableFilter
          enableCellChangeFlash
          rowData={_.cloneDeep(users)}
          deltaRowDataMode={true}
          getRowNodeId={data => data.id}
          onCellValueChanged={this.onCellValueChanged}
        />
      </Grid>
    );
  }
}

const userRolesQuery = gql`
  query allUserRoles {
    users {
      id
      email
      roles {
        id
        name
      }
    }

    roles {
      id
      name
    }
  }
`;

const unassignRole = gql`
  mutation($userId: String!, $roleId: String!) {
    unassignUserRole(userId: $userId, roleId: $roleId) {
      id
      email
      roles {
        id
        name
      }
    }
  }
`;

const assignRole = gql`
  mutation($userId: String!, $roleId: String!) {
    assignUserRole(userId: $userId, roleId: $roleId) {
      id
      email
      roles {
        id
        name
      }
    }
  }
`;

export default compose(
  graphql(userRolesQuery, {
    name: "userRoles",
    options: { fetchPolicy: "cache-and-network" }
  }),
  graphql(unassignRole, {
    name: "unassignRole"
  }),
  graphql(assignRole, {
    name: "assignRole"
  })
)(UserRole);

enter image description here

like image 203
Karan Avatar asked Jul 24 '18 05:07

Karan


People also ask

How do I get a checkbox on Ag grid?

To configure the column to have a checkbox, set colDef. headerCheckboxSelection=true . headerCheckboxSelection can also be a function, if you want the checkbox to appear sometimes (e.g. if the column is ordered first in the grid).

How do you use a checkbox for a Boolean data with AG grid?

An input element is created in the ag-Grid init lifecycle method (required) and it's checked attribute is set to the underlying boolean value of the cell it will be rendered in. A click event listener is added to the checkbox which updates this underlying cell value whenever the input is checked/unchecked.

How do you Rerender AG grid in React?

Refresh Cells: api. refreshCells(cellRefreshParams) - Gets the grid to refresh all cells. Change detection will be used to refresh only cells whose display cell values are out of sync with the actual value. If using a cellRenderer with a refresh method, the refresh method will get called.

How do you make AG grid responsive in React JS?

The quickest way to achieve a responsive grid is to set the grid's containing div to a percentage. With this simple change the grid will automatically resize based on the div size and columns that can't fit in the viewport will simply be hidden and available to the right via the scrollbar.


2 Answers

I don't know ag-grid but ... in this case making requests results in entire grid (UserRole component) redraw.

This is normal when you pass actions (to childs) affecting entire parent state (new data arrived in props => redraw).

You can avoid this by shouldComponentUpdate() - f.e. redraw only if rows amount changes.

But there is another problem - you're making optimistic changes (change checkbox state) - what if mutation fails? You have to handle apollo error and force redraw of entire grid - change was local (cell). This can be done f.e. by setting flag (using setState) and additional condition in shouldComponentUpdate.

like image 151
xadm Avatar answered Sep 30 '22 15:09

xadm


The best way for me to deal with this was to do a shouldComponentUpdate with network statuses in apollo, which took some digging around to see what was happening:

 /**
   * Important to understand that we use network statuses given to us by apollo to take over, if either are 4 (refetch) we hack around it by not updating
   * IF the statuses are also equal it indicates some sort of refetching is trying to take place
   * @param  {obj} nextProps [Next props passed into react lifecycle]
   * @return {[boolean]}           [true if should update, else its false to not]
   */

  shouldComponentUpdate = nextProps => {
    const prevNetStatus = this.props.userRoles.networkStatus;
    const netStatus = nextProps.userRoles.networkStatus;
    const error = nextProps.userRoles.networkStatus === 8;
    if (error) {
      return true;
    }
    return (
      prevNetStatus !== netStatus && prevNetStatus !== 4 && netStatus !== 4
    );
  };

It basically says if there is a error, just rerender to be accurate (and i think this ok assuming that errors wont happen much but you never know) then I check to see if any of the network statuses are not 4 (refetch) if they are I dont want a rerender, let me do what I want without react interfering at that level. (Like updating a child component).

prevNetStatus !== netStatus

This part of the code is just saying I want the initial load only to cause a UI update. I believe it works from loading -> success as a network status and then if you refetch from success -> refetch -> success or something of that nature.

Essentially I just looked in my props for the query and saw what I could work with.

like image 32
Karan Avatar answered Sep 30 '22 14:09

Karan