Building CRUD in ag-Grid with GraphQL & React

  |   Tutorial

This post illustrates how to perform CRUD operations using ag-Grid server-side row model and JSON-server, GraphQL, Apollo-client and React/TypeScript. We've built an example to demonstrate how to implement the necessary operations to have a fully-functional ag-Grid in this scenario. I hope this helps you effectively use ag-Grid with GraphQL and React!

Live sample

You can download the project from GitHub and easily reuse its code to implement this in your project. Please see a quick demo of the sample below showing the CRUD operations:

Architecture

Let's go over the components/frameworks and their roles in this:

  • JSON-server: Provides a full fake REST API that we'll use to create, read, update and delete records
  • GraphQL: Allows requesting the exact data we need from our server
  • Apollo-client: Allows building queries directly from the front-end
  • React/TypeScript: Used to bootstrap a single-page React application using create-react-app

Please see this illustrated in the diagram below:

Contents

Installation

Follow the steps below to set up the sample project:

  1. Clone or download the sample project from this Github repository.
  2. Install the dependencies:
$ npm install
$ npm run client-dependencies

3. Run the project

$ npm run dev
💡 Note:
  • JSON server runs on port 3000 (default)
  • GraphQL server runs on port 5000 (see /server.js)
  • Client runs on port 3006 (see /client/package.json)

Backend

JSON-server

JSON server will serve as the backend of our sample. The script that generates our JSON server is shown below:

"json:server": "json-server --watch data.json"

and here is the file being watched:

{
	"olympicWinners": [
    	{
    		"athlete": "Michael Phelps",
        	"age": 23,
        	"country": "United States",
        	"year": 2008,
	        "date": "24/08/2008",
    	    "sport": "Swimming",
       		"gold": 8,
	        "silver": 0,
    	    "bronze": 0,
       		"total": 8,
	        "id": "0"
        },

We are now able to make HTTP requests (GET / POST/  PUT / PATCH / DELETE) to interact with this data as if it were a real backend.

GraphQL

When requesting rows, ag-Grid executes the getRows(params) callback provided in your application's server-side datasource.  Rows fetched from the server are then provided back to the grid using the params.successCallback(rows, lastIndex) method with the parameters below:
(1) the records to be displayed in the grid
(2) the total number of rows in your dataset (optional - for configuring the grid's vertical scrollbar).

const ResponseType = new GraphQLObjectType({
	name: 'Response',
    fields: () => ({
    	lastRow: { type: GraphQLInt },
        rows: { type: new GraphQLList(OlympicWinnerType) }
    })
})

The params passed to getRows method contains all the information required by the server when fetching the data. Metadata is included for sorting, filtering, grouping and pivoting data.

In this post, we'll only focus on implementing sorting in our GraphQL service using the code below:

const RootQuery = new GraphQLObjectType({
	name: 'RootQueryType',
    fields: {
    	getRows: {
        	type: ResponseType,
            args: {
            	startRow: { type: GraphQLNonNull(GraphQLInt) },
                endRow: { type: GraphQLNonNull(GraphQLInt) },
                sortModel: { type: new GraphQLList(SortModelType) }
                // filterModel: {}
                // groupKeys: []
                // pivotCols: []
                // pivotMode: false
                // rowGroupCols: []
                // valueCols: []
            },
           	resolve(parentValue, args) {
                let endPoint = JSON_SERVER_ENDPOINT;
                const isRequestSorting = args.sortModel && args.sortModel.length > 0;

                if (isRequestSorting) {
                    const fields = [];
                    const orders = [];
                    args.sortModel.forEach(sM => {
                        fields.push(sM.colId);
                        orders.push(sM.sort)
                    });
                    // sorting
                    endPoint += `?_sort=${fields.join(',')}&_order=${orders.join(',')}`;
                    // starting from start row with a limit of endRow - startRows rows
                    endPoint += `&_start=${args.startRow}&_limit=${args.endRow - args.startRow}`;
                } else {
                    endPoint += `?_start=${args.startRow}&_end=${args.endRow}`;
                }

                return axios.get(endPoint)
                    .then(res => {
                        return {
                            rows: res.data,
                            lastRow: res.headers["x-total-count"]
                        }
                    })
                    .catch(err => console.log(err));
            }
        },
		// ...

Frontend

Apollo-client

To communicate with the GraphQL server we've built, we're using Apollo-client, instantiated like this:

const client = new ApolloClient({
	uri: 'http://localhost:5000/graphql',
    cache: new InMemoryCache()
});

We can now easily write GraphQL queries on our frontend (more specially, in our server-side datasource) in the GraphQL Schema Definition Language.

ag-Grid Datasource

Now that all the groundwork has been laid, configuring the ag-Grid server-side datasource is simple. You use the ag-Grid server-side row model when you want to load the data from the data source in chunks as it's needed instead of all in one go. You can see a comparison between the different row models in our documentation.

In this sample we're using the ag-Grid with the server-side row model to load data in chunks, which is useful for very large datasets. Whenever ag-Grid requests rows, we make queries to GraphQL using our Apollo-Client instance. We pass the parameters startRow and endRow to our query in order to retrieve the desired rows, and the parameter sortModel to apply whatever sorting is currently being applied on our ag-Grid instance.

const createServerSideDatasource = function (): IServerSideDatasourceWithCRUD {
	return {
    	getRows: function (params: IServerSideGetRowsParams) {
        	let {
            	startRow,
                endRow,
                sortModel,
                // filterModel,
                // groupKeys,
                // pivotCols,
                // pivotMode,
                // rowGroupCols,
                // valueCols
          	}: IServerSideGetRowsRequest = params.request;

			sortModel = sortModel.length > 0 ? sortModel : undefined;
        	const visibleColumnIds: string[] = params
            	.columnApi.getAllDisplayedColumns().map(col => col.getColId());

        	client.query({
            	query: gql`
                	query GetRows($startRow: Int!, $endRow: Int!, $sortModel: [SortModel]) {
                    	getRows(startRow: $startRow, endRow: $endRow, sortModel: $sortModel) {
                        	lastRow
                        	rows { 
                            	id, 
	                            ${visibleColumnIds.join('\n')}
    	                    }
                    	}
                	}
	            `,
            	variables: {
                	startRow,
	                endRow,
    	            sortModel
        	    }
	        })
    	        .then(res => res.data.getRows)
        	    .then(({ lastRow, rows }: { lastRow: number, rows: IOlympicWinner[] }) => {
            	    params.successCallback(rows, lastRow);
            	})
	            .catch(err => {
        	        params.failCallback();
            	});
    	},
		// ...

Note how we are leveraging GraphQL's ability to only request the exact data we need by listing the fields of each record that are visible in ag-Grid (in addition to the ID of each record).

getRows(startRow: $startRow, endRow: $endRow, sortModel: $sortModel) {
	lastRow
	rows { 
		id, 
		${visibleColumnIds.join('\n')}
	}
}

This works well with our application as we have a columns tool panel accessory that allows us to select/deselect what data fields we would like to show on our grid. This delivers faster query execution and a smaller memory footprint on the client as ag-Grid only requests the data it needs to show. See how we're selecting/de-selecting columns using the columns tool panel in the image below:

Finally, we'll register our datasource in our App component:

const App: FunctionComponent = (): React.ReactElement => {
	const datasource: IServerSideDatasourceWithCRUD = createServerSideDatasource();

  	const onGridReady = (params: GridReadyEvent) => {
    	params.api.setServerSideDatasource(datasource);
    }
	// ...

So far we've only used the GET HTTP protocol in our application. We'll know focus on adding CRUD operations so that we can interact with our back-end more!

Implementing CRUD Operations

Now that we've set up the frontend and backend components of our sample, let's look at how to implement support for adding/updating/deleting records via ag-Grid.

Creating Records

When creating a record, we open a form for the user to fill in as shown below:

When the form is submitted, we execute the function below to add this row:

const addRow: IFormSubmitHandler = (data: IOlympicWinner) => {
	datasource
    	.createRow(data)
        .then(() => {
        	gridApi.purgeServerSideCache();
		})
};

Our server-side datasource then sends the following query:

createRow(data: IOlympicWinner): Promise {
	return client.mutate({
    	mutation: gql`
        	mutation CreateRow($data: OlympicWinnerInput!) {
            	createRow(data: $data) {                                 
            		id                                 
                	athlete                                 
                	age                                 
                	country                                 
                	year                                 
                	date                                 
                	sport                                 
                	gold                                 
                	silver                                 
                	bronze                                 
	                total                         
            	}                     
        }`,
        variables: {
        	data
        }
    })
    	.then(res => {
        	return res.data.createRow;
        })
        .catch(err => console.log('err', err));
}

GraphQL responds to the mutation and sends a POST request to our JSON-server.

createRow: {
	type: OlympicWinnerType,
    args: {
    	data: { 
        	type: GraphQLNonNull(OlympicWinnerInputType) 		
        },
    },
    resolve(parentValue, args) {
    	return axios.post(JSON_SERVER_ENDPOINT, args.data)
        	.then(res => res.data)
            .catch(err => console.log(err));
    }
}

Note: Newly-created records are added to the bottom of the dataset.

Reading and Updating Records

Since our application only requests the fields of data that are currently shown in the grid, we first request to read all of a selected record (athlete's) data before opening up a form for the user to update said record (athlete).

const updateSelectedRowHandler: React.MouseEventHandler = () => {
	const selectedNode = getSelectedNode();
    if (selectedNode) {
    	const selectedRowId: string = selectedNode.id;
        // first query all of the rows data before passing it to the form
        datasource
        	.readRow(selectedRowId)
            .then((selectedRow: IOlympicWinner) => {
            	openForm(selectedRow, updateRow);
            });
    }
};

After submitting the form, a similar process to creating a row is performed - we first execute a method on our server-side data source, then send a query to GraphQL.

Here's the code executed to refresh the server-side data source:

const updateRow: IFormSubmitHandler = (data: IOlympicWinner) => {
	datasource
    	.updateRow(data)
        .then(() => {
        	gridApi.purgeServerSideCache();
        })
};

This is then followed by sending a query to GraphQL:

updateRow(data: IOlympicWinner): Promise<any> {
	return client.mutate({
		mutation: gql`
			mutation UpdateRow($data: OlympicWinnerInput!) {
				updateRow(data: $data) {  
					id
                    athlete
                    age
					country
					year
					date
					sport
					gold
					silver
					bronze
					total
                }
            }
        `,
        variables: {
			data
		}
	})
		.then(res => {
			return res.data.updateRow;
		})
		.catch(err => console.log('err', err));
}

GraphQL responds to this query and updates the record on our JSON-server via a PATCH request.

updateRow: {
    type: OlympicWinnerType,
    args: {
        data: { type: GraphQLNonNull(OlympicWinnerInputType) },
    },
    resolve(parentValue, args) {
        return axios.patch(`${JSON_SERVER_ENDPOINT}/${args.data.id}`, args.data)
            .then(res => res.data)
            .catch(err => console.log(err));
    }
}

Deleting Records

Let's now add the ability to delete records - please see this illustrated below:

On selecting a row and clicking the 'Delete Selected Row' button, the following handler is executed:

const deleteSelectedRowHandler: React.MouseEventHandler = () => {
	const selectedNode = getSelectedNode();
    if (selectedNode && window.confirm('Are you sure you want to delete this node?')) {
    	const selectedRowId: string = selectedNode.id;
        datasource
        	.deleteRow(selectedRowId)
            .then(() => {
            	gridApi.purgeServerSideCache();
            });
    }
}

Again, our datasource uses Apollo-client to write up the mutation and send it to GraphQL.

deleteRow(id: string): Promise {
	return client.mutate({
		mutation: gql`
			mutation DeleteRow($id: ID!) {                         		
				deleteRow(id: $id) {                                   	
                	id                             
					athlete                             
                    age                             
                    country                             
                    year                             
                    date                             
                    sport                             
                    gold                             
                    silver                             
                    bronze                             
                    total                         
                }                     
            }`,
		variables: {
			id
		}
    })
    	.then(res => {
        	return res.data.deleteRow;
        })
        .catch(err => console.log('err', err));
}

GraphQL picks this up and sends a DELETE request to our JSON-server.

deleteRow: {
	type: OlympicWinnerType,
	args: {
		id: { type: GraphQLNonNull(GraphQLID) }
	},
	resolve(parentValue, args) {
		return axios.delete(`${JSON_SERVER_ENDPOINT}/${args.id}`)
			.then(res => res.data)
			.catch(err => console.log(err))
	}
}

What's next?

We hope that you find this article useful if you're looking to use GraphQL with ag-Grid. Remember you can download the project from GitHub and easily reuse the code I've described above to implement this in your project.

If you would like to try out ag-Grid check out our getting started guides (JS / React / Angular / Vue)

Happy coding!

Read more posts about...