Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing frequent re-renders with latest app-lib-dotnet #2683

Merged
merged 3 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/features/datamodel/DataModelsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from 'src/features/datamodel/utils';
import { useLayouts } from 'src/features/form/layout/LayoutsContext';
import { useFormDataQuery } from 'src/features/formData/useFormDataQuery';
import { useLaxInstanceAllDataElements, useLaxInstanceDataElements } from 'src/features/instance/InstanceContext';
import { useLaxInstanceAllDataElementsNow, useLaxInstanceDataElements } from 'src/features/instance/InstanceContext';
import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError';
import { useIsPdf } from 'src/hooks/useIsPdf';
import { isAxiosError } from 'src/utils/isAxiosError';
Expand Down Expand Up @@ -136,7 +136,7 @@ function DataModelsLoader() {
const layouts = useLayouts();
const defaultDataType = useCurrentDataModelName();
const isStateless = useApplicationMetadata().isStatelessApp;
const dataElements = useLaxInstanceAllDataElements();
const dataElements = useLaxInstanceAllDataElementsNow();

// Subform
const overriddenDataElement = useTaskStore((state) => state.overriddenDataModelUuid);
Expand Down
16 changes: 10 additions & 6 deletions src/features/datamodel/useBindingSchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'src/features/applicationMetadata/appMetadataUtils';
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
import { useLayoutSets } from 'src/features/form/layoutSets/LayoutSetsProvider';
import { useLaxInstanceAllDataElements, useLaxInstanceId } from 'src/features/instance/InstanceContext';
import { useLaxInstanceData, useLaxInstanceId } from 'src/features/instance/InstanceContext';
import { useProcessTaskId } from 'src/features/instance/useProcessTaskId';
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { useAllowAnonymous } from 'src/features/stateless/getAllowAnonymous';
Expand All @@ -29,17 +29,21 @@ export type AsSchema<T> = {
};

export function useCurrentDataModelGuid() {
const dataElements = useLaxInstanceAllDataElements();
const application = useApplicationMetadata();
const layoutSets = useLayoutSets();
const taskId = useProcessTaskId();

const overriddenDataModelGuid = useTaskStore((s) => s.overriddenDataModelUuid);
if (overriddenDataModelGuid) {
return overriddenDataModelGuid;
}

return getCurrentTaskDataElementId({ application, dataElements, taskId, layoutSets });
// Instance data elements will update often (after each save), so we have to use a selector to make
// sure components don't re-render too often.
return useLaxInstanceData((data) => {
if (overriddenDataModelGuid) {
return overriddenDataModelGuid;
}

return getCurrentTaskDataElementId({ application, dataElements: data.data, taskId, layoutSets });
});
}

type DataModelDeps = {
Expand Down
33 changes: 30 additions & 3 deletions src/features/instance/InstanceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ export type ChangeInstanceData = (callback: (instance: IInstance | undefined) =>

type InstanceStoreProps = Pick<InstanceContext, 'partyId' | 'instanceGuid'>;

const { Provider, useMemoSelector, useSelector, useLaxMemoSelector, useHasProvider } = createZustandContext({
const {
Provider,
useMemoSelector,
useSelector,
useLaxMemoSelector,
useHasProvider,
useLaxStore,
useLaxDelayedSelector,
} = createZustandContext({
name: 'InstanceContext',
required: true,
initialCreateStore: (props: InstanceStoreProps) =>
Expand Down Expand Up @@ -219,8 +227,6 @@ export const useLaxInstanceId = () => useLaxInstance((state) => state.instanceId
export const useLaxInstanceData = <U,>(selector: (data: IInstance) => U) =>
useLaxInstance((state) => (state.data ? selector(state.data) : undefined));
export const useLaxInstanceAllDataElements = () => useLaxInstance((state) => state.data?.data) ?? emptyArray;
export const useLaxInstanceDataElements = (dataType: string | undefined) =>
useLaxInstance((state) => state.data?.data.filter((d) => d.dataType === dataType)) ?? emptyArray;
export const useLaxInstanceStatus = () => useLaxInstance((state) => state.data?.status);
export const useLaxAppendDataElement = () => useLaxInstance((state) => state.appendDataElement);
export const useLaxMutateDataElement = () => useLaxInstance((state) => state.mutateDataElement);
Expand All @@ -229,6 +235,27 @@ export const useLaxInstanceDataSources = () => useLaxInstance((state) => state.d
export const useLaxChangeInstance = (): ChangeInstanceData | undefined => useLaxInstance((state) => state.changeData);
export const useHasInstance = () => useHasProvider();

/** Beware that in later versions, this will re-render your component after every save, as
* the backend sends us updated instance data */
export const useLaxInstanceDataElements = (dataType: string | undefined) =>
useLaxInstance((state) => state.data?.data.filter((d) => d.dataType === dataType)) ?? emptyArray;

export type DataElementSelector = <U>(selector: (data: IData[]) => U, deps: unknown[]) => U | typeof ContextNotProvided;
export const useLaxDataElementsSelector = (): DataElementSelector =>
useLaxDelayedSelector({
mode: 'innerSelector',
makeArgs: (state) => [state.data?.data ?? emptyArray],
});

/** Like useLaxInstanceAllDataElements, but will never re-render when the data changes */
export const useLaxInstanceAllDataElementsNow = () => {
const store = useLaxStore();
if (store === ContextNotProvided) {
return emptyArray;
}
return store.getState().data?.data ?? emptyArray;
};

export const useStrictInstanceRefetch = () => useSelector((state) => state.reFetch);
export const useStrictInstanceId = () => useSelector((state) => state.instanceId);
export const useStrictAppendDataElement = () => useSelector((state) => state.appendDataElement);
Expand Down
4 changes: 2 additions & 2 deletions src/features/validation/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ApplicationMetadata } from 'src/features/applicationMetadata/types';
import type { AttachmentsSelector } from 'src/features/attachments/AttachmentsStorePlugin';
import type { Expression, ExprValToActual } from 'src/features/expressions/types';
import type { DataElementSelector } from 'src/features/instance/InstanceContext';
import type { TextReference, ValidLangParam } from 'src/features/language/useLanguage';
import type { DataElementHasErrorsSelector } from 'src/features/validation/validationContext';
import type { FormDataSelector } from 'src/layout';
import type { ILayoutSets } from 'src/layout/common.generated';
import type { IData } from 'src/types/shared';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
import type { NodeDataSelector } from 'src/utils/layout/NodesContext';

Expand Down Expand Up @@ -225,7 +225,7 @@ export type ValidationDataSources = {
attachmentsSelector: AttachmentsSelector;
nodeDataSelector: NodeDataSelector;
applicationMetadata: ApplicationMetadata;
dataElements: IData[];
dataElementsSelector: DataElementSelector;
layoutSets: ILayoutSets;
dataElementHasErrorsSelector: DataElementHasErrorsSelector;
};
Expand Down
8 changes: 4 additions & 4 deletions src/features/validation/nodeValidation/useNodeValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAttachmentsSelector } from 'src/features/attachments/hooks';
import { DataModels } from 'src/features/datamodel/DataModelsProvider';
import { useLayoutSets } from 'src/features/form/layoutSets/LayoutSetsProvider';
import { FD } from 'src/features/formData/FormDataWrite';
import { useLaxInstanceAllDataElements } from 'src/features/instance/InstanceContext';
import { useLaxDataElementsSelector } from 'src/features/instance/InstanceContext';
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { Validation } from 'src/features/validation/validationContext';
import { implementsValidateComponent, implementsValidateEmptyField } from 'src/layout';
Expand Down Expand Up @@ -85,7 +85,7 @@ function useValidationDataSources(): ValidationDataSources {
const currentLanguage = useCurrentLanguage();
const nodeSelector = NodesInternal.useNodeDataSelector();
const applicationMetadata = useApplicationMetadata();
const dataElements = useLaxInstanceAllDataElements();
const dataElementsSelector = useLaxDataElementsSelector();
const layoutSets = useLayoutSets();
const dataElementHasErrorsSelector = Validation.useDataElementHasErrorsSelector();

Expand All @@ -97,7 +97,7 @@ function useValidationDataSources(): ValidationDataSources {
currentLanguage,
nodeDataSelector: nodeSelector,
applicationMetadata,
dataElements,
dataElementsSelector,
layoutSets,
dataElementHasErrorsSelector,
}),
Expand All @@ -108,7 +108,7 @@ function useValidationDataSources(): ValidationDataSources {
currentLanguage,
nodeSelector,
applicationMetadata,
dataElements,
dataElementsSelector,
layoutSets,
dataElementHasErrorsSelector,
],
Expand Down
10 changes: 5 additions & 5 deletions src/layout/AttachmentList/AttachmentListComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useMemo } from 'react';
import React from 'react';

import { AltinnAttachment } from 'src/components/atoms/AltinnAttachment';
import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider';
import { useLaxInstanceAllDataElements } from 'src/features/instance/InstanceContext';
import { useLaxInstanceData } from 'src/features/instance/InstanceContext';
import { useLaxProcessData } from 'src/features/instance/ProcessContext';
import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper';
import { DataTypeReference, filterDisplayPdfAttachments, getDisplayAttachments } from 'src/utils/attachmentsUtils';
Expand All @@ -15,12 +15,12 @@ export type IAttachmentListProps = PropsFromGenericComponent<'AttachmentList'>;
const emptyDataTypeArray: IDataType[] = [];

export function AttachmentListComponent({ node }: IAttachmentListProps) {
const instanceData = useLaxInstanceAllDataElements();
const currentTaskId = useLaxProcessData()?.currentTask?.elementId;
const dataTypes = useApplicationMetadata().dataTypes ?? emptyDataTypeArray;
const { dataTypeIds, textResourceBindings } = useNodeItem(node);

const attachments = useMemo(() => {
const attachments = useLaxInstanceData((data) => {
const instanceData = data.data ?? [];
const allowedTypes = new Set(dataTypeIds ?? []);
const includePdf =
allowedTypes.has(DataTypeReference.RefDataAsPdf) || allowedTypes.has(DataTypeReference.IncludeAll);
Expand Down Expand Up @@ -54,7 +54,7 @@ export function AttachmentListComponent({ node }: IAttachmentListProps) {

const otherAttachments = getDisplayAttachments(attachments);
return [...pdfAttachments, ...otherAttachments];
}, [currentTaskId, dataTypes, instanceData, dataTypeIds]);
});

return (
<ComponentStructureWrapper node={node}>
Expand Down
10 changes: 6 additions & 4 deletions src/layout/Subform/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class Subform extends SubformDef implements ValidateComponent<'Subform'>,
node: LayoutNode<'Subform'>,
{
applicationMetadata,
dataElements,
dataElementsSelector,
nodeDataSelector,
layoutSets,
dataElementHasErrorsSelector,
Expand All @@ -107,8 +107,8 @@ export class Subform extends SubformDef implements ValidateComponent<'Subform'>,

const validations: ComponentValidation[] = [];

const elements = dataElements.filter((x) => x.dataType === targetType);
const numDataElements = elements?.length ?? 0;
const elements = dataElementsSelector((d) => d.filter((x) => x.dataType === targetType), [targetType]);
const numDataElements = Array.isArray(elements) ? elements.length : 0;
const { minCount, maxCount } = dataTypeDefinition;

if (minCount > 0 && numDataElements < minCount) {
Expand All @@ -129,7 +129,9 @@ export class Subform extends SubformDef implements ValidateComponent<'Subform'>,
});
}

const subformIdsWithError = elements?.map((dE) => dE.id).filter((id) => dataElementHasErrorsSelector(id));
const subformIdsWithError = Array.isArray(elements)
? elements?.map((dE) => dE.id).filter((id) => dataElementHasErrorsSelector(id))
: [];
if (subformIdsWithError?.length) {
const validation: SubformValidation = {
subformDataElementIds: subformIdsWithError,
Expand Down
Loading