diff --git a/src/components/loop/roster-for-loop/roster-for-loop.tsx b/src/components/loop/roster-for-loop/roster-for-loop.tsx index 21f25ab6c..eae012650 100644 --- a/src/components/loop/roster-for-loop/roster-for-loop.tsx +++ b/src/components/loop/roster-for-loop/roster-for-loop.tsx @@ -93,6 +93,7 @@ export const RosterForLoop = createCustomizableLunaticField< ({ ...otherProps, ...c, diff --git a/src/components/lunatic-components.tsx b/src/components/lunatic-components.tsx index f904cf0f1..93c8f206b 100644 --- a/src/components/lunatic-components.tsx +++ b/src/components/lunatic-components.tsx @@ -1,6 +1,7 @@ import { Fragment, isValidElement, + memo, type PropsWithChildren, type ReactElement, type ReactNode, @@ -10,6 +11,7 @@ import * as lunaticComponents from './index'; import type { FilledLunaticComponentProps } from '../use-lunatic/commons/fill-components/fill-components'; import { useAutoFocus } from '../hooks/use-auto-focus'; import { hasComponentType } from '../use-lunatic/commons/component'; +import { useWhyRender } from '../hooks/use-why-render'; type Props = { // List of components to display (coming from getComponents) @@ -18,6 +20,8 @@ type Props = { | ReactElement | { label: string; [key: string]: unknown } )[]; + // Should we memoized children + memo?: boolean; // Key that trigger autofocus when it changes (pageTag) autoFocusKey?: string; // Returns the list of extra props to add to components @@ -43,6 +47,7 @@ export function LunaticComponents< autoFocusKey, componentProps, blocklist, + memo, wrapper = ({ children }) => (
{children}
), @@ -81,7 +86,11 @@ export function LunaticComponents< return ( {wrapper({ - children: , + children: memo ? ( + + ) : ( + + ), index: k, ...props, })} @@ -128,6 +137,8 @@ function LunaticComponent(props: ItemProps) { return ; } +const LunaticComponentMemo = memo(LunaticComponent); + function computeId( component: Record, fallback: number | string diff --git a/src/stories/behaviour/performance/performance.stories.jsx b/src/stories/behaviour/performance/performance.stories.jsx new file mode 100644 index 000000000..5076240df --- /dev/null +++ b/src/stories/behaviour/performance/performance.stories.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Orchestrator from '../../utils/orchestrator'; +import source from './source.json'; +import { generateData } from '../../../../tests/utils/lunatic'; +import { times } from '../../../utils/array'; + +const stories = { + title: 'Behaviour/Performance', + component: Orchestrator, +}; + +export default stories; + +const Template = (args) => ; +export const Default = Template.bind({}); +Default.args = { + id: 'performance-default', + pagination: true, + source, + data: generateData({ + PRENOM: times(200, (k) => `John${k}`), + NOM: times(200, (k) => `Doe${k}`), + AGE: times(200, (k) => k + 1), + BIRTHDAY: times(200, (k) => `2${k.toString().padStart(3, '0')}-01-01`), + }), +}; diff --git a/src/stories/behaviour/performance/source.json b/src/stories/behaviour/performance/source.json new file mode 100644 index 000000000..5d6349af8 --- /dev/null +++ b/src/stories/behaviour/performance/source.json @@ -0,0 +1,172 @@ +{ + "maxPage": "3", + "components": [ + { + "id": "seq", + "componentType": "Sequence", + "label": { + "value": "\"Description des individus de votre logement\"", + "type": "VTL|MD" + }, + "conditionFilter": { "value": "true", "type": "VTL" }, + "page": "1" + }, + { + "id": "loop-prenom", + "componentType": "RosterForLoop", + "header": [ + { "headerCell": true, "label": "Prénom" }, + { "headerCell": true, "label": "Nom" }, + { "headerCell": true, "label": "Date de naissance" }, + { "headerCell": true, "label": "Age" } + ], + "label": { "value": "\"Ajouter un individu\"", "type": "VTL|MD" }, + "conditionFilter": { "value": "true", "type": "VTL" }, + "bindingDependencies": ["PRENOM", "AGE"], + "lines": { + "min": { "value": 1, "type": "VTL" }, + "max": { "value": 10, "type": "VTL" } + }, + "page": "1", + "components": [ + { + "componentType": "Input", + "conditionFilter": { "value": "true", "type": "VTL" }, + "maxLength": 30, + "bindingDependencies": ["PRENOM"], + "id": "prenom", + "response": { + "name": "PRENOM" + } + }, + { + "componentType": "Input", + "conditionFilter": { "value": "true", "type": "VTL" }, + "maxLength": 30, + "bindingDependencies": ["NOM"], + "id": "nom", + "response": { + "name": "NOM" + } + }, + { + "componentType": "Datepicker", + "conditionFilter": { "value": "true", "type": "VTL" }, + "maxLength": 30, + "bindingDependencies": ["BIRTHDAY"], + "id": "birthday", + "dateFormat": "YYYY-MM-DD", + "response": { + "name": "BIRTHDAY" + } + }, + { + "componentType": "Input", + "conditionFilter": { "value": "true", "type": "VTL" }, + "maxLength": 30, + "bindingDependencies": ["AGE"], + "id": "age", + "response": { + "name": "AGE" + } + } + ] + }, + { + "id": "loop", + "componentType": "Loop", + + "loopDependencies": ["PRENOM"], + "iterations": { "value": "count(PRENOM)", "type": "VTL" }, + "page": "2", + "maxPage": "1", + "depth": 1, + "paginatedLoop": true, + "conditionFilter": { + "value": "true", + "type": "VTL" + }, + "components": [ + { + "id": "age", + "label": { + "value": "PRENOM || \", quel est vôtre âge ?\"", + "type": "VTL" + }, + "conditionFilter": { + "value": "true", + "type": "VTL" + }, + "page": "2.1", + "componentType": "InputNumber", + "min": 0, + "max": 120, + "decimals": 0, + "response": { "name": "AGE" } + } + ] + }, + { + "id": "seq-end", + "componentType": "Sequence", + "label": { + "value": "\"End\"", + "type": "VTL|MD" + }, + "conditionFilter": { "value": "true", "type": "VTL" }, + "page": "3" + } + ], + "resizing": { + "PRENOM": { + "size": "count(PRENOM)", + "variables": ["AGE"] + } + }, + "variables": [ + { + "variableType": "COLLECTED", + "name": "PRENOM", + "values": { + "PREVIOUS": [null], + "COLLECTED": [null], + "FORCED": [null], + "EDITED": [null], + "INPUTED": [null] + } + }, + { + "variableType": "COLLECTED", + "name": "NOM", + "values": { + "PREVIOUS": [null], + "COLLECTED": [null], + "FORCED": [null], + "EDITED": [null], + "INPUTED": [null] + } + }, + { + "variableType": "COLLECTED", + "name": "BIRTHDAY", + "values": { + "PREVIOUS": [null], + "COLLECTED": [null], + "FORCED": [null], + "EDITED": [null], + "INPUTED": [null] + } + }, + { + "variableType": "COLLECTED", + "name": "AGE", + "values": { + "PREVIOUS": [null], + "COLLECTED": [null], + "FORCED": [null], + "EDITED": [null], + "INPUTED": [null] + } + } + ] +} diff --git a/src/use-lunatic/commons/fill-components/fill-specific-expression.ts b/src/use-lunatic/commons/fill-components/fill-specific-expression.ts index c60560441..ff1b46332 100644 --- a/src/use-lunatic/commons/fill-components/fill-specific-expression.ts +++ b/src/use-lunatic/commons/fill-components/fill-specific-expression.ts @@ -59,9 +59,10 @@ function fillChildComponentsWithIteration( getComponents: (iteration: number) => fillComponents(component.components, { ...state, - handleChange: (response, value) => { - state.handleChange(response, value, { iteration: [iteration] }); - }, + handleChange: createChangeHandlerForIteration( + state.handleChange, + iteration + ), pager: { ...state.pager, iteration: iteration, @@ -71,6 +72,28 @@ function fillChildComponentsWithIteration( }; } +// Create change handler memoized for every iteration +let changeHandler = null as null | LunaticState['handleChange']; +const changeHandlerMap = new Map(); +function createChangeHandlerForIteration( + handleChange: LunaticState['handleChange'], + iteration: number +) { + if (handleChange !== changeHandler) { + changeHandler = handleChange; + changeHandlerMap.clear(); + } + let handler = changeHandlerMap.get(iteration); + if (handler) { + return handler; + } + handler = (response, value) => { + handleChange(response, value, { iteration: [iteration] }); + }; + changeHandlerMap.set(iteration, handler); + return handler; +} + /** * For pairwise, inject a method to retrieve component at a specific iteration combination */