Handling errors within Presentation components
Presentation rules - driven components get their data by making Presentation RPC requests, which may fail due to unexpected issues like network problems. Such failures, generally, result in an error being thrown in the frontend code, and, if not handled appropriately, may result in the whole application crash. For example, in such situation React applications render nothing and log something like this into browser's console:
As stated in the above error message, React suggests using error boundaries to handle such errors. A very simplistic error boundary component used in the below examples looks like this:
/**
* A sample React error boundary which handles errors thrown by child components by merely
* rendering the error message. Check out React's Error Boundary documentation for how to
* implement a more elaborate solution.
*/
export class ErrorBoundary extends Component<{ children: React.ReactNode }, { error?: Error }> {
public constructor(props: { children: React.ReactNode }) {
super(props);
this.state = {};
}
public static getDerivedStateFromError(error: Error) {
// just save the error in the internal component's state
return { error };
}
public override render() {
// in case we got an error - render the error message
if (this.state.error) {
return this.state.error?.message ?? "Error";
}
// otherwise - render provided child component
return this.props.children;
}
}
Handling errors in property grid
Capturing property grid errors is as simple as wrapping rendering of the component with an error boundary:
function MyPropertyGrid(props: { imodel: IModelConnection; elementKey: InstanceKey }) {
// create a presentation rules driven data provider; the provider implements `IDisposable`, so we
// create it through `useOptionalDisposable` hook to make sure it's properly cleaned up
const dataProvider = useOptionalDisposable(
useCallback(() => {
const provider = new PresentationPropertyDataProvider({ imodel: props.imodel });
provider.keys = new KeySet([props.elementKey]);
return provider;
}, [props.imodel, props.elementKey]),
);
// width and height should generally we computed using ResizeObserver API or one of its derivatives
const [width] = useState(400);
const [height] = useState(600);
if (!dataProvider) {
return null;
}
// render the property grid within an error boundary - any errors thrown by the property grid will be captured
// and handled by the error boundary
return (
<ErrorBoundary>
<VirtualizedPropertyGridWithDataProvider dataProvider={dataProvider} width={width} height={height} />
</ErrorBoundary>
);
}
Result when there's no error:
Result when there's an error getting data for the property grid:
Handling errors in table
For the Table component, all requests are made by the usePresentationTable hook (or usePresentationTableWithUnifiedSelection when using it with Unified Selection). That means the hook needs to be used within the error boundary for it's errors to be captured. For that we use 2 components: one is responsible for rendering the table, the other - for wrapping it with an error boundary.
/** Props for `MyTable` and `MyProtectedTable` components */
interface MyTableProps {
imodel: IModelConnection;
keys: KeySet;
}
/** The actual table component that may throw an error */
function MyProtectedTable(props: MyTableProps) {
// the `usePresentationTable` hook requests table data from the backend and maps it to something we
// can render, it may also throw in certain situations
const { columns, rows, isLoading } = usePresentationTable({
imodel: props.imodel,
ruleset,
pageSize: 10,
columnMapper: mapTableColumns,
rowMapper: mapTableRow,
keys: props.keys,
});
// either loading or nothing to render
if (isLoading || !columns || !columns.length) {
return null;
}
// render a simple HTML table
return (
<table>
<thead>
<tr>
{columns.map((col, i) => (
<td key={i}>{col.label}</td>
))}
</tr>
</thead>
<tbody>
{rows.map((row, ri) => (
<tr key={ri}>
{columns.map((col, ci) => (
<td key={ci}>
<Cell record={row[col.id]} />
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
/** A table component that renders the table within an error boundary */
function MyTable(props: MyTableProps) {
// any errors thrown by `MyProtectedTable` will be captured and handled by the error boundary
return (
<ErrorBoundary>
<MyProtectedTable {...props} />
</ErrorBoundary>
);
}
/** Cell renderer that uses `PropertyValueRendererManager` to render property values */
function Cell(props: { record: PropertyRecord | undefined }) {
return <>{props.record ? PropertyValueRendererManager.defaultManager.render(props.record) : null}</>;
}
/** A function that maps presentation type of column definition to something that table renderer knows how to render */
const mapTableColumns = (columnDefinitions: TableColumnDefinition) => ({
id: columnDefinitions.name,
label: columnDefinitions.label,
});
/** A function that maps presentation type of row definition to something that table renderer knows how to render */
function mapTableRow(rowDefinition: TableRowDefinition) {
const rowValues: { [cellKey: string]: PropertyRecord } = {};
rowDefinition.cells.forEach((cell) => {
rowValues[cell.key] = cell.record;
});
return rowValues;
}
Result when there's no error:
Result when there's an error getting data for the table:
Handling errors in tree
Tree component is slightly different from the above components, because it runs different queries to get data for different hierarchy levels. It's possible that we successfully get response for some of the requests, but fail for only one of them. In such situations, we want to show an error only where it happened, while still showing information that we successfully received.
At the moment this complexity is handled by PresentationTreeRenderer, which renders an "error node" for the whole hierarchy level upon an error, so there's no need to use an error boundary:
function MyTree(props: { imodel: IModelConnection }) {
const state = usePresentationTreeState({
imodel: props.imodel,
ruleset,
pagingSize: 100,
});
// width and height should generally we computed using ResizeObserver API or one of its derivatives
const [width] = useState(400);
const [height] = useState(600);
if (!state) {
return null;
}
// presentation-specific tree renderer takes care of handling errors when requesting nodes
const treeRenderer = (treeRendererProps: TreeRendererProps) => <PresentationTreeRenderer {...treeRendererProps} nodeLoader={state.nodeLoader} />;
return <PresentationTree width={width} height={height} state={state} selectionMode={SelectionMode.Extended} treeRenderer={treeRenderer} />;
}
Result when there's no error:
Result when an error occurs requesting child nodes:
Result when an error occurs requesting root nodes:
Last Updated: 13 May, 2024