Frontend Frameworks

React

Maintained by
Meta
License
MIT
Version
18.x / 19 RC
Paradigm
Components + Hooks

React is the foundation of modern web frontend development. Its component model, unidirectional data flow, and ecosystem of supporting tools (React Query, Zustand, React Hook Form, Framer Motion) enable teams to build complex, maintainable user interfaces at scale. React 19 adds significant capabilities: the useOptimistic hook for optimistic UI updates, the use() API for promise-based data loading, and React Compiler for automatic memoization.

Axevate builds React applications across eCommerce frontends, SaaS dashboards, AI-powered interfaces, and marketing applications. Our frontend work consistently involves the same challenges: state management that doesn't become a maintenance burden, component architecture that scales with team size, and performance that holds up under real-world usage patterns. This page covers what we've learned about building React applications that are fast, maintainable, and testable.


1Component Architecture and Composition

The most consequential architectural decision in a React application is how components are structured. The single responsibility principle applies directly: each component should do one thing, do it well, and be testable in isolation. Components that handle data fetching, business logic, and presentation simultaneously become impossible to test and difficult to reuse. Separate data-fetching logic (custom hooks, React Query), presentational components (pure rendering based on props), and container components (composition and layout) as distinct layers.

Composition over inheritance is the React way. Rather than extending base components, compose smaller components into more complex ones. The children prop, render props, and component slots (passing components as props) are the primary composition patterns. Good composition makes it easy to swap out individual pieces - replace a loading spinner, change a form layout, swap an icon - without modifying parent components.

Context API is appropriate for global state that doesn't change frequently: theme, locale, user auth status. It's a poor choice for high-frequency state (form inputs, animations, cursor position) because every consumer re-renders when context changes. For high-frequency state, component-local state (useState) or a targeted state management library is more appropriate.

2State Management

The state management landscape has simplified. For server state (data fetched from APIs), React Query (TanStack Query) or SWR handle caching, background refetching, optimistic updates, and loading/error states more effectively than Redux-based solutions ever did. For global client state, Zustand is lightweight, intuitive, and avoids Redux's boilerplate overhead. For local UI state, useState and useReducer are sufficient for the majority of use cases.

The most common state management mistake is using global state for data that should be local. Every piece of state should live as close to where it's used as possible: first in the component itself, then in a parent component if siblings need it, then in a global store only if truly app-wide. Global state that starts as 'convenient' becomes a debugging nightmare as the application grows - mutations anywhere in the app affect components everywhere.

3Performance Patterns

React re-renders are the primary performance concern in complex applications. A component re-renders when its state changes, its props change, or its parent re-renders. Unnecessary re-renders compound in component trees where many components respond to shared state. React.memo, useMemo, and useCallback are the escape hatches, but they add complexity and should be applied to measured performance bottlenecks rather than as a default.

React Compiler (stable in React 19) automatically applies the equivalent of React.memo, useMemo, and useCallback where appropriate. Teams on React 19 can remove most manual memoization and let the compiler handle it. For teams on earlier React versions, the same result requires more deliberate architecture: stable references for functions and objects passed as props, proper dependency arrays in useEffect and useMemo, and key prop management in lists.


How We Use It in Practice

Real architectural problems across industries — and how we approach them.

SaaS / B2B Dashboard

React Query + Zustand: Eliminating the Over-Fetching Pattern in a Data-Heavy Dashboard

A B2B SaaS analytics dashboard was re-fetching all its data on every route change — including data that hadn't changed, like organization settings, user preferences, and reference lookup tables. With 12 API calls per navigation, page transitions felt slow and the backend was handling 5x the necessary load. The existing Redux store had grown to 40+ actions and a reducer file over 800 lines; the team was adding new features slowly because every data addition required changes in multiple places.

Our approach

Decomposed into two state layers: React Query for all server state (API data), Zustand for client UI state (selected filters, modal state, sidebar collapse). React Query's stale-while-revalidate pattern means data that was fetched within the last 5 minutes is served from cache immediately while a background refresh happens invisibly — navigation feels instant. Reference data (org settings, user roles, lookup tables) uses staleTime: Infinity and only invalidates on explicit mutations. The Redux store was removed entirely over 3 sprints of incremental migration. Result: page transition time from 900ms to 80ms, backend API call volume down 78%, and the team's feature velocity measurably improved by sprint 2 of the new architecture.

eCommerce / High-Traffic Storefront

React Performance Audit: Re-render Storm in a Product Listing Page

A headless Shopify storefront built in React had a product listing page that lagged noticeably when users applied filters — a 400ms freeze on mid-range mobile devices whenever a filter changed. The page rendered 48 product cards, each with a hover state, an add-to-cart button, and a wishlist toggle. The React DevTools Profiler showed that every filter change was triggering re-renders on all 48 cards simultaneously, even for cards whose data hadn't changed.

Our approach

Root cause: the product array was being recreated on every filter state change (new array reference), causing every card to see a new prop even when its individual product data was identical. Fix: memoized the filtered product array with useMemo, keyed cards by product ID (not array index), and wrapped the ProductCard component in React.memo. The add-to-cart handler was recreated on every render because it closed over cart state — moved to useCallback with stable cart dispatch reference. Post-optimization: filter interaction triggers re-renders only on the cards that were added or removed from the filtered set. Profiler-measured render time on filter change: from 380ms to 22ms. No React 19 or Compiler needed — the fix was in the data flow architecture, not the rendering infrastructure.

AI Application / Real-Time UI

Streaming LLM Responses in React: useOptimistic + Incremental State Updates

An AI writing assistant built in React showed a poor user experience when generating content: the page was blank until the full LLM response arrived (2-4 seconds), then showed the complete text at once. Attempts to stream the response caused jarring partial renders where incomplete sentences appeared, and the textarea scroll position would jump as content was appended. The team also needed the UI to show a 'working' state that was immediately responsive to user cancellation.

Our approach

Streaming via Server-Sent Events from a Next.js route handler that proxies the Anthropic streaming API. On the React side: useOptimistic for the immediate 'submitted' state (shows the user's prompt as sent before the API confirms); a useRef accumulator that collects incoming chunks without triggering renders; a useEffect that flushes the accumulated text to display state every 50ms (throttled re-renders) rather than on every token. Scroll behavior managed with a ref tracking whether the user had manually scrolled up — if they had, auto-scroll pauses; if they're at the bottom, the view tracks the growing content. Cancellation implemented via AbortController threaded through from the client cancel button to the fetch call in the route handler. Perceived responsiveness went from 'blank then instant full text' to 'smooth progressive appearance' with cancellation working at any point in generation.

FAQ

Next.js for almost everything. It provides SSR, SSG, routing, image optimization, and edge deployment capabilities that a standalone React SPA requires you to build yourself. The SPA model (Create React App or Vite) makes sense for: internal tools where SEO and initial load performance don't matter, applications fully behind authentication, or specific use cases where you need complete control over the rendering pipeline.

Ready to build with React?

Talk to Us