---
product: "AG Grid"
title: "Calculated Columns - Restrictions & Validation"
description: "Control what users can change in React Data Grid calculated columns: read-only columns, hidden menu items, restricted column references and custom validation of the Calculated Column dialog."
enterprise: true
framework: react
version: "36.3.0"
related:
    - title: "Dialog & Customisation"
      url: "https://www.ag-grid.com/react-data-grid/calculated-columns-dialog/"
    - title: "API, Events & State"
      url: "https://www.ag-grid.com/react-data-grid/calculated-columns-api/"
llms: "https://www.ag-grid.com/llms.txt"
---

# Calculated Columns - Restrictions & Validation

Control what users can change: make [calculated columns](https://www.ag-grid.com/react-data-grid/calculated-columns/) read-only, hide their menu items, restrict which columns they can reference and validate what users enter.

## Read-Only Calculated Columns

Set `calculatedColumnReadOnly: true` to prevent users from changing a column's title, type or expression in the dialog. Users can choose **View Calculated Column** to inspect its definition. **Remove Calculated Column** is disabled in the column and cell context menus.

```jsx
const calculatedColumns = true;
const [columnDefs, setColumnDefs] = useState([
    { field: 'revenue' },
    { field: 'cost' },
    {
        colId: 'grossMargin',
        calculatedExpression: '([revenue] - [cost]) / [revenue]',
        cellDataType: 'number',
        calculatedColumnReadOnly: true,
    },
]);

<AgGridReact
    calculatedColumns={calculatedColumns}
    columnDefs={columnDefs}
/>
```

The example keeps Gross Margin read-only, while Profit and user-created calculated columns remain editable.

#### Read-Only Calculated Columns

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  CalculatedColumnsGridOption,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridOptions,
  ModuleRegistry,
  enableDevValidations,
} from "ag-grid-community";
import {
  CalculatedColumnsModule,
  ColumnMenuModule,
  ContextMenuModule,
} from "ag-grid-enterprise";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  ClientSideRowModelModule,
  CalculatedColumnsModule,
  ColumnMenuModule,
  ContextMenuModule,
];

interface SalesRow {
  product: string;
  revenue: number;
  cost: number;
}

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [rowData, setRowData] = useState<SalesRow[]>([
    { product: "Solar panel kit", revenue: 142000, cost: 96000 },
    { product: "Smart thermostat", revenue: 78000, cost: 52000 },
    { product: "Battery pack", revenue: 126000, cost: 101000 },
    { product: "EV charger", revenue: 92000, cost: 61000 },
    { product: "Heat pump", revenue: 168000, cost: 119000 },
  ]);
  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "product" },
    { field: "revenue" },
    { field: "cost" },
    {
      colId: "grossMargin",
      headerName: "Gross Margin",
      calculatedExpression: "([revenue] - [cost]) / [revenue]",
      cellDataType: "number",
      calculatedColumnReadOnly: true,
      valueFormatter: ({ value }) =>
        typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "",
    },
    {
      colId: "profit",
      headerName: "Profit",
      calculatedExpression: "[revenue] - [cost]",
      cellDataType: "number",
    },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return { flex: 1, minWidth: 130 };
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<SalesRow>
            rowData={rowData}
            columnDefs={columnDefs}
            calculatedColumns={true}
            defaultColDef={defaultColDef}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Read-Only Calculated Columns](https://www.ag-grid.com/examples/calculated-columns-validation/calculated-columns-read-only/reactFunctionalTs/)

If a column becomes read-only while its edit dialog is open, the dialog closes and any pending edits are discarded. Changes already applied are kept.

> **Note**
>
> This setting restricts UI changes only: updating `columnDefs` and restoring [Grid State](https://www.ag-grid.com/react-data-grid/grid-state/) can still change or remove the column. Dialog-created columns ignore `defaultColDef.calculatedColumnReadOnly`, but [processColDef](https://www.ag-grid.com/react-data-grid/calculated-columns-dialog/#processcoldef) can make them read-only.

## Hiding Menu Items

The calculated column actions are built-in menu items. To stop users adding, editing or removing calculated columns from the menus, filter them out of the default items with [getColumnMenuItems](https://www.ag-grid.com/react-data-grid/column-menu/#customising-the-menu-items) and [getContextMenuItems](https://www.ag-grid.com/react-data-grid/context-menu/#configuring-the-context-menu):

- `calculatedColumn`: **Add Calculated Column**, in the column menu.
- `editCalculatedColumn`: **Edit Calculated Column**, or **View Calculated Column** when the column is read-only, in the column menu.
- `removeCalculatedColumn`: **Remove Calculated Column**, in the column menu and the cell context menu.

```jsx
const calculatedColumns = true;
const getColumnMenuItems = ({ defaultItems }) =>
    defaultItems.filter(
        item => item !== 'editCalculatedColumn' && item !== 'removeCalculatedColumn'
    );
const getContextMenuItems = ({ defaultItems }) =>
    defaultItems?.filter(item => item !== 'removeCalculatedColumn');

<AgGridReact
    calculatedColumns={calculatedColumns}
    getColumnMenuItems={getColumnMenuItems}
    getContextMenuItems={getContextMenuItems}
/>
```

As with [read-only columns](https://www.ag-grid.com/react-data-grid/calculated-columns-validation/#read-only-calculated-columns), this only removes the menu entries. Updating `columnDefs` and restoring Grid State can still change or remove the column.

## Restricting Column References

Return `false` from `isColumnReferenceable` to exclude a column from the Columns picker, inline autocomplete and newly entered references. Use `calculatedColumn` to identify the column being edited, or `sourceColumn` for the column whose menu initiated **Add Calculated Column**. `sourceColumn` is only provided when adding a column; it is `null` when editing an existing column.

This example allows Salary references only when editing Bonus:

```jsx
const calculatedColumns = {
    isColumnReferenceable: ({ colDef, calculatedColumn }) => {
        if (colDef.field === 'salary') {
            return calculatedColumn?.getColId() === 'bonus';
        }
        return true;
    },
};

<AgGridReact calculatedColumns={calculatedColumns} />
```

Properties available on the `IsColumnReferenceableParams&lt;TData = any, TContext = any&gt;` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `column` | [`Column`](https://www.ag-grid.com/react-data-grid/column-object/) |  |  | The candidate source column. |
| `colDef` | [`ColDef`](https://www.ag-grid.com/react-data-grid/column-properties/) |  |  | The candidate source column's definition. |
| `calculatedColumn` | [`Column \| null`](https://www.ag-grid.com/react-data-grid/column-object/) |  |  | The column being edited, including a newly created column in live mode. `null` when adding in deferred mode. |
| `sourceColumn` | [`Column \| null`](https://www.ag-grid.com/react-data-grid/column-object/) |  |  | The column whose menu initiated Add Calculated Column. Only provided while adding a column, in either apply mode. `null` when editing an existing column or when there is no source column. |
| `api` | [`GridApi`](https://www.ag-grid.com/react-data-grid/grid-api/) |  |  | The grid api. |
| `context` | [`TContext`](https://www.ag-grid.com/react-data-grid/typescript-generics/#context-tcontext) |  |  | Application context as set on `gridOptions.context`. |

- The callback runs when the dialog opens.
- Existing permitted expressions can remain unchanged, and programmatic expressions are unrestricted unless explicitly revalidated with [`refreshCalculatedColumns()`](https://www.ag-grid.com/react-data-grid/calculated-columns-validation/#refreshing-calculated-columns).
- Restricted edits disable Apply in deferred mode. In live mode, they are saved but display `#REF!` until corrected; reopening the dialog or restoring Grid State preserves the error.

This is a UI restriction, not a security boundary.

#### Restricting Column References

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  CalculatedColumnsGridOption,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridOptions,
  ModuleRegistry,
  enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  ClientSideRowModelModule,
  CalculatedColumnsModule,
  ColumnMenuModule,
];

interface Employee {
  employee: string;
  salary: number;
  sales: number;
}

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [rowData, setRowData] = useState<Employee[]>([
    { employee: "Alex Morgan", salary: 50000, sales: 100000 },
    { employee: "Sam Taylor", salary: 60000, sales: 125000 },
    { employee: "Jamie Lee", salary: 55000, sales: 110000 },
  ]);
  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employee" },
    { field: "salary" },
    { field: "sales" },
    {
      colId: "bonus",
      headerName: "Bonus",
      calculatedExpression: "[salary] * 0.1",
      cellDataType: "number",
    },
    {
      colId: "target",
      headerName: "Sales Target",
      calculatedExpression: "[sales] * 1.2",
      cellDataType: "number",
    },
  ]);
  const calculatedColumns = useMemo<CalculatedColumnsGridOption>(() => {
    return {
      isColumnReferenceable: ({ colDef, calculatedColumn }) => {
        if (colDef.field === "salary") {
          return calculatedColumn?.getColId() === "bonus";
        }
        return true;
      },
    };
  }, []);
  const defaultColDef = useMemo<ColDef>(() => {
    return { flex: 1, minWidth: 120 };
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<Employee>
            rowData={rowData}
            columnDefs={columnDefs}
            calculatedColumns={calculatedColumns}
            defaultColDef={defaultColDef}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Restricting Column References](https://www.ag-grid.com/examples/calculated-columns-validation/calculated-columns-reference-restrictions/reactFunctionalTs/)

In the example, edit Bonus and Sales Target. Only Bonus can reference Salary.

## Validating Dialog Fields

Use `getValidationErrors` to validate the dialog's title, type and expression. Return messages keyed by `headerName`, `cellDataType` or `calculatedExpression`, or `null` to accept the draft:

- Omitted fields and empty arrays are valid.
- Return or spread `internalErrors` to retain built-in feedback.
- The callback runs while typing, so keep it quick.
- Return translated messages; the grid shows them as you supply them.

```jsx
const calculatedColumns = {
    applyMode: 'deferred',
    getValidationErrors: ({ colDef, internalErrors }) => ({
        ...internalErrors,
        headerName: (colDef.headerName?.length ?? 0) > 30
            ? ['Use a title of at most 30 characters.']
            : internalErrors?.headerName,
    }),
};

<AgGridReact calculatedColumns={calculatedColumns} />
```

In the example, edit Sales Bonus and enter a title longer than 30 characters or an expression using `[Salary]` to see field-specific errors.

#### Validating Dialog Fields

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  CalculatedColumnsGridOption,
  ClientSideRowModelModule,
  ColDef,
  ColGroupDef,
  GridOptions,
  ModuleRegistry,
  enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  ClientSideRowModelModule,
  CalculatedColumnsModule,
  ColumnMenuModule,
];

interface Employee {
  employee: string;
  salary: number;
  sales: number;
}

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [rowData, setRowData] = useState<Employee[]>([
    { employee: "Alex Morgan", salary: 50000, sales: 100000 },
    { employee: "Sam Taylor", salary: 60000, sales: 125000 },
    { employee: "Jamie Lee", salary: 55000, sales: 110000 },
  ]);
  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employee" },
    { field: "salary" },
    { field: "sales" },
    {
      colId: "bonus",
      headerName: "Sales Bonus",
      calculatedExpression: "[sales] * 0.1",
      cellDataType: "number",
    },
  ]);
  const calculatedColumns = useMemo<CalculatedColumnsGridOption>(() => {
    return {
      applyMode: "deferred",
      getValidationErrors: ({ colDef, referencedColumns, internalErrors }) => ({
        ...internalErrors,
        headerName:
          (colDef.headerName?.length ?? 0) > 30
            ? ["Use a title of at most 30 characters."]
            : internalErrors?.headerName,
        calculatedExpression: referencedColumns.some(
          (column) => column.getColId() === "salary",
        )
          ? [
              "Salary cannot be used in this report.",
              "Choose a sales-based expression.",
            ]
          : internalErrors?.calculatedExpression,
      }),
    };
  }, []);
  const defaultColDef = useMemo<ColDef>(() => {
    return { flex: 1, minWidth: 120 };
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<Employee>
            rowData={rowData}
            columnDefs={columnDefs}
            calculatedColumns={calculatedColumns}
            defaultColDef={defaultColDef}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Validating Dialog Fields](https://www.ag-grid.com/examples/calculated-columns-validation/calculated-columns-custom-validation/reactFunctionalTs/)

Properties available on the `CalculatedColumnValidationParams&lt;TData = any, TContext = any&gt;` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `colDef` | `CalculatedColumnDef` |  |  | A copy of the draft definition, with recognised references converted to stored `[colId]` references. |
| `displayExpression` | `string` |  |  | The expression displayed in the dialog, using column header references. |
| `referencedColumns` | `readonly Column[]` |  |  | Unique columns directly referenced by the expression, including hidden columns. For invalid syntax, includes recognised complete bracket references. |
| `internalErrors` | `CalculatedColumnValidationErrors \| null` |  |  | Built-in validation errors, or `null`. Include these in the callback result to retain built-in validation. |
| `column` | [`Column \| null`](https://www.ag-grid.com/react-data-grid/column-object/) |  |  | The calculated column, or `null` when adding a column in deferred mode. |
| `sourceColumn` | [`Column \| null`](https://www.ag-grid.com/react-data-grid/column-object/) |  |  | The column whose menu initiated Add Calculated Column. Only provided while adding a column, in either apply mode. `null` when editing an existing column or when there is no source column. |
| `api` | [`GridApi`](https://www.ag-grid.com/react-data-grid/grid-api/) |  |  | The grid api. |
| `context` | [`TContext`](https://www.ag-grid.com/react-data-grid/typescript-generics/#context-tcontext) |  |  | Application context as set on `gridOptions.context`. |

Editing the title or type revalidates the expression too, even when the expression is unchanged. Opening an editable live dialog rechecks expression rejections, even without edits.

> **Note**
>
> This is a dialog guard, not a security boundary: `columnDefs` and Grid State restores do not run the callback. Rejections restored from Grid State remain blocked.

### How Rejected Edits Behave

Any field error blocks Apply in deferred mode. In live mode, the outcome depends on the field:

| Rejected edit | Deferred mode | Live mode |
| --- | --- | --- |
| Title or type rejected by `getValidationErrors` | Blocks Apply | Keeps its last applied value, while other fields apply |
| Expression rejected by `getValidationErrors` | Blocks Apply | Saved, and displays `#ERROR!`, including when the callback returns built-in expression errors |
| Expression referencing a column restricted by `isColumnReferenceable` | Disables Apply | Saved, and displays `#REF!` until corrected. Reference restrictions remain enforced, and use `#REF!` instead of `#ERROR!`, even when `getValidationErrors` is set |

## Refreshing Calculated Columns

Call `refreshCalculatedColumns()` after application validation rules change to revalidate expressions, update saved errors and recalculate cells and dependants without opening the dialog.

```js
api.refreshCalculatedColumns({ columns: ['profit'] });
// Omit columns to refresh all calculated columns.
api.refreshCalculatedColumns();
```

The refresh explicitly applies the current `getValidationErrors` and `isColumnReferenceable` rules:

- It includes read-only and programmatically defined columns.
- Applied expressions are checked with `sourceColumn: null`.
- Removed rules clear their saved rejections.
- Open dialogs also revalidate their drafts, without applying them or restarting the debounce.
- Validation changes emit `calculatedColumnChanged` with `source: 'api'`.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `refreshCalculatedColumns` | `Function` |  |  | Revalidate committed calculated expressions against the current `getValidationErrors` and `isColumnReferenceable` callbacks, update saved errors and recalculate values and dependants. Applies to read-only columns too. Does not apply pending dialog edits. Empty expressions are allowed. Open dialogs revalidate their drafts without restarting pending live updates. Validation changes raise `calculatedColumnChanged` with `source: 'api'`. [Module](https://www.ag-grid.com/react-data-grid/modules/): `CalculatedColumnsModule`. |
