- Server state → TanStack React Query.
- Validated types → Zod at the SDK boundary.
- Shared client state → Zustand (when state changes often) or React Context (for stable globals).
- Component shape → business logic in hooks, components for rendering UI.
Across all patterns
These apply regardless of which patterns you follow. For certification scoring on design and accessibility, see Flows custom apps quality guidelines.State: choosing the right tool
Use the table below to pick the right state tool. It prevents most over- and under-engineering before you read the patterns.| State type | Tool |
|---|---|
| Local to one component | useState |
| Stable global value (the CDF client, current user, feature flags) | React Context |
| Frequently-updating shared state (selections, filters, wizard drafts) | Zustand |
| Anything fetched from CDF | React Query — never copy into useState |
CogniteSdkProvider, so you thin-wrap its hook. Reach for Zustand when state changes frequently across many components or causes re-render storms.
Recommended folder shape
Group code by feature, not by technical type. Instead of top-levelhooks/, components/, and services/ folders that mix unrelated concerns, each feature gets its own folder with everything it needs: data, state, schema, and UI.
Patterns
This section covers areas where Flows custom apps can drift as they grow. Where a pattern has a clear counterpart to avoid, a side-by-side example shows both.Use React Query for all server state
A common pattern in growing Flows custom apps is fetching data withuseState + useEffect. Developers often add a manual loading flag, a cancelled ref to handle race conditions, and a counter to force refetches.
This hand-rolled loop is roughly what React Query does — except without caching, deduplication, background refresh, or retries. React Query ships with every Flows custom app; use it for all CDF reads and writes. See TanStack Query in Flows for how the scaffold wires data fetching to CDF.
useState is intended for local UI state (modals, form inputs, toggles). React Query handles server state and API data — never copy query data into useState, which opts out of background updates and causes the UI cache to drift from the server.Parse at the SDK boundary with Zod
CDF instance properties are loosely typed by nature. Without an explicit parse step, that looseness leaks into the rest of your Flows custom app: every component that needs a value ends up re-asserting the same assumptions withas casts and ad-hoc coercers.
Parse the response into a known shape once, at the point where it enters your Flows custom app, and downstream code can trust the type without re-asserting it.
Parse at exactly two seams: SDK → app (responses) and form → SDK (writes). Downstream functions receive a real typed value, not
unknown.Keep data access thin
It’s tempting to wrap CDF calls in service classes with interfaces for testability, but for a read-mostly UI, the overhead rarely pays off. An interface, a class, a ServicesContext, and a per-hook DepsContext to perform a singlelist() means touching four files for what should be one hook. A typed query hook is sufficient.
All CDF API calls must go through the SDK via
useCogniteSdk(). Never make raw fetch or axios requests to CDF endpoint URLs. The SDK handles token refresh, retries on rate-limit errors (429s), typed error responses, and consistent request tracing — none of which you get from a plain fetch.Choose the right home for client state
React Context and Zustand solve different problems. Context re-renders every consumer when its value changes, which suits values that rarely change. Zustand re-renders only components that subscribe to the changed slice, which suits values that change frequently. Use Context for app-shell configuration that is set once and read everywhere, for example, the CDF client, current user, and feature flags. Use Zustand for state that updates during user interaction, such as selections, filters, and wizard drafts, or that many unrelated components read.Start with
useState for local state. Lift to Context when a stable value needs to be available across the tree. Reach for Zustand when that value starts changing frequently, when you’re prop-drilling beyond 3 levels, or when Context re-renders become noticeable.Separate data from display
When components grow past a few hundred lines, they are usually doing too much: fetching their own data, computing derived values, and rendering in one place. That makes them harder to test in isolation and easier to break with unrelated edits. Extract data and logic into a view-model hook instead, and let the component focus on rendering.No business logic in UI components
This extends the view-model pattern above. When agents or developers add features quickly, business logic accumulates in components: auseQuery call here, a useEffect fetch there, an inline command handler that should live in a hook. Over time the component becomes the source of truth for data fetching and commands that belong in hooks. Keep components responsible for rendering and local UI state only; everything else belongs in a hook instead.
Modal open/close, hover, and resize observers are UI state. Keep them in the component.
Compute derived values during rendering
When a value can be computed from existing props or state, the temptation is to store a copy inuseState and keep it in sync with useEffect. This is unnecessary: React re-runs the component on every relevant state change anyway, and an inline const is always up to date. The useEffect version causes two renders: one with the stale value, one after the Effect fires, and adds a copy that can drift from its source.
Keep hooks single-purpose
As features grow, hooks often return unrelated values together: query data, derived fields, and callbacks in one object. A component that uses only one of those values still re-renders when any other value changes. Split hooks by responsibility, not by feature area.Signal to split: multiple components call the same hook but destructure completely different values.
Expose a narrow interface from hooks
Returning the fullquery or mutation object from a hook is tempting, but it leaks React Query internals to every caller. If you later change how data is fetched, such as swapping a query for a subscription or adding a transformation, you must update every component that destructures the raw object. Expose only what callers need: data, a loading flag, and named command functions.
Make impure dependencies injectable
Hooks that calluseQuery, useMutation, routing hooks, or SDK clients are impure. Hard-coded imports make them difficult to test without vi.mock. That workaround is path-coupled and not type-checked. It can also break silently when you rename or move a file.
There are two ways to make dependencies injectable. Choose based on the unit’s depth in the tree, and be consistent within a unit:
- For shallow units, add a
depsprop with real implementations as the default. Tests pass a typed substitute directly, no mocking needed. - For containers and nested trees, put dependencies in a
.context.tsxfile with real defaults so production code needs no Provider. Tests inject via<Context.Provider value={fakeDeps}>.
vi.mock remains appropriate for third-party and external libraries. For first-party code, prefer a typed seam. If you find yourself prop-drilling deps through intermediate components, switch to Context DI instead.Use Aura instead of inline styles
Inlinestyle={{}} objects bypass the aura/no-overriding-styles ESLint guardrail and guarantee visual drift from the Aura design system. They also signal that the component is doing too much: they often appear in oversized components that also mix in data fetching and business logic.
If you need to customize the appearance or styling, follow this order of priority:
- Use the component props and variants available in the design system.
- Use Tailwind utility classes for additional styling.
- If neither option is sufficient, raise a support ticket with the Aura team.
The
aura/no-overriding-styles ESLint rule enforces this automatically. Don’t disable or suppress it. Browse Aura primitives for layout and feedback components — for example Card and Badge — instead of styled HTML elements.Centralize your data model config
Do not scatter view IDs, space names, and property keys as string literals across your codebase. When you port a Flows custom app to another CDF project, or rename a space or view, you must find and replace every occurrence by hand. Keep all identifiers inconfig/model.ts and import from there instead. For batching and concurrency when you read and write instances, use the dm-limits-and-best-practices builder skill (About builder skills).
Handle errors in two layers
Without a defined error strategy, each component handles failures differently. Some store error text in state and render a red<pre>. Others fail silently with an empty screen. An uncaught render error can crash the whole iframe. Use two layers for most cases.
Quick reference
The following table summarizes the patterns on this page.| Stop doing | Start doing |
|---|---|
Fetching with useState + useEffect + fetchKey | All CDF reads and writes behind useQuery/useMutation in a dedicated hook |
Chained as casts at every call site | One Zod schema per view; parse() at both seams; type via z.infer |
Interface + class + two context layers for a single list() | Typed use* hook with useQuery inline |
| Prop-drilling shared state beyond 3 levels | Zustand for frequently-updating state; Context for stable globals |
| Components that fetch, compute, and render | View-model hook + thin Aura-built component; useQuery/useMutation only in hooks, never in .tsx files |
Hooks that return full query/mutation objects | Narrow return: { data, isLoading, addData } |
| One hook returning everything for a domain | Focused single-purpose hooks per concern |
vi.mock on first-party modules to bypass missing seams | Typed DI (Default-Props or Context) |
Inline style={{}} objects | Aura/Tailwind primitives enforced by aura/no-overriding-styles |
| View IDs and space names scattered as string literals | Central config/model.ts |
Per-component error strings / red <pre> | One <ErrorBoundary> at the shell + shared <ErrorState onRetry /> |
Related topics
- About Flows custom app features — Scaffolding, TanStack Query, auth, and what ships with every Flows custom app.
- Auth API —
connectToHostApp,HostAppAPI, and authenticated SDK access. - About spec-driven development — Checked-in specs for you and your agent.
- About builder skills — Skills including
design,dm-limits-and-best-practices, andsecurity. - Flows custom apps quality guidelines — Certification rubric including Aura consistency.
- Aura design system — Components, tokens, and patterns for Flows custom app UIs.
- Get started with Flows — Create a project and run it in CDF.