Migration à partir de points d’entrée Jutro internes
Certaines applications peuvent être importées involontairement à partir du point d’entrée interne @jutro/components/jutro-editor. Ce chemin ne fait pas partie de la surface de l’API publique Jutro et n’a jamais été couvert par les garanties de rétrocompatibilité de Jutro. Il a été supprimé dans Jutro 10.13.
Si votre build échoue avec des erreurs sur l’une des importations suivantes depuis @jutro/components/jutro-editor, ce guide explique comment migrer votre application :
FormContaineruseFormDatauseFormValidationuseFormContextformFieldoverridePropsgetUniqueValueEditorInputcopyUniqueValueModalNextAlertModalNextConfirmationModalNextCustomInlineNotification
Choix d’une option de migration
La bonne approche dépend de la façon dont FormContainer et les crochets associés sont utilisés dans votre application. Il existe trois modèles, chacun avec un correctif recommandé différent :
| Modèle | Comment l’identifier ? | Correctif recommandé |
|---|---|---|
| Wrapper passif | useFormData et useFormValidation sont appelés, mais leurs sorties ne sont transmises qu’à FormContainer ; rien d’autre dans le composant ne les lit. | Supprimez <FormContainer> et les crochets. |
| État du formulaire actif | Les sorties des crochets déterminent le comportement réel : soumission, déclenchement de validation ou filtrage. | Déplacez l’état dans un crochet spécifique à la page avec des setters spécifiques au domaine. |
| Utilisateurs de contexte | Un composant enfant importe useFormContext pour lire les données du formulaire directement à partir du contexte. | Supprimez la dépendance au contexte et transmettez les valeurs en tant que propriétés explicites. |
Si vous ne pouvez pas terminer la migration avant la mise à niveau vers Jutro 10.13, reportez-vous à Alternative temporaire : copier les implémentations localement.
Option 1 : retirer le wrapper passif
Utilisez cette option lorsque FormContainer est un wrapper structurel sans rôle actif : useFormData et useFormValidation sont appelés, mais leurs sorties ne sont transmises qu’aux propriétés FormContainer et rien d’autre dans le composant ne les lit.
Dans ce cas, l’élément FormContainer peut être supprimé avec les crochets inutilisés. La sortie affichée et le comportement du composant sont identiques sans eux.
Pour refactoriser le code, procédez comme suit :
- Supprimez l’élément
<FormContainer>du fichier JSX et déplacez ses éléments enfants d’un niveau. - Supprimez les appels de crochet
useFormDataetuseFormValidation, ainsi que toutes les variables et fonctions locales qui existaient uniquement pour alimenter les propriétésFormContainer, par exemple,callbackMap,submittedet tous les gestionnaires de réinitialisation ou d’enregistrement intégrés. - Supprimez l’importation de
@jutro/components/jutro-editor.
Exemple de suppression de wrapper passif
L’exemple suivant montre un composant de page dans lequel <FormContainer> encapsule le contenu et useFormData et useFormValidation sont appelés, mais leurs sorties n’alimentent rien au-delà des propriétés FormContainer.
Avant
Le composant appelle useFormData et useFormValidation et encapsule ses éléments enfants dans <FormContainer>, mais aucune des sorties de crochet n’est lue nulle part en dehors des propriétés FormContainer. Des fonctions supplémentaires d’état local et de gestionnaire existent uniquement pour satisfaire l'API de FormContainer.
import React, { useState } from 'react';
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
interface PoliciesProps {
className?: string;
overrides: { [key: string]: Record<string, unknown> };
}
export const Policies: React.FC<PoliciesProps> = (props) => {
const [formData, setFormValue, resetFormData] = useFormData({});
const [isFormValid, handleFormValidation] = useFormValidation();
const [submitted, setSubmitted] = useState(false);
const resetForm = () => {
setSubmitted(false);
resetFormData({});
};
const saveForm = () => {
if (!isFormValid) setSubmitted(true);
};
const callbackMap = { resetForm, saveForm };
return (
<FormContainer
data={formData}
onDataChange={setFormValue}
callbackMap={callbackMap}
onValidationChange={handleFormValidation}
showErrors={submitted}>
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, props.className)}>
<PolicyTable className={classNames('instance', styles.policyTable)} />
</div>
</div>
</FormContainer>
);
};
Après
Les crochets, leur état de soutien et <FormContainer> sont entièrement retirés. La sortie affichée et le comportement du composant sont identiques.
import React from 'react';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
interface PoliciesProps {
className?: string;
}
export const Policies: React.FC<PoliciesProps> = ({ className }) => (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<PolicyTable className={classNames('instance', styles.policyTable)} />
</div>
</div>
);
Option 2 : remplacer par un crochet spécifique à la page
Utilisez cette option lorsque les sorties du crochet alimentent une logique de composant réel, par exemple, pour contrôler si un formulaire peut être soumis, contrôler l’exécution d’une extraction de données ou appliquer un filtre basé sur l’entrée utilisateur.
La solution consiste à déplacer l’état du formulaire dans un crochet dédié avec des setters et des gestionnaires spécifiques au domaine, et à supprimer FormContainer du fichier JSX. Les composants enfants reçoivent des valeurs et des gestionnaires sous forme de propriétés simples.
Pour refactoriser le code, procédez comme suit :
- Créez un fichier de crochet dédié, par exemple
usePolicies.ts. - Dans le crochet, remplacez
useFormDatapar des appels individuelsuseState, un pour chaque valeur suivie. Donnez à chaque setter un nom qui reflète l’objectif de son domaine plutôt que d’utiliser la signature générique(value, path). - Remplacez
useFormValidationpar une expression de validité simple que le crochet détient. Le crochet définit ce que signifie « valide » pour cette page spécifique. - Déplacez l’action et la logique de réinitialisation dans des fonctions de gestionnaire nommées dans le crochet qui reflètent l'objectif de leur domaine. Supprimez
callbackMap. - Supprimez
<FormContainer>du fichier JSX. - Utilisez le crochet dans le composant de page et transmettez les valeurs et les gestionnaires directement en tant que propriétés aux composants enfants.
- Supprimez l’importation de
@jutro/components/jutro-editor.
Exemple de crochet spécifique à une page
L’exemple suivant montre un composant de page où useFormData et useFormValidation pilotent un comportement réel, dans ce cas, la validation d’un champ de numéro de police et son application en tant que filtre. La version antérieure gère tous les états intégrés à l’aide de FormContainer. La version « après » déplace cet état dans un crochet dédié usePolicies et supprime FormContainer entièrement, les composants enfants recevant des valeurs et des gestionnaires en tant que propriétés simples.
Avant
Le composant de page gère tous les états du formulaire intégrés. useFormData effectue le suivi de la valeur du champ, useFormValidation suit la validité et FormContainer agit en tant que fournisseur de données. La logique de soumission et de réinitialisation se trouve directement dans le corps du composant.
import React, { useState } from 'react';
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';
import { TextInput, Button } from '@jutro/components';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
interface PoliciesProps {
className?: string;
overrides: { [key: string]: Record<string, unknown> };
}
export const Policies: React.FC<PoliciesProps> = (props: PoliciesProps) => {
const [formData, setFormValue, resetFormData] = useFormData({
policyNumber: '',
});
const [isFormValid, handleFormValidation] = useFormValidation();
const [submitted, setSubmitted] = useState(false);
const [appliedFilter, setAppliedFilter] = useState('');
const resetForm = () => {
setSubmitted(false);
resetFormData({ policyNumber: '' });
setAppliedFilter('');
};
const saveForm = () => {
if (!isFormValid) {
setSubmitted(true);
return;
}
setAppliedFilter(formData.policyNumber as string);
};
return (
<FormContainer
data={formData}
onDataChange={setFormValue}
callbackMap={{ resetForm, saveForm }}
onValidationChange={handleFormValidation}
showErrors={submitted}>
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, props.className)}>
<TextInput
path="policyNumber"
label="Policy number"
value={formData.policyNumber as string}
onValueChange={setFormValue}
onValidationChange={handleFormValidation}
showErrors={submitted}
required
/>
<Button onClick={saveForm}>Apply</Button>
<Button
onClick={resetForm}
type="tertiary">
Reset
</Button>
<PolicyTable
filter={appliedFilter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
</FormContainer>
);
};
Après
Le composant de page est maintenant disponible comme interface utilisateur uniquement. Il consomme le crochet et transmet les valeurs et les gestionnaires en tant que propriétés. Il n’y a pas d’état de formulaire ou FormContainer dans le fichier JSX.
import React from 'react';
import { TextInput, Button } from '@jutro/components';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
import { usePolicies } from './usePolicies';
interface PoliciesProps {
className?: string;
}
export const Policies: React.FC<PoliciesProps> = ({ className }) => {
const {
policyNumber,
setPolicyNumber,
appliedFilter,
showErrors,
applyFilter,
resetFilter,
} = usePolicies();
return (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<TextInput
path="policyNumber"
label="Policy number"
value={policyNumber}
onValueChange={setPolicyNumber}
showErrors={showErrors}
required
/>
<Button onClick={applyFilter}>Apply</Button>
<Button
onClick={resetFilter}
type="tertiary">
Reset
</Button>
<PolicyTable
filter={appliedFilter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
);
};
Le crochet détient l’ensemble de la gestion et de la logique d’état. Chaque champ a son propre appel useState avec un setter nommé. La validité est une expression simple. Les fonctions Soumettre et Réinitialiser sont des fonctions nommées (applyFilter et resetFilter) que le composant de page appelle directement.
import { useCallback, useState } from 'react';
export const usePolicies = () => {
const [policyNumber, setPolicyNumberState] = useState('');
const [appliedFilter, setAppliedFilter] = useState('');
const [showErrors, setShowErrors] = useState(false);
const isValid = policyNumber.trim().length > 0;
// path is unused but kept to match the (value, path) onValueChange signature
const setPolicyNumber = useCallback((value: string, _path: string) => {
setPolicyNumberState(value);
}, []);
const applyFilter = useCallback(() => {
if (!isValid) {
setShowErrors(true);
return;
}
setAppliedFilter(policyNumber);
}, [isValid, policyNumber]);
const resetFilter = useCallback(() => {
setShowErrors(false);
setPolicyNumberState('');
setAppliedFilter('');
}, []);
return {
policyNumber,
setPolicyNumber,
appliedFilter,
showErrors,
applyFilter,
resetFilter,
};
};
Option 3 : remplacer le contexte implicite par des propriétés explicites
Utilisez cette option lorsqu’un composant enfant importe useFormContext pour lire les données à partir d’un ancêtre FormContainer.
La solution consiste à supprimer la dépendance au contexte de l’enfant et à transmettre les valeurs requises en tant que propriétés explicites du parent à la place. Cela rend les dépendances de l’enfant visibles dans son type de propriété et supprime la nécessité d’un ancêtre FormContainer.
Pour refactoriser le code, procédez comme suit :
- Dans le composant enfant, ajoutez les valeurs requises en tant que propriétés explicites dans l’interface du composant.
- Supprimez l’appel
useFormContextet remplacez toutes les valeurs de lecture contextuelle par les nouvelles propriétés. - Supprimez l’importation de
@jutro/components/jutro-editordans le composant enfant. - Dans le composant parent, supprimez tous les éléments
useFormDataetuseFormValidationet les variables d’état local ou de gestionnaire associées qui étaient présentes uniquement pour alimenterFormContainer. Gérez l’état requis directement à l’aide deuseStateet transmettez les valeurs en tant que propriétés explicites. - Supprimez
<FormContainer>du parent.
Exemple de remplacement du contexte par des propriétés explicites
L’exemple suivant montre un composant enfant qui lit une valeur de filtre à partir du contexte du formulaire à l’aide de useFormContext. Après la migration, l’élément enfant déclare la valeur en tant que propriété explicite et l'élément parent gère directement l’état, en supprimant la dépendance sur FormContainer.
Avant
Le composant enfant lit data.policyNumber à partir de FormContext à l’aide de useFormContext. Cela crée une dépendance implicite sur un ancêtre FormContainer ; l'élément enfant ne peut pas être utilisé ou testé indépendamment.
import React from 'react';
import { useFormContext } from '@jutro/components/jutro-editor';
interface PolicyTableProps {
className?: string;
}
export const PolicyTable: React.FC<PolicyTableProps> = ({ className }) => {
const { data } = useFormContext();
const filter = (data?.policyNumber as string) ?? '';
// ...fetch / filter rows by `filter`, render the table
return <table className={className}>{/* rows for `filter` */}</table>;
};
Après
L’élément enfant déclare maintenant la valeur requise en tant que propriété explicite. La dépendance au contexte a disparu et le composant n’a plus besoin d’un ancêtre FormContainer.
import React from 'react';
interface PolicyTableProps {
className?: string;
filter?: string;
}
export const PolicyTable: React.FC<PolicyTableProps> = ({
className,
filter = '',
}) => {
// ...fetch / filter rows by `filter`, render the table
return <table className={className}>{/* rows for `filter` */}</table>;
};
L'élément parent gère l’état directement avec useState et le transmet en tant que propriété. Il n’y a pas de FormContainer dans le fichier JSX.
import React, { useState } from 'react';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
interface PoliciesProps {
className?: string;
}
export const Policies: React.FC<PoliciesProps> = ({ className }) => {
const [filter] = useState('');
return (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<PolicyTable
filter={filter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
);
};
Alternative temporaire : copier les implémentations localement
Si vous ne pouvez pas effectuer la migration complète avant la mise à niveau vers Jutro 10.13, vous pouvez copier les implémentations concernées dans votre application sous forme de fichiers locaux et mettre à jour uniquement les chemins d’importation. La logique du composant et JSX restent inchangés.
Pour appliquer l’alternative temporaire, procédez comme suit :
- Créez les trois fichiers suivants dans votre application, par exemple sous
src/utils/form/:
import React, { createContext, useContext, useMemo } from 'react';
export type FormContextValue = {
data?: Record<string, unknown>;
onDataChange?: (value: unknown, path: string) => void;
onValidationChange?: (isFieldValid: boolean, fieldPath: string) => void;
callbackMap?: Record<string, (...args: unknown[]) => unknown>;
showErrors?: boolean;
readOnly?: boolean;
disabled?: boolean;
displayOnly?: boolean;
};
const FormContext = createContext<FormContextValue | undefined>(undefined);
export const useFormContext = (): FormContextValue => {
const ctx = useContext(FormContext);
if (!ctx) {
throw new Error('Please check that your page is wrapped in FormContainer');
}
return ctx;
};
export type FormContainerProps = FormContextValue & {
children: React.ReactNode;
};
export const FormContainer: React.FC<FormContainerProps> = ({
children,
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
}) => {
const value = useMemo(
() => ({
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
}),
[
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
]
);
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};
import { useCallback, useRef, useState } from 'react';
import { cloneDeep, set } from 'lodash';
type FormValue = unknown;
type FormData = Record<string, FormValue>;
type SetFormValue = (value: FormValue, path: string) => void;
type ResetFormData = (next?: FormData | (() => FormData)) => void;
export const useFormData = (
initial: FormData | (() => FormData) = {}
): [FormData, SetFormValue, ResetFormData, FormData | undefined] => {
const previous = useRef<FormData | undefined>(undefined);
const [formData, setFormData] = useState<FormData>(() => {
const seed =
typeof initial === 'function' ? (initial as () => FormData)() : initial;
previous.current = seed ? cloneDeep(seed) : {};
return seed;
});
const resetFormData: ResetFormData = useCallback((next) => {
const seed =
typeof next === 'function' ? (next as () => FormData)() : next ?? {};
setFormData(() => {
previous.current = cloneDeep(seed);
return seed;
});
}, []);
const setFormValue: SetFormValue = useCallback((value, path) => {
setFormData((prev) => {
const nextState = { ...prev };
const v = value as {
code?: unknown;
name?: { defaultMessage?: unknown };
} | null;
if (
v &&
typeof v === 'object' &&
v.code !== undefined &&
v.name?.defaultMessage !== undefined
) {
set(nextState, path, { code: v.code, name: v.name.defaultMessage });
} else {
set(nextState, path, value);
}
return nextState;
});
}, []);
return [formData, setFormValue, resetFormData, previous.current];
};
import { useCallback, useState } from 'react';
type ValidationChangeFn = (isFieldValid: boolean, fieldPath: string) => void;
export const useFormValidation = (): [boolean, ValidationChangeFn] => {
const [invalidPaths, setInvalidPaths] = useState<string[]>([]);
const handleFieldValidationChange: ValidationChangeFn = useCallback(
(isFieldValid, fieldPath) => {
setInvalidPaths((prev) => {
const idx = prev.indexOf(fieldPath);
if (isFieldValid && idx !== -1) {
const next = [...prev];
next.splice(idx, 1);
return next;
}
if (!isFieldValid && idx === -1) {
return [...prev, fieldPath];
}
return prev;
});
},
[]
);
return [invalidPaths.length === 0, handleFieldValidationChange];
};
- Une fois les fichiers en place, mettez seulement à jour les instructions d’importation dans chaque fichier concerné :
// Before
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';
// After
import { FormContainer } from 'src/utils/form/FormContext';
import { useFormData } from 'src/utils/form/useFormData';
import { useFormValidation } from 'src/utils/form/useFormValidation';