Home › Technical › Their stack

Their stack: Nx, AG Grid, TanStack

The three named tools in the JD where you don't have deep production experience. Over-prepare here. The bar isn't expert — it's "vocabulary fluent and ramps quickly." That's a few hours of reading away.

Honest framing. Don't pretend deep experience you don't have. The right answer is: "I've worked in similar architectures and understand the patterns these tools solve. I haven't shipped production with this exact toolchain, but I'd ramp quickly." Then prove it by talking fluently about the concepts below.

01Nx — monorepo & micro-frontend tooling

What Nx is, in one minute

Nx is a build system and dev toolchain for monorepos. It does three big things:

  1. Project graph — analyzes your code to know which projects depend on which.
  2. Affected commands — runs lint/test/build only on what changed since main, dramatically speeding CI.
  3. Computation caching — caches the results of any task locally and (optionally) remotely so you never recompute the same work twice.

Vocabulary you must know

TermWhat it is
workspaceThe Nx monorepo itself, rooted at nx.json
apps/Top-level deployable units (web apps, services). Cannot import from each other.
libs/Shared code. Can be imported by apps and other libs. Where the bulk of your code actually lives.
project graphThe dependency graph Nx builds by analyzing imports and config
tagsLabels on projects (e.g. scope:orders, type:ui)
module boundary rulesESLint rules using tags to enforce architecture (e.g. scope:orders can't import from scope:inventory)
affectedCompute the set of projects impacted by changes since a base commit
generatorsCode scaffolders. nx g @nx/react:lib feature-orders
executorsTask runners. The thing that actually runs your build, test, lint.
computation cacheCaches task outputs. Local by default, can be remote via Nx Cloud.
Module FederationWebpack feature Nx supports for runtime micro-frontend composition

Common Nx commands

nx build my-app                    # build a project
nx test my-app                     # test a project
nx lint my-app

nx affected -t test                # run tests on affected projects
nx affected -t build --parallel=3

nx graph                           # visualize the project graph
nx g @nx/react:app web-frontend    # generate an app
nx g @nx/react:lib feature-orders  # generate a lib

Library types (a useful Nx convention)

Standard taxonomy you'll see in Nx workspaces:

Module-boundary rules typically: feature can use anything, ui can use util only, data-access can use util, util imports nothing.

Likely questions

"Have you used Nx?"

Honest: "I haven't shipped a production Nx app, but I've worked in monorepo setups and the patterns Nx solves — affected builds, module boundaries, computation caching, library isolation — are familiar. I'd ramp on the specifics quickly."

"How would you organize features in our Nx workspace?"

I'd lean on the standard library-type convention — feature, ui, data-access, util — with tags enforcing module boundaries. For an ERP, I'd group by domain: orders, inventory, production, partners. Each gets its own feature lib and its own data-access lib. Cross-cutting UI components live in a shared UI lib. Module-boundary rules prevent orders from reaching into inventory internals — they go through public APIs only.

The "shared kernel" — types, contracts, design tokens — lives in a small util or shared lib that everything can depend on but that depends on nothing.

"Module Federation vs build-time composition?"

Build-time composition (separate libs, imported normally) is simpler and almost always sufficient. Module Federation is for when you genuinely need independent deployments — different teams shipping at different cadences, or wanting to update one app without redeploying the others.

Real costs of Module Federation: shared dependency management is hairy, type sharing across remotes needs extra work, debugging becomes harder, and you take on runtime fetch failures. I'd start with build-time composition and only adopt Module Federation when there's a deployment-independence story that justifies the cost.


02AG Grid — enterprise data grid

What AG Grid is, in one minute

AG Grid is the de facto enterprise React data grid. It's built for scale: tens of thousands of rows with virtualization, complex column definitions, sorting/filtering/grouping, inline editing, and customization at every level. Comes in Community (free, MIT-ish) and Enterprise (paid, with extra features like row grouping, pivoting, master/detail).

Core mental model

Three things you give AG Grid:

  1. Row data — the array of objects to display
  2. Column definitions — how to render each column
  3. Grid options — overall behavior (row models, selection, etc.)

Vocabulary

TermWhat it is
rowDataThe array of row objects
columnDefsArray of column configs — field, header, renderer, editor, width, etc.
defaultColDefDefaults applied to every column unless overridden
rowModelTypeclientSide (default — all rows in memory) vs serverSide / infinite / viewport (lazy-load chunks)
cellRendererCustom React component to render a cell
cellEditorCustom React component for inline editing
valueGetter / valueFormatterCompute / format cell value (useful when not 1:1 with field)
gridApi / columnApiImperative API. Refresh cells, get selection, export, scroll.
quickFilterBuilt-in global text filter across all visible columns
rowGroupingEnterprise feature. Group by a column.
masterDetailEnterprise. Expandable row with a sub-grid.
aggFuncAggregation in grouped/pivoted mode (sum, avg, count)

Tiny example

import { AgGridReact } from 'ag-grid-react';

const columnDefs = [
  { field: 'sku', headerName: 'SKU', sortable: true, filter: true },
  { field: 'name', headerName: 'Product', flex: 1 },
  { field: 'qty', headerName: 'Qty', editable: true },
  {
    field: 'status',
    headerName: 'Status',
    cellRenderer: StatusBadge, // custom React component
  },
];

<AgGridReact
  rowData={items}
  columnDefs={columnDefs}
  defaultColDef={{ resizable: true }}
  rowSelection="multiple"
  pagination
  paginationPageSize={50}
/>

The four things to know cold

1. Virtualization (default on)

AG Grid only renders rows visible in the viewport plus a buffer. That's why it can handle 100k rows. The cost: you can't rely on every row's DOM existing — measuring positions, attaching observers per row, etc. needs the grid API.

2. Row models

3. Cell renderers and editors

Common pattern: build a small component for any cell that's more than text. Status pills, dropdowns, action buttons. Use React components via the grid's React adapter.

function StatusBadge({ value }) {
  const color = value === 'active' ? 'green' : 'gray';
  return <span className={`badge bg-${color}-200`}>{value}</span>;
}

4. Grid API for imperative actions

const onGridReady = (params) => {
  gridApiRef.current = params.api;
};

// Later:
gridApiRef.current.exportDataAsCsv();
gridApiRef.current.getSelectedRows();
gridApiRef.current.refreshCells({ force: true });

Likely questions

"Have you used AG Grid?"

"I haven't shipped AG Grid specifically, but I've worked with virtualized data grids and the patterns around them — column-based config, custom cell renderers, server-side data sources, performance considerations for large rows. The conceptual model is familiar. I'd come up to speed quickly."

"How would you handle 100k rows in a grid?"

For AG Grid specifically, virtualization handles the rendering side automatically. The remaining concerns are:

  • Don't load 100k rows into memory if you don't have to. Use server-side row model — fetch chunks, push grouping and aggregation to the backend.
  • Memoize cell renderers. Big lists punish unnecessary re-renders.
  • Watch column count too — wide grids with custom renderers can hurt as much as long ones.
  • Defer non-critical cell content — formatting heavy values, fetching avatars — until visible.
  • Profile in DevTools. Don't optimize without numbers.
"How would you make a column with editable status that updates the backend?"

I'd put a custom cellEditor on the column — likely a select dropdown component. On commit, fire the mutation via TanStack Query with an optimistic update. If it fails, roll back the cache and toast the user. The grid API has events like onCellValueChanged that fire on commit — that's the hook into your mutation flow.


03TanStack Query — server state

What it solves

"Server state" is fundamentally different from client state — async, cached, can become stale, can fail, lives on the server. Treating it like client state (Redux store, useState) means you reinvent caching, deduplication, refetching, and stale handling badly. TanStack Query (formerly React Query) handles all of that.

Mental model

Every server resource has a query key (a serializable array). The library deduplicates fetches with the same key, caches the result, and re-uses it across all components that ask for it.

const { data, isLoading, error } = useQuery({
  queryKey: ['orders', { status: 'open' }],
  queryFn: () => fetchOrders({ status: 'open' }),
});

Key concepts

TermWhat it is
queryKeyThe cache key. Array of serializable values.
queryFnThe async function that fetches
staleTimeHow long data is "fresh" — within this, no refetch on mount/focus
gcTime (formerly cacheTime)How long unused data stays in cache before being garbage-collected
useMutationFor writes — creates, updates, deletes
invalidateQueriesMark queries stale, triggering refetch
setQueryDataImperatively update the cache (for optimistic updates)
onSuccess / onError / onSettledLifecycle callbacks on mutations
placeholderDataShow cached/old data while a new query loads — great for paginated UIs
QueryClientThe instance you configure once at the app root

Mutations + optimistic updates

const queryClient = useQueryClient();

const updateOrder = useMutation({
  mutationFn: patchOrder,
  onMutate: async (newOrder) => {
    // optimistic: cancel in-flight, snapshot, set new
    await queryClient.cancelQueries({ queryKey: ['orders'] });
    const previous = queryClient.getQueryData(['orders']);
    queryClient.setQueryData(['orders'], (old) =>
      old.map(o => o.id === newOrder.id ? newOrder : o)
    );
    return { previous };
  },
  onError: (err, newOrder, context) => {
    queryClient.setQueryData(['orders'], context.previous); // roll back
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['orders'] });
  },
});

Likely questions

"What's the difference between staleTime and gcTime?"

staleTime = how long the data is considered fresh. Within staleTime, no automatic refetches on mount or window focus. Default is 0 — meaning always-stale, refetch eagerly.

gcTime = how long unused data lingers in the cache before being garbage-collected. Default is 5 minutes. After it's collected, the next query starts fresh from a loading state.

Tuning: increase staleTime for data that doesn't change often (user profile, config) so you don't waste fetches. Lower it for fast-moving data.

"How do you handle pagination?"

Two patterns:

  • Cursor / offset pagination — each page is its own query, with the page number in the queryKey. Use placeholderData: keepPreviousData to avoid flicker.
  • Infinite scrolluseInfiniteQuery with getNextPageParam. Pages stack into a list. Combine with react-virtual for performance.
"How do you keep query keys consistent across the app?"

Use a query key factory — a typed object that builds keys for each domain. Single source of truth, prevents typos, makes invalidation precise.

const orderKeys = {
  all: ['orders'] as const,
  lists: () => [...orderKeys.all, 'list'] as const,
  list: (filters: Filters) => [...orderKeys.lists(), filters] as const,
  details: () => [...orderKeys.all, 'detail'] as const,
  detail: (id: string) => [...orderKeys.details(), id] as const,
};

Now invalidateQueries({ queryKey: orderKeys.lists() }) invalidates every list query without touching detail queries.

"How do you handle errors and retries?"

By default TanStack Query retries 3 times with exponential backoff. Configure per-query or globally on the QueryClient. For specific status codes you don't want to retry (404, 401), customize the retry function:

retry: (failureCount, error) => {
  if (error.status === 404) return false;
  return failureCount < 3;
}

Surface errors via the error field on the hook, plus a global error handler on the QueryClient for monitoring (Sentry / App Insights).


04Base UI — headless component primitives

The JD mentions "Base UI (where appropriate)". Base UI is the unstyled, accessible primitives library from the MUI team (it grew out of MUI Base / MUI's "headless" layer). Think of it as a competitor to Radix UI or Headless UI: it ships behavior + accessibility + keyboard handling for things like Dialog, Popover, Select, Tabs, Menu, Slider, Tooltip — but no styles. You bring the styles (Tailwind, CSS modules, whatever).

Why teams pick it

Tiny example (Dialog)

import { Dialog } from '@base-ui-components/react/dialog';

<Dialog.Root>
  <Dialog.Trigger className="btn">Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Backdrop className="fixed inset-0 bg-black/40" />
    <Dialog.Popup className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg">
      <Dialog.Title>Confirm</Dialog.Title>
      <Dialog.Description>Are you sure?</Dialog.Description>
      <Dialog.Close className="btn">Cancel</Dialog.Close>
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

Likely questions

"Why would you reach for Base UI over building your own?"

Accessibility is the unsexy work that's easy to get wrong — focus management in a dialog, roving tabindex in a menu, ARIA roles in a tab list. Base UI ships that correctly. Building it yourself is weeks of edge cases and screen-reader testing. Reaching for a primitive library is the leverage move; you keep your design freedom because nothing's pre-styled.

"Base UI vs. Radix vs. Headless UI?"

All three solve the same problem — accessible, unstyled primitives. Differences are mostly API style and ecosystem fit. Base UI sits closer to MUI's design philosophy and has a slightly more compositional API. Radix is the most mature and has the largest community. Headless UI is the smallest surface area and ships from the Tailwind team. For a team already on the MUI side of the world, Base UI is the natural choice.


05Routing — likely TanStack Router

The JD says "Modern routing solution" without naming one. Given they're already TanStack-aligned for Query, the most likely answer is TanStack Router. Worth knowing the basics either way; you can ask in stage 3.

Why TanStack Router gets picked

Mental shift from React Router

If you've used React Router v6/v7, the conceptual jump is small — nested routes, outlets, loaders, actions all map. The two big differences: TanStack Router is more aggressive about types (you'll feel it on every Link and navigate call), and search-params-as-state is a much bigger deal than React Router treats it.

Likely questions

"What router are you familiar with?"

"I've used React Router across several projects. I haven't shipped TanStack Router in production but I'm familiar with the model — the type-safe params, the loader pattern, the search-param-as-state idea. If you're using it I'd ramp quickly because the conceptual model is the same; the win is the type safety. Are you on TanStack Router, or did you go a different direction?"

(Then: actually ask. The JD is vague on purpose or by accident; either way, it's a legitimate clarifier.)

"How would TanStack Router and TanStack Query work together?"

The clean pattern: each route's loader calls queryClient.ensureQueryData(...) with the same query key the component will later use via useQuery. The loader guarantees the data is in cache before the route renders, so the component never shows a loading state on first paint. After mount, useQuery takes over for refetches and invalidations. You get instant navigation + proper caching + a single source of truth for what data the route needs.


06How they fit together at Omnesoft

Likely architecture you'll be working in:

  • Nx workspace — apps/ for the deployable web frontends, libs/ for features and shared code, with module-boundary rules enforcing architecture.
  • TanStack Query in libs/data-access-* — domain-specific hooks that wrap the .NET / FastEndpoints API, with typed responses generated from OpenAPI.
  • AG Grid as the workhorse for inventory, orders, partners, scheduling — the core ERP UIs.
  • React + TypeScript + Tailwind as the surface layer — design system in a libs/ui lib, likely composing Base UI primitives for accessibility-heavy bits (dialogs, popovers, menus).
  • A modern router — most likely TanStack Router given the rest of the stack — wiring route loaders into TanStack Query for instant navigation.
  • Playwright for E2E flows. Jest + RTL for unit/integration. GitHub Actions running affected builds with Nx Cloud caching.

When asked, describe it back to them like this. They'll feel understood.