From 0e697e75f59da1dc2a6e1e169e3e7ecd8f795698 Mon Sep 17 00:00:00 2001 From: Lukas Boll Date: Thu, 15 Aug 2024 17:23:29 +0200 Subject: [PATCH] fix(translations): memoize array translation This commit addresses an issue where array translations were created as new objects within the core module for each render cycle, causing unnecessary rerenders. By memoizing the translation object in the material renderer set, this commit optimizes performance and prevents redundant rerenders. closes #2342 --- .../library/layouts/array-layout.renderer.ts | 25 +++++++++++--- .../src/library/other/master-detail/master.ts | 21 ++++++++++-- .../src/library/other/table.renderer.ts | 6 ++-- packages/angular/src/library/array-control.ts | 18 ++++++++-- packages/core/src/mappers/renderer.ts | 33 ++----------------- .../MaterialListWithDetailRenderer.tsx | 12 +++++-- .../complex/MaterialArrayControlRenderer.tsx | 31 ++++++++++++----- .../src/complex/MaterialTableControl.tsx | 3 +- .../src/layouts/MaterialArrayLayout.tsx | 7 ++-- .../layouts/MaterialArrayLayoutRenderer.tsx | 22 ++++++++++--- packages/react/src/JsonFormsContext.tsx | 20 +++++++++++ .../src/complex/TableArrayControl.tsx | 15 +++++++-- .../complex/array/ArrayControlRenderer.tsx | 13 ++++++-- 13 files changed, 157 insertions(+), 69 deletions(-) diff --git a/packages/angular-material/src/library/layouts/array-layout.renderer.ts b/packages/angular-material/src/library/layouts/array-layout.renderer.ts index 9cf7b6c9af..9f8850600f 100644 --- a/packages/angular-material/src/library/layouts/array-layout.renderer.ts +++ b/packages/angular-material/src/library/layouts/array-layout.renderer.ts @@ -33,10 +33,13 @@ import { JsonFormsAbstractControl, } from '@jsonforms/angular'; import { + arrayDefaultTranslations, ArrayLayoutProps, ArrayTranslations, createDefaultValue, + defaultJsonFormsI18nState, findUISchema, + getArrayTranslations, isObjectArrayWithNesting, JsonFormsState, mapDispatchToArrayControlProps, @@ -169,7 +172,7 @@ export class ArrayLayoutRenderer implements OnInit, OnDestroy { noData: boolean; - translations: ArrayTranslations; + translations: ArrayTranslations = {}; addItem: (path: string, value: any) => () => void; moveItemUp: (path: string, index: number) => () => void; moveItemDown: (path: string, index: number) => () => void; @@ -181,9 +184,19 @@ export class ArrayLayoutRenderer constructor(jsonFormsService: JsonFormsAngularService) { super(jsonFormsService); } - mapToProps(state: JsonFormsState): StatePropsOfArrayLayout { + mapToProps( + state: JsonFormsState + ): StatePropsOfArrayLayout & { translations: ArrayTranslations } { const props = mapStateToArrayLayoutProps(state, this.getOwnProps()); - return { ...props }; + const t = + state.jsonforms.i18n?.translate ?? defaultJsonFormsI18nState.translate; + const translations = getArrayTranslations( + t, + arrayDefaultTranslations, + props.i18nKeyPrefix, + props.label + ); + return { ...props, translations }; } remove(index: number): void { this.removeItems(this.propsPath, [index])(); @@ -211,10 +224,12 @@ export class ArrayLayoutRenderer this.moveItemDown = moveDown; this.removeItems = removeItems; } - mapAdditionalProps(props: ArrayLayoutProps) { - this.translations = props.translations; + mapAdditionalProps( + props: ArrayLayoutProps & { translations: ArrayTranslations } + ) { this.noData = !props.data || props.data === 0; this.uischemas = props.uischemas; + this.translations = props.translations; } getProps(index: number): OwnPropsOfRenderer { const uischema = findUISchema( diff --git a/packages/angular-material/src/library/other/master-detail/master.ts b/packages/angular-material/src/library/other/master-detail/master.ts index 4669f9d3f9..d50d4f092b 100644 --- a/packages/angular-material/src/library/other/master-detail/master.ts +++ b/packages/angular-material/src/library/other/master-detail/master.ts @@ -36,11 +36,14 @@ import { } from '@jsonforms/angular'; import { ArrayControlProps, + arrayDefaultTranslations, ArrayTranslations, ControlElement, createDefaultValue, decode, + defaultJsonFormsI18nState, findUISchema, + getArrayTranslations, getFirstPrimitiveProp, JsonFormsState, mapDispatchToArrayControlProps, @@ -194,7 +197,9 @@ export class MasterListComponent this.removeItems = removeItems; } - mapAdditionalProps(props: ArrayControlProps) { + mapAdditionalProps( + props: ArrayControlProps & { translations: ArrayTranslations } + ) { const { data, path, schema, uischema } = props; const controlElement = uischema as ControlElement; this.propsPath = props.path; @@ -282,9 +287,19 @@ export class MasterListComponent this.removeItems(this.propsPath, [item])(); } - protected mapToProps(state: JsonFormsState): StatePropsOfArrayControl { + protected mapToProps( + state: JsonFormsState + ): StatePropsOfArrayControl & { translations: ArrayTranslations } { const props = mapStateToArrayControlProps(state, this.getOwnProps()); - return { ...props }; + const t = + state.jsonforms.i18n?.translate ?? defaultJsonFormsI18nState.translate; + const translations = getArrayTranslations( + t, + arrayDefaultTranslations, + props.i18nKeyPrefix, + props.label + ); + return { ...props, translations }; } } diff --git a/packages/angular-material/src/library/other/table.renderer.ts b/packages/angular-material/src/library/other/table.renderer.ts index 5f38a9f6a6..3be3d7aaa5 100644 --- a/packages/angular-material/src/library/other/table.renderer.ts +++ b/packages/angular-material/src/library/other/table.renderer.ts @@ -158,7 +158,7 @@ export class TableRenderer extends JsonFormsArrayControl implements OnInit { moveItemUp: (path: string, index: number) => () => void; moveItemDown: (path: string, index: number) => () => void; removeItems: (path: string, toDelete: number[]) => () => void; - translations: ArrayTranslations; + translations: ArrayTranslations = {}; constructor(jsonformsService: JsonFormsAngularService) { super(jsonformsService); @@ -166,7 +166,9 @@ export class TableRenderer extends JsonFormsArrayControl implements OnInit { trackElement(index: number, _element: any) { return index ? index : null; } - mapAdditionalProps(props: ArrayControlProps) { + mapAdditionalProps( + props: ArrayControlProps & { translations: ArrayTranslations } + ) { this.items = this.generateCells(props.schema, props.path); this.displayedColumns = this.items.map((item) => item.property); if (this.isEnabled()) { diff --git a/packages/angular/src/library/array-control.ts b/packages/angular/src/library/array-control.ts index 013f6cd613..5e0bfbc5e5 100644 --- a/packages/angular/src/library/array-control.ts +++ b/packages/angular/src/library/array-control.ts @@ -23,6 +23,10 @@ THE SOFTWARE. */ import { + arrayDefaultTranslations, + ArrayTranslations, + defaultJsonFormsI18nState, + getArrayTranslations, JsonFormsState, mapStateToArrayControlProps, StatePropsOfArrayControl, @@ -34,8 +38,18 @@ export class JsonFormsArrayControl extends JsonFormsAbstractControl implements OnInit, OnDestroy { - protected mapToProps(state: JsonFormsState): StatePropsOfArrayControl { + protected mapToProps( + state: JsonFormsState + ): StatePropsOfArrayControl & { translations: ArrayTranslations } { const props = mapStateToArrayControlProps(state, this.getOwnProps()); - return { ...props }; + const t = + state.jsonforms.i18n?.translate ?? defaultJsonFormsI18nState.translate; + const translations = getArrayTranslations( + t, + arrayDefaultTranslations, + props.i18nKeyPrefix, + props.label + ); + return { ...props, translations }; } } diff --git a/packages/core/src/mappers/renderer.ts b/packages/core/src/mappers/renderer.ts index 60e8adfca0..fabdaa607c 100644 --- a/packages/core/src/mappers/renderer.ts +++ b/packages/core/src/mappers/renderer.ts @@ -47,13 +47,8 @@ import { getI18nKey, getI18nKeyPrefix, getI18nKeyPrefixBySchema, - getArrayTranslations, - CombinatorTranslations, - getCombinatorTranslations, - combinatorDefaultTranslations, getTranslator, getErrorTranslator, - arrayDefaultTranslations, ArrayTranslations, } from '../i18n'; import cloneDeep from 'lodash/cloneDeep'; @@ -789,7 +784,6 @@ export interface ControlWithDetailProps */ export interface StatePropsOfArrayControl extends StatePropsOfControlWithDetail { - translations: ArrayTranslations; childErrors?: ErrorObject[]; } @@ -804,12 +798,11 @@ export const mapStateToArrayControlProps = ( state: JsonFormsState, ownProps: OwnPropsOfControl ): StatePropsOfArrayControl => { - const { path, schema, uischema, i18nKeyPrefix, label, ...props } = + const { path, schema, uischema, label, ...props } = mapStateToControlWithDetailProps(state, ownProps); const resolvedSchema = Resolve.schema(schema, 'items', props.rootSchema); const childErrors = getSubErrorsAt(path, resolvedSchema)(state); - const t = getTranslator()(state); return { ...props, @@ -820,12 +813,6 @@ export const mapStateToArrayControlProps = ( childErrors, renderers: ownProps.renderers || getRenderers(state), cells: ownProps.cells || getCells(state), - translations: getArrayTranslations( - t, - arrayDefaultTranslations, - i18nKeyPrefix, - label - ), }; }; @@ -1060,7 +1047,6 @@ export interface StatePropsOfCombinator extends StatePropsOfControl { indexOfFittingSchema: number; uischemas: JsonFormsUISchemaRegistryEntry[]; data: any; - translations: CombinatorTranslations; } export const mapStateToCombinatorRendererProps = ( @@ -1072,13 +1058,6 @@ export const mapStateToCombinatorRendererProps = ( mapStateToControlProps(state, ownProps); const ajv = state.jsonforms.core.ajv; - const t = getTranslator()(state); - const translations = getCombinatorTranslations( - t, - combinatorDefaultTranslations, - i18nKeyPrefix, - label - ); const structuralKeywords = [ 'required', 'additionalProperties', @@ -1125,7 +1104,6 @@ export const mapStateToCombinatorRendererProps = ( label, indexOfFittingSchema, uischemas: getUISchemas(state), - translations, }; }; @@ -1160,7 +1138,6 @@ export const mapStateToOneOfProps = ( export interface StatePropsOfArrayLayout extends StatePropsOfControlWithDetail { data: number; - translations: ArrayTranslations; minItems?: number; disableRemove?: boolean; disableAdd?: boolean; @@ -1176,7 +1153,7 @@ export const mapStateToArrayLayoutProps = ( state: JsonFormsState, ownProps: OwnPropsOfControl ): StatePropsOfArrayLayout => { - const { path, schema, uischema, errors, i18nKeyPrefix, label, ...props } = + const { path, schema, uischema, errors, label, ...props } = mapStateToControlWithDetailProps(state, ownProps); const resolvedSchema = Resolve.schema(schema, 'items', props.rootSchema); @@ -1204,12 +1181,6 @@ export const mapStateToArrayLayoutProps = ( data: props.data ? props.data.length : 0, errors: allErrors, minItems: schema.minItems, - translations: getArrayTranslations( - t, - arrayDefaultTranslations, - i18nKeyPrefix, - label - ), }; }; diff --git a/packages/material-renderers/src/additional/MaterialListWithDetailRenderer.tsx b/packages/material-renderers/src/additional/MaterialListWithDetailRenderer.tsx index ba943d3f79..22cbc22298 100644 --- a/packages/material-renderers/src/additional/MaterialListWithDetailRenderer.tsx +++ b/packages/material-renderers/src/additional/MaterialListWithDetailRenderer.tsx @@ -25,6 +25,7 @@ import { and, ArrayLayoutProps, + ArrayTranslations, composePaths, computeLabel, createDefaultValue, @@ -36,7 +37,9 @@ import { } from '@jsonforms/core'; import { JsonFormsDispatch, + withArrayTranslationProps, withJsonFormsArrayLayoutProps, + withTranslateProps, } from '@jsonforms/react'; import { Grid, List, Typography } from '@mui/material'; import map from 'lodash/map'; @@ -63,11 +66,11 @@ export const MaterialListWithDetailRenderer = ({ cells, config, rootSchema, - translations, description, disableAdd, disableRemove, -}: ArrayLayoutProps) => { + translations, +}: ArrayLayoutProps & { translations: ArrayTranslations }) => { const [selectedIndex, setSelectedIndex] = useState(undefined); const handleRemoveItem = useCallback( (p: string, value: any) => () => { @@ -101,6 +104,7 @@ export const MaterialListWithDetailRenderer = ({ ), [uischemas, schema, uischema.scope, path, uischema, rootSchema] ); + const appliedUiSchemaOptions = merge({}, config, uischema.options); const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd; const doDisableRemove = disableRemove || appliedUiSchemaOptions.disableRemove; @@ -179,4 +183,6 @@ export const materialListWithDetailTester: RankedTester = rankWith( and(uiTypeIs('ListWithDetail'), isObjectArray) ); -export default withJsonFormsArrayLayoutProps(MaterialListWithDetailRenderer); +export default withJsonFormsArrayLayoutProps( + withTranslateProps(withArrayTranslationProps(MaterialListWithDetailRenderer)) +); diff --git a/packages/material-renderers/src/complex/MaterialArrayControlRenderer.tsx b/packages/material-renderers/src/complex/MaterialArrayControlRenderer.tsx index 346312b71f..8906ccbbb1 100644 --- a/packages/material-renderers/src/complex/MaterialArrayControlRenderer.tsx +++ b/packages/material-renderers/src/complex/MaterialArrayControlRenderer.tsx @@ -25,21 +25,28 @@ import React, { useCallback, useState } from 'react'; import { ArrayLayoutProps, + ArrayTranslations, RankedTester, isObjectArrayControl, isPrimitiveArrayControl, or, rankWith, } from '@jsonforms/core'; -import { withJsonFormsArrayLayoutProps } from '@jsonforms/react'; +import { + withArrayTranslationProps, + withJsonFormsArrayLayoutProps, + withTranslateProps, +} from '@jsonforms/react'; import { MaterialTableControl } from './MaterialTableControl'; import { DeleteDialog } from './DeleteDialog'; -export const MaterialArrayControlRenderer = (props: ArrayLayoutProps) => { +export const MaterialArrayControlRenderer = ( + props: ArrayLayoutProps & { translations: ArrayTranslations } +) => { const [open, setOpen] = useState(false); const [path, setPath] = useState(undefined); const [rowData, setRowData] = useState(undefined); - const { removeItems, visible } = props; + const { removeItems, visible, translations } = props; const openDeleteDialog = useCallback( (p: string, rowIndex: number) => { @@ -63,16 +70,20 @@ export const MaterialArrayControlRenderer = (props: ArrayLayoutProps) => { return ( <> - + ); @@ -83,4 +94,6 @@ export const materialArrayControlTester: RankedTester = rankWith( or(isObjectArrayControl, isPrimitiveArrayControl) ); -export default withJsonFormsArrayLayoutProps(MaterialArrayControlRenderer); +export default withJsonFormsArrayLayoutProps( + withTranslateProps(withArrayTranslationProps(MaterialArrayControlRenderer)) +); diff --git a/packages/material-renderers/src/complex/MaterialTableControl.tsx b/packages/material-renderers/src/complex/MaterialTableControl.tsx index e221894a88..00239fc6c0 100644 --- a/packages/material-renderers/src/complex/MaterialTableControl.tsx +++ b/packages/material-renderers/src/complex/MaterialTableControl.tsx @@ -451,7 +451,8 @@ const TableRows = ({ }; export class MaterialTableControl extends React.Component< - ArrayLayoutProps & WithDeleteDialogSupport, + ArrayLayoutProps & + WithDeleteDialogSupport & { translations: ArrayTranslations }, any > { addItem = (path: string, value: any) => this.props.addItem(path, value); diff --git a/packages/material-renderers/src/layouts/MaterialArrayLayout.tsx b/packages/material-renderers/src/layouts/MaterialArrayLayout.tsx index 6cec1d884d..6846fd421e 100644 --- a/packages/material-renderers/src/layouts/MaterialArrayLayout.tsx +++ b/packages/material-renderers/src/layouts/MaterialArrayLayout.tsx @@ -26,6 +26,7 @@ import range from 'lodash/range'; import React, { useState, useCallback } from 'react'; import { ArrayLayoutProps, + ArrayTranslations, composePaths, computeLabel, createDefaultValue, @@ -35,7 +36,9 @@ import { ArrayLayoutToolbar } from './ArrayToolbar'; import ExpandPanelRenderer from './ExpandPanelRenderer'; import merge from 'lodash/merge'; -const MaterialArrayLayoutComponent = (props: ArrayLayoutProps) => { +const MaterialArrayLayoutComponent = ( + props: ArrayLayoutProps & { translations: ArrayTranslations } +) => { const [expanded, setExpanded] = useState(false); const innerCreateDefaultValue = useCallback( () => createDefaultValue(props.schema, props.rootSchema), @@ -65,10 +68,10 @@ const MaterialArrayLayoutComponent = (props: ArrayLayoutProps) => { rootSchema, config, uischemas, - translations, description, disableAdd, disableRemove, + translations, } = props; const appliedUiSchemaOptions = merge({}, config, props.uischema.options); const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd; diff --git a/packages/material-renderers/src/layouts/MaterialArrayLayoutRenderer.tsx b/packages/material-renderers/src/layouts/MaterialArrayLayoutRenderer.tsx index d684301431..2fd1001e29 100644 --- a/packages/material-renderers/src/layouts/MaterialArrayLayoutRenderer.tsx +++ b/packages/material-renderers/src/layouts/MaterialArrayLayoutRenderer.tsx @@ -26,29 +26,39 @@ import React, { useCallback } from 'react'; import { ArrayLayoutProps, + ArrayTranslations, isObjectArrayWithNesting, RankedTester, rankWith, } from '@jsonforms/core'; import { MaterialArrayLayout } from './MaterialArrayLayout'; -import { withJsonFormsArrayLayoutProps } from '@jsonforms/react'; +import { + withArrayTranslationProps, + withJsonFormsArrayLayoutProps, + withTranslateProps, +} from '@jsonforms/react'; export const MaterialArrayLayoutRenderer = ({ visible, addItem, + translations, ...props -}: ArrayLayoutProps) => { +}: ArrayLayoutProps & { translations: ArrayTranslations }) => { const addItemCb = useCallback( (p: string, value: any) => addItem(p, value), [addItem] ); - if (!visible) { return null; } return ( - + ); }; @@ -56,4 +66,6 @@ export const materialArrayLayoutTester: RankedTester = rankWith( 4, isObjectArrayWithNesting ); -export default withJsonFormsArrayLayoutProps(MaterialArrayLayoutRenderer); +export default withJsonFormsArrayLayoutProps( + withTranslateProps(withArrayTranslationProps(MaterialArrayLayoutRenderer)) +); diff --git a/packages/react/src/JsonFormsContext.tsx b/packages/react/src/JsonFormsContext.tsx index 84752883a3..b10323b65f 100644 --- a/packages/react/src/JsonFormsContext.tsx +++ b/packages/react/src/JsonFormsContext.tsx @@ -78,6 +78,9 @@ import { CoreActions, Middleware, defaultMiddleware, + arrayDefaultTranslations, + getArrayTranslations, + ArrayTranslations, } from '@jsonforms/core'; import debounce from 'lodash/debounce'; import React, { @@ -889,3 +892,20 @@ export const withTranslateProps =

( return ; }; + +export const withArrayTranslationProps =

( + Component: ComponentType

+) => + function withArrayTranslatationProps(props: P & TranslateProps) { + const translations = useMemo( + () => + getArrayTranslations( + props.t, + arrayDefaultTranslations, + props.i18nKeyPrefix, + props.label + ), + [props.t, props.i18nKeyPrefix, props.label] + ); + return ; + }; diff --git a/packages/vanilla-renderers/src/complex/TableArrayControl.tsx b/packages/vanilla-renderers/src/complex/TableArrayControl.tsx index 1850b631a2..efca8f45e8 100644 --- a/packages/vanilla-renderers/src/complex/TableArrayControl.tsx +++ b/packages/vanilla-renderers/src/complex/TableArrayControl.tsx @@ -41,8 +41,14 @@ import { Test, getControlPath, encode, + ArrayTranslations, } from '@jsonforms/core'; -import { DispatchCell, withJsonFormsArrayControlProps } from '@jsonforms/react'; +import { + DispatchCell, + withArrayTranslationProps, + withJsonFormsArrayControlProps, + withTranslateProps, +} from '@jsonforms/react'; import { withVanillaControlProps } from '../util'; import type { VanillaRendererProps } from '../index'; @@ -61,7 +67,8 @@ export const tableArrayControlTester: RankedTester = rankWith( ); class TableArrayControl extends React.Component< - ArrayControlProps & VanillaRendererProps, + ArrayControlProps & + VanillaRendererProps & { translations: ArrayTranslations }, any > { confirmDelete = (path: string, index: number) => { @@ -240,5 +247,7 @@ class TableArrayControl extends React.Component< } export default withVanillaControlProps( - withJsonFormsArrayControlProps(TableArrayControl) + withJsonFormsArrayControlProps( + withTranslateProps(withArrayTranslationProps(TableArrayControl)) + ) ); diff --git a/packages/vanilla-renderers/src/complex/array/ArrayControlRenderer.tsx b/packages/vanilla-renderers/src/complex/array/ArrayControlRenderer.tsx index 884cfe586c..4c62e8ae00 100644 --- a/packages/vanilla-renderers/src/complex/array/ArrayControlRenderer.tsx +++ b/packages/vanilla-renderers/src/complex/array/ArrayControlRenderer.tsx @@ -31,10 +31,13 @@ import { findUISchema, Helpers, ControlElement, + ArrayTranslations, } from '@jsonforms/core'; import { JsonFormsDispatch, + withArrayTranslationProps, withJsonFormsArrayControlProps, + withTranslateProps, } from '@jsonforms/react'; import type { VanillaRendererProps } from '../../index'; import { withVanillaControlProps } from '../../util'; @@ -58,7 +61,8 @@ export const ArrayControl = ({ renderers, rootSchema, translations, -}: ArrayControlProps & VanillaRendererProps) => { +}: ArrayControlProps & + VanillaRendererProps & { translations: ArrayTranslations }) => { const controlElement = uischema as ControlElement; const childUiSchema = useMemo( () => @@ -184,7 +188,8 @@ export const ArrayControlRenderer = ({ enabled, errors, translations, -}: ArrayControlProps & VanillaRendererProps) => { +}: ArrayControlProps & + VanillaRendererProps & { translations: ArrayTranslations }) => { const controlElement = uischema as ControlElement; const labelDescription = Helpers.createLabelDescriptionFrom( controlElement, @@ -230,5 +235,7 @@ export const ArrayControlRenderer = ({ }; export default withVanillaControlProps( - withJsonFormsArrayControlProps(ArrayControlRenderer) + withJsonFormsArrayControlProps( + withTranslateProps(withArrayTranslationProps(ArrayControlRenderer)) + ) );