Skip to content

Minor performance optimizations for large repeating groups #3606

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

olemartinorg
Copy link
Contributor

@olemartinorg olemartinorg commented Aug 13, 2025

Description

  • Removing process() from createQueryContext(), as this can be covered by functionality already in tanstack query, or the code could simply be added to the query function instead. This caused spooky stuff to happen, as I initially removed the useMemo() from the wrapper provider inside createQueryContext(), and I think that caused react compiler to optimize a bit too much. With a useMemo() it works fine, but without it the app would never load.
  • Removing usages of delayedSelector in some utility functions for RepeatingGroup. These functions does not really need to be re-created every time some state changes - they can just get the fresh state every call. There's still an issue in RepGroupHooks.useGetFreshRowsWithButtons() which also could just get fresh state instead of re-rendering after it has been called, but that would mean I'd have to create a non-reactive variant of expression data sources - which will be much easier after we move to one big store.
  • Preventing a forced child re-render in RepeatingGroupTable.tsx because an object was passed instead of just the index and uuid properties that rarely changes.
  • Fixing memoization issues in useTableComponentIds(), as a dependency to useMemo() was not itself memoized.
  • Preventing unnecessary work in useRowWithExpressions(). The result rarely changes, but since data sources may force a re-render when a new row is added, this memo would be re-generated a bit often, and would cause more work to be done downstream.

These are all minor stuff, but in sum they further boost the performance when testing the ssb/ra0485-01 app. Along with pagination in that repeating group, I can just about keep working with ~370 rows.

Related Issue(s)

Verification/QA

  • Manual functionality testing
    • I have tested these changes manually
    • Creator of the original issue (or service owner) has been contacted for manual testing (or will be contacted when released in alpha)
    • No testing done/necessary
  • Automated tests
    • Unit test(s) have been added/updated
    • Cypress E2E test(s) have been added/updated
    • No automatic tests are needed here (no functional changes/additions)
    • I want someone to help me make some tests
  • UU/WCAG (follow these guidelines until we have our own)
    • I have tested with a screen reader/keyboard navigation/automated wcag validator
    • No testing done/necessary (no DOM/visual changes)
    • I want someone to help me perform accessibility testing
  • User documentation @ altinn-studio-docs
    • Has been added/updated
    • No functionality has been changed/added, so no documentation is needed
    • I will do that later/have created an issue
  • Support in Altinn Studio
    • Issue(s) created for support in Studio
    • This change/feature does not require any changes to Altinn Studio
  • Sprint board
    • The original issue (or this PR itself) has been added to the Team Apps project and to the current sprint board
    • I don't have permissions to do that, please help me out
  • Labels
    • I have added a kind/* and backport* label to this PR for proper release notes grouping
    • I don't have permissions to add labels, please help me out

Summary by CodeRabbit

  • New Features

    • Displays a “saving” loader while form data is being saved, preventing premature interaction.
  • Bug Fixes

    • More consistent page ordering and task navigation items across forms.
    • Smoother loading/error handling for layouts and text resources.
  • Refactor

    • Streamlined data fetching by moving post-fetch processing into query fetches.
    • Improved performance and stability for repeating groups via memoization and simpler props.
  • Chores

    • Simplified context APIs to reduce configuration complexity.

@olemartinorg olemartinorg self-assigned this Aug 13, 2025
@olemartinorg olemartinorg added kind/other Pull requests containing chores/repo structure/other changes backport-ignore This PR is a new feature and should not be cherry-picked onto release branches labels Aug 13, 2025
@olemartinorg olemartinorg moved this to 👷 In Progress in Team Apps Aug 14, 2025
@olemartinorg
Copy link
Contributor Author

/publish

Copy link
Contributor

github-actions bot commented Aug 15, 2025

PR release:

  • <link rel="stylesheet" type="text/css" href="https://altinncdn.no/toolkits/altinn-app-frontend/4.21.0-pr.1929.performance-fixes2.531bb13f/altinn-app-frontend.css">
  • <script src="https://altinncdn.no/toolkits/altinn-app-frontend/4.21.0-pr.1929.performance-fixes2.531bb13f/altinn-app-frontend.js"></script>

⚙️ Building...
✅ Done!

Base automatically changed from refactor/performance-fixes to main August 19, 2025 08:39
@olemartinorg olemartinorg force-pushed the refactor/performance-fixes2 branch 2 times, most recently from 8862d98 to b83a7f7 Compare August 19, 2025 09:05
@olemartinorg olemartinorg changed the base branch from main to chore/navigate-to-component-fix August 19, 2025 09:05
Copy link

coderabbitai bot commented Aug 22, 2025

📝 Walkthrough

Walkthrough

Refactors QueryContext to remove processing and UI overrides, returning raw query data. Moves per-provider data transformations into queryFns and adjusted typings. Adds a mutation-based loading gate in DataModelsProvider. Refactors RepeatingGroup internals to read directly from the Zustand store, changes RowToDisplay props, and tightens memoization in several hooks.

Changes

Cohort / File(s) Change summary
Core QueryContext API refactor
src/core/contexts/queryContext.tsx
Removed context-level process, DisplayError, and Loader props; QueryContextProps now exposes only query and optional shouldDisplayError. createQueryContext generic reduced to <QD, Req> and provides raw query data via useQuery. Adjusted imports and provider implementation.
Providers: move transformations into queryFns
src/features/form/layoutSets/LayoutSetsProvider.tsx, src/features/form/layoutSettings/LayoutSettingsContext.tsx, src/features/language/textResources/TextResourcesProvider.tsx
Removed per-context process hooks; each query's queryFn now performs previous processing: taskNavigation id injection (uuidv4) in LayoutSets, full LayoutSettings → ProcessedLayoutSettings conversion in LayoutSettings, and ITextResourceResult → TextResourceMap conversion in TextResources. Typings updated to reflect processed return types.
Layouts memoization
src/features/form/layout/LayoutsContext.tsx
useLayoutQuery now memoizes augmented data (adds lookups via makeLayoutLookups) with useMemo, returning { ...utils, data } to preserve shape.
DataModels mutation gate
src/features/datamodel/DataModelsProvider.tsx
Adds useMutationState check for pending saveFormData mutations and a useRef guard; renders Loader with reason save-form-data until mutation state has been observed settled. No public API changes.
RepeatingGroup: store access & props change
src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx, src/layout/RepeatingGroup/Table/RepeatingGroupTable.tsx
Removed delayed inner selectors; replaced with direct Zustand store.getState() reads. RowToDisplay now takes index and uuid props instead of a row object; call sites updated accordingly. Public APIs unchanged.
RepeatingGroup: memoization & hooks
src/layout/RepeatingGroup/useTableComponentIds.ts, src/layout/RepeatingGroup/utils.ts
useTableComponentIds memoizes computed children list with useMemo. useRowWithExpressions switching to useMemoDeepEqual for deep-equality memoization of dependencies. No exported API changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/performance-fixes2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Base automatically changed from chore/navigate-to-component-fix to main August 22, 2025 13:52
Ole Martin Handeland added 10 commits August 22, 2025 15:52
… either select() in tanstack query or by wrapping the query function.
…memoization. In ssb/ra0485-01 each click on the 'add new row' button seems to re-render this and re-render the entire nodes context.
…t have to re-render these tools for them them to work correctly next time they're called.
…useMemo() call that was here before. Leaving it in seems to fix things, but I suspect there is a react-compiler issue at the core of this. I tried opting out of react-compiler, but that didn't work as expected either.
…ntirely and clear all state. Saving is trigged on unmount, but that saving isn't always done when re-mounting again, so DataModelsProvider would end up getting outdated initial form data for the next render.
…ng the entire application if the component never re-renders. Also, if you're unlucky you'll suddenly get the loader again during normal form filling because this re-rendered and suddenly found a (normal) mutation was in progress.

Fixing this seems to make both tests stable again:
- components.ts 'should be possible to change language back and forth and reflect the change in the UI', which was the initially flaky one that made me write this code
- validation.ts 'Required field validation should be visible on submit, not on blur', which now failed sometimes because you can't blur a field that doesn't have focus (and it didn't have focus any more because the loader flashed by for a short while when mutating).
@olemartinorg olemartinorg force-pushed the refactor/performance-fixes2 branch from b15be75 to b1a2275 Compare August 22, 2025 13:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (1)

84-91: Type-safe omitUndefined implementation (fixes potential TS errors in strict mode).

The current reduce initializer is {}, which is not typed; indexing into it with arbitrary keys will error under stricter TS settings.

Apply this diff:

-function omitUndefined<T extends { [K: string]: unknown }>(obj: T): Partial<T> {
-  return Object.keys(obj).reduce((newObj, key) => {
-    if (obj[key] !== undefined) {
-      newObj[key] = obj[key];
-    }
-    return newObj;
-  }, {});
-}
+function omitUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
+  const result: Partial<T> = {};
+  (Object.keys(obj) as Array<keyof T>).forEach((key) => {
+    if (obj[key] !== undefined) {
+      result[key] = obj[key];
+    }
+  });
+  return result;
+}
🧹 Nitpick comments (9)
src/layout/RepeatingGroup/utils.ts (1)

149-176: useMemoDeepEqual here should cut re-renders; ensure the returned object stays immutable.

This change will stabilize the reference when the computed payload is deep-equal across renders. One caveat: if a consumer mutates the returned object, future deep-equality checks may become unreliable. If you’ve had accidental mutations in this area before, consider freezing the result in dev to catch it early.

Optional dev-only guard you could wrap behind a DEV flag:

   return {
     ...row,
     hidden: evalBool({ expr: hiddenRow, ...baseProps }) ?? false,
     textResourceBindings: trb
       ? {
           edit_button_close: evalString({ expr: trb.edit_button_close, ...baseProps }),
           edit_button_open: evalString({ expr: trb.edit_button_open, ...baseProps }),
           save_and_next_button: evalString({ expr: trb.save_and_next_button, ...baseProps }),
           save_button: evalString({ expr: trb.save_button, ...baseProps }),
         }
       : undefined,
     edit: edit
       ? {
           alertOnDelete: evalBool({ expr: edit.alertOnDelete, ...baseProps }),
           editButton: evalBool({ expr: edit.editButton, ...baseProps, defaultValue: true }),
           deleteButton: evalBool({ expr: edit.deleteButton, ...baseProps, defaultValue: true }),
           saveAndNextButton: evalBool({ expr: edit.saveAndNextButton, ...baseProps }),
           saveButton: evalBool({ expr: edit.saveButton, ...baseProps, defaultValue: true }),
         }
       : undefined,
   };
+  // if (__DEV__) Object.freeze(result);
src/features/form/layout/LayoutsContext.tsx (1)

60-72: Memoizing lookups off the query result is solid; minor dependency/type tidy-ups recommended.

  • The lookups only depend on layouts, so using utils.data?.layouts as the dependency avoids unnecessary recompute if other fields change.
  • Consider renaming data to dataWithLookups for clarity in the return expression.
  • Today LayoutContextValue doesn’t include lookups, yet the context value returned from useLayoutQuery now does. TS will infer the wider type via createQueryContext, but making lookups explicit in LayoutContextValue improves consistency.

Apply within this hunk:

-  const data = useMemo(() => {
-    if (utils.data) {
-      return {
-        ...utils.data,
-        lookups: makeLayoutLookups(utils.data.layouts),
-      };
-    }
-
-    return utils.data;
-  }, [utils.data]);
-
-  return { ...utils, data };
+  const dataWithLookups = useMemo(() => {
+    const layouts = utils.data?.layouts;
+    if (!layouts) {
+      return utils.data;
+    }
+    return { ...utils.data, lookups: makeLayoutLookups(layouts) };
+  }, [utils.data?.layouts]);
+
+  return { ...utils, data: dataWithLookups };

Outside this hunk (for typing clarity), you can update the interface:

// near line 24
import type { LayoutLookups } from 'src/features/form/layout/makeLayoutLookups';

export interface LayoutContextValue {
  layouts: ILayouts;
  hiddenLayoutsExpressions: IHiddenLayoutsExternal;
  expandedWidthLayouts: IExpandedWidthLayouts;
  lookups: LayoutLookups; // new
}
src/features/datamodel/DataModelsProvider.tsx (1)

260-271: One-time pending-save gate prevents 409s after unmount-triggered autosave — confirmed. All occurrences of the ['saveFormData'] mutationKey in FormDataWrite.tsx (lines 124, 177, 352, 603) and DataModelsProvider.tsx (line 260) align perfectly.

Nitpick (optional): if FormDataWrite can ever unmount without unmounting its parent (e.g. BlockUntilLoaded), this one-shot guard won’t catch a subsequent pending save on the same mount. Consider resetting hasPassedMutationCheck.current while initial data/schema loading is still in progress to cover that scenario.

src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx (1)

474-484: Pagination page calculation uses current store page — minor micro-optimization possible.

Current approach is correct. If you want to shave a read, cache store.getState() in a local variable in getPaginationState; impact is negligible, so up to you.

-  const getPaginationState = () =>
-    producePaginationState(store.getState().currentPage, pagination, getState().visibleRows);
+  const getPaginationState = () => {
+    const { currentPage } = store.getState();
+    return producePaginationState(currentPage, pagination, getState().visibleRows);
+  };
src/core/contexts/queryContext.tsx (1)

39-54: Minor readability nits: avoid shadowing and unnecessary memo.

  • The variable name rest is used both for props (outer scope) and for the query result spread (inner scope). Rename the inner one to queryRest to avoid confusion.
  • value = useMemo(() => data, [data]) doesn’t add value here; passing data directly is equivalent.
-  function WrappingProvider({ children }: PropsWithChildren) {
-    const { data, isPending, error, ...rest } = useQuery();
-    const enabled = 'enabled' in rest && !required ? rest.enabled : true;
-    const value = useMemo(() => data, [data]);
+  function WrappingProvider({ children }: PropsWithChildren) {
+    const { data, isPending, error, ...queryRest } = useQuery();
+    const enabled = 'enabled' in queryRest && !required ? queryRest.enabled : true;
@@
-    return <Provider value={enabled ? (value ?? defaultValue) : defaultValue}>{children}</Provider>;
+    return <Provider value={enabled ? (data ?? defaultValue) : defaultValue}>{children}</Provider>;
src/features/form/layoutSets/LayoutSetsProvider.tsx (1)

17-29: Avoid random ids in queryFn to prevent remounts on refetch; prefer deterministic ids (preserve-existing or index-based).

Generating new UUIDs on every fetch will change React keys for taskNavigation items and can cause unnecessary unmount/mount cycles and lost local state, undermining the performance goal of this PR. Use stable, deterministic ids instead and only synthesize an id when one is missing.

Apply this diff to make ids deterministic and to preserve any existing id:

-    queryFn: async () => {
-      const layoutSets = await fetchLayoutSets();
-      if (layoutSets?.uiSettings?.taskNavigation) {
-        return {
-          ...layoutSets,
-          uiSettings: {
-            ...layoutSets.uiSettings,
-            taskNavigation: layoutSets.uiSettings.taskNavigation.map((g) => ({ ...g, id: uuidv4() })),
-          },
-        };
-      }
-      return layoutSets;
-    },
+    queryFn: async () => {
+      const layoutSets = await fetchLayoutSets();
+      if (layoutSets?.uiSettings?.taskNavigation) {
+        return {
+          ...layoutSets,
+          uiSettings: {
+            ...layoutSets.uiSettings,
+            taskNavigation: layoutSets.uiSettings.taskNavigation.map((g, i) =>
+              // Preserve any existing id; otherwise generate a stable one
+              (('id' in g && g.id) ? g : { ...g, id: String(i) })
+            ),
+          },
+        };
+      }
+      return layoutSets;
+    },

Additionally remove the now-unused uuid import:

-import { v4 as uuidv4 } from 'uuid';

Optional follow-up: the same id-injection pattern appears in LayoutSettingsContext.tsx; consider centralizing it in a small helper to avoid drift.

src/features/language/textResources/TextResourcesProvider.tsx (1)

41-46: Optional: consider centralizing common TanStack Query options.

If you’re adopting a shared queryOptions helper elsewhere (per repo guidelines), you could wrap the key/fn here too for consistency. No functional change required.

src/layout/RepeatingGroup/useTableComponentIds.ts (2)

15-26: Defensive optional chaining to avoid potential crashes if children is missing.

If component.children were ever undefined (misconfig, partial load), calling .map would throw. Optional chain safely falls back to emptyArray (already in place).

Apply this diff:

-      component.children.map((id) => {
+      component.children?.map((id) => {
         if (multiPage) {
           const [, childId] = id.split(':', 2);
           return layoutLookups.getComponent(childId);
         }
 
         return layoutLookups.getComponent(id);
-      }) ?? emptyArray,
+      }) ?? emptyArray,

37-44: Micro-optimization: avoid O(n^2) sort by precomputing header indexes.

When tableHeaders is present, using indexOf in the comparator repeatedly is O(n^2). Precompute a map once to keep sort O(n log n).

Apply this diff:

-    if (tableHeaders) {
-      ids.sort((a, b) => {
-        const aIndex = tableHeaders.indexOf(a);
-        const bIndex = tableHeaders.indexOf(b);
-        return aIndex - bIndex;
-      });
-    }
+    if (tableHeaders) {
+      const indexMap = new Map(tableHeaders.map((h, i) => [h, i] as const));
+      ids.sort((a, b) => (indexMap.get(a)! - indexMap.get(b)!));
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 15a54bf and b1a2275.

📒 Files selected for processing (10)
  • src/core/contexts/queryContext.tsx (4 hunks)
  • src/features/datamodel/DataModelsProvider.tsx (2 hunks)
  • src/features/form/layout/LayoutsContext.tsx (2 hunks)
  • src/features/form/layoutSets/LayoutSetsProvider.tsx (1 hunks)
  • src/features/form/layoutSettings/LayoutSettingsContext.tsx (2 hunks)
  • src/features/language/textResources/TextResourcesProvider.tsx (2 hunks)
  • src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx (3 hunks)
  • src/layout/RepeatingGroup/Table/RepeatingGroupTable.tsx (2 hunks)
  • src/layout/RepeatingGroup/useTableComponentIds.ts (1 hunks)
  • src/layout/RepeatingGroup/utils.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Avoid using any and unnecessary type casts (as Type) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options via queryOptions

Files:

  • src/layout/RepeatingGroup/utils.ts
  • src/layout/RepeatingGroup/useTableComponentIds.ts
  • src/features/form/layout/LayoutsContext.tsx
  • src/features/form/layoutSets/LayoutSetsProvider.tsx
  • src/features/language/textResources/TextResourcesProvider.tsx
  • src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx
  • src/features/datamodel/DataModelsProvider.tsx
  • src/layout/RepeatingGroup/Table/RepeatingGroupTable.tsx
  • src/core/contexts/queryContext.tsx
  • src/features/form/layoutSettings/LayoutSettingsContext.tsx
🧬 Code graph analysis (10)
src/layout/RepeatingGroup/utils.ts (1)
src/hooks/useStateDeepEqual.ts (1)
  • useMemoDeepEqual (27-41)
src/layout/RepeatingGroup/useTableComponentIds.ts (1)
src/features/expressions/expression-functions.ts (1)
  • component (426-462)
src/features/form/layout/LayoutsContext.tsx (1)
src/features/form/layout/makeLayoutLookups.ts (1)
  • makeLayoutLookups (115-167)
src/features/form/layoutSets/LayoutSetsProvider.tsx (1)
src/queries/queries.ts (1)
  • fetchLayoutSets (240-240)
src/features/language/textResources/TextResourcesProvider.tsx (4)
src/core/queries/useQueryWithStaleData.ts (1)
  • useQueryWithStaleData (13-39)
src/utils/network/sharedNetworking.ts (1)
  • HttpClientError (5-5)
src/queries/queries.ts (1)
  • fetchTextResources (285-286)
src/core/contexts/queryContext.tsx (1)
  • createQueryContext (33-61)
src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx (4)
src/utils/layout/hooks.ts (1)
  • useExternalItem (16-22)
src/features/validation/callbacks/onGroupCloseValidation.ts (1)
  • useOnGroupCloseValidation (15-57)
src/layout/RepeatingGroup/utils.ts (1)
  • RepGroupHooks (63-231)
src/utils/layout/types.ts (1)
  • BaseRow (12-15)
src/features/datamodel/DataModelsProvider.tsx (1)
src/core/loading/Loader.tsx (1)
  • Loader (15-38)
src/layout/RepeatingGroup/Table/RepeatingGroupTable.tsx (2)
src/layout/layout.ts (1)
  • IDataModelBindings (61-64)
src/utils/layout/types.ts (1)
  • BaseRow (12-15)
src/core/contexts/queryContext.tsx (1)
src/core/contexts/context.tsx (1)
  • createContext (45-89)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (5)
src/core/queries/usePrefetchQuery.ts (1)
  • QueryDefinition (4-4)
src/core/contexts/AppQueriesProvider.tsx (1)
  • useAppQueries (58-58)
src/queries/queries.ts (1)
  • fetchLayoutSettings (244-245)
src/core/contexts/delayedContext.tsx (1)
  • delayedContext (11-38)
src/core/contexts/queryContext.tsx (1)
  • createQueryContext (33-61)
🔇 Additional comments (8)
src/layout/RepeatingGroup/utils.ts (1)

7-7: Good call switching to deep-equality memoization.

Importing useMemoDeepEqual is appropriate here given the heavy, mostly-stable object you return per row.

src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx (2)

360-375: Replacing delayed selectors with store.getState() at call time fixes stale reads.

Grabbing editingAll/editingId/editingNone and computing index from fresh rows inside the returned function ensures validation runs against the latest state.


412-427: Toggling edit state now reads the live editingId — good.

Using store.getState() avoids capturing a stale editingId in the handler closure.

src/core/contexts/queryContext.tsx (2)

21-24: Type surface is cleaner; default shouldDisplayError hook is a nice touch.

No issues here.


33-38: API simplification confirmed – no call sites pass removed props
Ran the provided ripgrep checks across the repository and found zero occurrences of process, DisplayError, Loader in createQueryContext calls, nor any old three-generic-parameter usages. The API change is safe to approve.

src/features/language/textResources/TextResourcesProvider.tsx (1)

25-29: LGTM: moving transformation into queryFn aligns with the new QueryContext API and keeps types precise.

The queryKey includes the selectedLanguage, the conversion is tight, and useQueryWithStaleData preserves UX during transitions. No issues spotted.

src/features/form/layoutSettings/LayoutSettingsContext.tsx (1)

21-22: LGTM: processing moved to queryFn with a typed ProcessedLayoutSettings result.

The queryKey scopes to layoutSetId and the processing path is single-sourced via processData.

src/layout/RepeatingGroup/Table/RepeatingGroupTable.tsx (1)

150-152: LGTM: passing only index/uuid reduces prop churn and child re-renders.

This narrows RowToDisplay’s props to the minimal stable surface, which should cut unnecessary renders and aligns with the PR goal.

Also applies to: 174-176

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (2)

46-55: Throwing Error objects: LGTM, resolves earlier concern.

Switching from string throws to Error preserves stack traces and matches createQueryContext’s error handling.


62-75: Stabilize generated ids to prevent remounts on refetch (perf).

Recomputing uuidv4() on each refetch assigns new keys to groups/taskNavigation, causing remounts and extra work downstream. Preserving an existing id and synthesizing a deterministic fallback (e.g., index) keeps keys stable across refetches and aligns with the PR’s performance objectives.

Apply this diff:

   return {
     order,
-    groups: 'groups' in settings.pages ? settings.pages.groups.map((g) => ({ ...g, id: uuidv4() })) : undefined,
+    groups:
+      'groups' in settings.pages
+        ? settings.pages.groups.map((g, i) =>
+            (('id' in g && g.id) ? g : { ...g, id: String(i) })
+          )
+        : undefined,
     pageSettings: omitUndefined({
       autoSaveBehavior: settings.pages.autoSaveBehavior,
       expandedWidth: settings.pages.expandedWidth,
       hideCloseButton: settings.pages.hideCloseButton,
       showExpandWidthButton: settings.pages.showExpandWidthButton,
       showLanguageSelector: settings.pages.showLanguageSelector,
       showProgress: settings.pages.showProgress,
-      taskNavigation: settings.pages.taskNavigation?.map((g) => ({ ...g, id: uuidv4() })),
+      taskNavigation: settings.pages.taskNavigation?.map((g, i) =>
+        (('id' in g && g.id) ? g : { ...g, id: String(i) })
+      ),
     }),
     pdfLayoutName: settings.pages.pdfLayoutName,
   };

If you adopt this, you can remove the uuid import at the top of the file as a follow-up.

🧹 Nitpick comments (2)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (2)

17-23: Good shift: processing moved into queryFn; consider queryOptions for consistency.

This aligns with the PR’s goal (do work once in the query pipeline). If your codebase uses TanStack’s queryOptions to centralize defaults (staleTime/gcTime/select/meta), consider wrapping these options to keep behavior consistent with other queries.

Apply this diff if you want to standardize on queryOptions:

-import { useQuery } from '@tanstack/react-query';
+import { useQuery, queryOptions } from '@tanstack/react-query';
@@
-export function useLayoutSettingsQueryDef(layoutSetId?: string): QueryDefinition<ProcessedLayoutSettings> {
+export function useLayoutSettingsQueryDef(layoutSetId?: string): QueryDefinition<ProcessedLayoutSettings> {
   const { fetchLayoutSettings } = useAppQueries();
-  return {
-    queryKey: ['layoutSettings', layoutSetId],
-    queryFn: async () => processData(layoutSetId ? await fetchLayoutSettings(layoutSetId) : null),
-  };
+  return queryOptions({
+    queryKey: ['layoutSettings', layoutSetId] as const,
+    queryFn: async () => processData(layoutSetId ? await fetchLayoutSettings(layoutSetId) : null),
+  });
 }

36-44: Prefer returning groups: undefined instead of [] when settings are missing.

ProcessedLayoutSettings declares groups as optional. Returning [] here but undefined elsewhere makes consumers handle two shapes for “no groups”. Using undefined consistently typically avoids unnecessary renders and guards like arr.length === 0.

Proposed change:

   if (!settings) {
     return {
       order: [],
-      groups: [],
+      groups: undefined,
       pageSettings: {},
       pdfLayoutName: undefined,
     };
   }

Please verify no downstream code relies on [] specifically.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b1a2275 and a296a45.

📒 Files selected for processing (1)
  • src/features/form/layoutSettings/LayoutSettingsContext.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Avoid using any and unnecessary type casts (as Type) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options via queryOptions

Files:

  • src/features/form/layoutSettings/LayoutSettingsContext.tsx
🧬 Code graph analysis (1)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (5)
src/core/queries/usePrefetchQuery.ts (1)
  • QueryDefinition (4-4)
src/core/contexts/AppQueriesProvider.tsx (1)
  • useAppQueries (58-58)
src/queries/queries.ts (1)
  • fetchLayoutSettings (244-245)
src/core/contexts/delayedContext.tsx (1)
  • delayedContext (11-38)
src/core/contexts/queryContext.tsx (1)
  • createQueryContext (33-61)
🔇 Additional comments (2)
src/features/form/layoutSettings/LayoutSettingsContext.tsx (2)

57-61: Confirm behavior: groups without “order” are silently ignored when deriving page order.

The flatten will drop any group lacking an order array. If that’s intended, all good. If not, consider logging a warning (or appending those pages in declared order).


79-84: Context wiring: LGTM.

Typed createQueryContext<ProcessedLayoutSettings, true> and deferring transformation to the query layer is consistent with the new pattern and reduces Provider churn.

Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-ignore This PR is a new feature and should not be cherry-picked onto release branches kind/other Pull requests containing chores/repo structure/other changes
Projects
Status: 👷 In Progress
Development

Successfully merging this pull request may close these issues.

1 participant