React
Software Mile builds application front ends in React – the mainstream choice for interactive, maintainable web interfaces, and one your future developers will actually know how to work on.
Why React for the Front End
- Application UIs, dashboards, and portals with React and the modern ecosystem
- React Native for cross-platform mobile from a shared skill set
- Component libraries and design-system implementation for consistency at scale
- Integration with your APIs, auth, and back-end services
React is popular for a good reason: a huge talent pool and a mature ecosystem mean your app stays maintainable. Tell us about the interface you need.
Do You Actually Need React for This?
If the screens are mostly reading, forms, and navigation, a server-rendered application in the language your team already writes will usually be simpler to build and simpler to keep alive. React adds a build pipeline, a dependency tree, and a second place where state lives. That complexity is worth carrying when the interface has genuine client-side behavior: live filtering, drag and drop, editors, dashboards that update without a reload, or offline support.
The question is not whether React is good. It is whether this particular screen holds enough client-side state to justify a client-side framework at all. Plenty of applications answer no, and they are easier to run for it.
SPA, Server Rendering, or a Meta-Framework?
When the application lives behind a login and search engines never see it, a plain single-page build served as static files with an API behind it is often the simplest arrangement that works. Meta-frameworks with server rendering earn their complexity when public pages need to be indexed and fast on first load, or when you want data fetching to happen close to the database.
The trade-off is hosting and debugging. Server rendering means running a Node process in production with its own scaling, logging, and failure modes. Decide that deliberately, because it changes who has to be on call and what your operations team has to learn.
A third option is worth keeping in view: server-rendered pages from the back end you already have, with React used only on the handful of screens that need it. Mixing is less tidy than committing to one model, and it keeps the rewrite confined to the screens that actually needed one.
What Makes a React Codebase Expensive to Maintain?
The framework is rarely the culprit. The expense usually comes from decisions made in the first month and never revisited, then multiplied across every screen added since. The patterns below are the common ones, and each is easier to correct early than to unpick once the codebase has grown around it.
- Dependency sprawl. Every library added to solve a small problem becomes something to upgrade. A short list of well-maintained packages ages better than a clever one.
- Confusing server state with client state. Data that came from an API needs caching, refetching, and invalidation. A hand-rolled global store wrapped around it ends up reimplementing all three, badly.
- Components that know about the network. When fetching, formatting, and rendering share one file, nothing can be tested or reused.
- No shared component layer. Without one, every developer builds their own button, and a design change becomes an archaeology project.
- Skipped upgrades. Staying one version behind is routine. Falling several majors behind turns ordinary maintenance into a project with its own plan.
When Does React Native Make Sense, and When Does It Not?
React Native fits when the app is largely forms, lists, and API calls, when you need both platforms at once, and when your team’s skills are already in JavaScript. It fits poorly when the product depends on platform-specific behavior: heavy graphics, background processing, tight hardware integration, or a design that should feel native on each platform.
Shared code is not free either. You still need people who can read a stack trace on each platform and handle store submissions, and some features end up written twice anyway behind a shared interface. When the app is the product itself, native iOS and Android development is often the better route.
How Do You Move an Existing Front End to React Without a Rewrite?
Page by page. Pick one route that is genuinely painful, rebuild it in React behind the same URL and the same session, and ship it. Old and new front ends can coexist for a long time provided they agree on authentication and on where the API lives, which is usually where the real work turns out to be.
That agreement is worth settling before the first screen. Shared session handling, a consistent way to read the current user, and one story for error handling matter more to the pace of the migration than any decision about state management.
The approach keeps the business running and gives you an early read on whether the new front end is better before the rest of the budget is committed. Replace in slices, and keep the ability to stop.
Three Kinds of Slow, and the Measurement That Separates Them
Complaints that an interface is slow almost always resolve into one of three unrelated problems, and each has its own measurement. Bundle-bound is the time before anything appears at all: the browser has to download, parse, and execute the JavaScript before the first render happens, so the number that matters is the size of the entry chunk in the production build output. Render-bound is lag during interaction on a page that has already loaded: the React DevTools Profiler records each commit, attributes time to individual components, and reports why each one rendered. Data-bound is a shell that paints immediately and then sits empty: the browser network panel shows whether the requests started together or one after another.
The distinction matters because the fixes do not transfer between categories. Memoization does nothing for a large bundle. Code splitting does nothing for a chain of dependent requests. Caching on the API does nothing for a component that re-renders a thousand rows on every keystroke. Time spent optimizing the wrong category is not just wasted, it usually adds indirection that makes the real problem harder to see.
Measure a production build. Development React runs extra checks, keeps additional bookkeeping for the Profiler, and under Strict Mode deliberately renders components twice and mounts, unmounts, and remounts them so that unsafe effects show themselves. That behavior is useful during development and it makes development timings both slower and different in shape from what users get.
- A context provider whose value is a fresh object on every render re-renders every consumer beneath it, however unrelated that consumer is to what changed.
- An array index used as a key makes React reuse the wrong DOM node when a list reorders, which usually surfaces as state stuck on the wrong row rather than as slowness.
- An effect that fetches, inside a component that renders a child which also fetches, serializes two round trips that could have run as one.
- A date, icon, or charting library imported as a whole module can outweigh the application code itself, and the build's bundle report will say so in a line or two.
What the Browser Stops Doing Once Routing Moves to the Client
A client-side router replaces navigation with a history API call and a re-render. The address bar changes, but no document load happens, and a set of behaviors that came free with real navigation quietly stop. None of them are hard to restore. They are simply invisible until someone reports them, and the report almost never mentions routing.
Scroll is the first. Browsers restore scroll position on back and forward for real navigations, and with a client-side router that restoration fires against the old document before the new view has rendered, which drops the user in the wrong place or at the top. The usual answer is to set history.scrollRestoration to manual and record position per history entry yourself.
Error reporting is the second. Error boundaries catch errors thrown while rendering the subtree below them; they do not catch errors in event handlers, in async callbacks, during server rendering, or thrown by the boundary itself. Without global window error and unhandledrejection listeners reporting to the same place, a whole class of failures never reaches the dashboard and the only symptom is a button that stopped responding.
Forms are the third. Submitting through a real form element with a submit button gives keyboard submission, browser validation messaging, and correct behavior from password managers and autofill; a div with a click handler gives none of it, and calling preventDefault on the submit event keeps the semantics while stopping the navigation. What has to be rebuilt by hand beyond that is a short list, and an easy one to forget:
- Focus moved into the new view on navigation, because the browser no longer moves it and a keyboard user is left where they were.
- The document title, which changed on every real navigation and now changes only if the router updates it.
- An announcement that the view changed, usually a live region naming the new page, since assistive technology has no load event to react to.
- A genuine not-found response, because the server answered 200 for the shell long before the router decided the route matches nothing.
Version Boundaries That Decide What You Can Use
The React major a project is pinned to decides more than syntax. React 19 removed APIs that had carried deprecation warnings for years, so a codebase that ignored those warnings meets all of them at once during the upgrade rather than gradually before it. In application code the removals are mechanical to fix. The difficulty is that a dependency you do not control may still call them, and in JavaScript that surfaces at runtime, in one component, rather than at build time.
React Native sits on its own version line. Each React Native release pairs with a specific React version, so the two upgrades are not independent, and the New Architecture changes how native modules communicate with JavaScript. The binding constraint there is rarely the React code. It is the native dependency set, where a single unmaintained module can hold an entire app on an old release.
Installs make version conflicts visible in a way that is also easy to silence. npm 7 and later install peer dependencies automatically and stop with ERESOLVE when the graph cannot be satisfied; passing --legacy-peer-deps or adding an overrides block makes the install succeed without making the conflict untrue, and the mismatch returns later as a runtime error. TypeScript types ship as separate packages on their own release line, so the types can be a major apart from the React actually installed and the editor will report errors about code that runs correctly. Build tooling has its own floor as well, declared as an engines range. An upgrade inventory therefore covers:
- Which React major each third-party component library declares as a peer dependency, and whether an override is a real answer or a deferral.
- Which versions of @types/react and @types/react-dom match the React that is actually installed.
- Which removed APIs the code still calls, with ReactDOM.render, findDOMNode, string refs, and defaultProps on function components the usual ones.
- Which native dependencies a React Native app carries, and the New Architecture status of each.
- Which Node version the build tooling now requires, which tends to break in CI before anyone notices it locally.
Where the Front End Meets Auth and the API
Most of the integration effort in a React project lands on this boundary, and the first decision is where the session credential lives. Anything in localStorage or sessionStorage is readable by any script running on the origin, which turns a single cross-site scripting bug into token theft, and it persists until something explicitly clears it. A cookie marked httpOnly cannot be read from JavaScript at all, which removes that class of theft, but it puts the cookie's own rules into the path of every request the app makes.
Those rules are worth reading before the architecture is settled. SameSite=Lax means the cookie is not sent on cross-site fetch or XHR calls, only on top-level navigations, so a front end on one domain calling an API on another will not carry it. SameSite=None restores that, but browsers require the Secure attribute alongside it, which makes it HTTPS only. Credentialed cross-origin calls also require the API to answer preflight requests and to name the exact origin in Access-Control-Allow-Origin, because a wildcard is rejected once Access-Control-Allow-Credentials is true. Serving the front end and the API from the same site sidesteps all of it.
Token refresh is the part that usually gets built twice. When an access token expires, several in-flight requests fail at once, and a naive interceptor fires one refresh per failed request; the second refresh presents a token the first one already rotated, and the user is signed out at random intervals that nobody can reproduce. One refresh in flight at a time, with the other requests queued behind it and replayed afterward, is the whole fix, and it is far easier to write before there are twenty call sites.
One more agreement belongs to the same boundary, between the built files and whatever serves them. A client-side router owns paths the server has never heard of, so the host has to rewrite unknown paths to index.html, with an exception for the asset directory. Without that exception a request for a missing or renamed script returns the HTML shell with a 200 status and a text/html content type, and the browser reports a syntax error on an unexpected < character, which sends people hunting for a bug in JavaScript that is not there.
Frequently Asked Questions
Is React a framework or a library?
React is a library for rendering user interfaces from components, plus the rules for updating them when state changes. It ships no router, no data-fetching layer, no form handling, and no build tool, which is why two React projects can share a version number and look nothing alike. That is an advantage when the surrounding choices are made deliberately, and a liability when they accumulate one package at a time. A meta-framework is a bundle of those decisions made in advance, which is the actual trade when one is adopted.
What does React need to run on?
Node is required to build a React project even when nothing runs Node in production. The toolchain that compiles JSX, resolves modules, and emits the output files runs on developer machines and in CI, and the Node version it demands is a separate question from what serves the result. On mobile, React Native uses the same React and the same component model but not react-dom: there is no DOM, no document, and no CSS. Styling is a per-component subset of flexbox with no cascade, no media queries, and no stylesheet inheritance, so web components and existing CSS do not carry across even though the language and the mental model do.
Can React be added to an existing application without replacing it?
The handoff is the part worth planning. The surrounding page has to pass initial state into the React root, usually as data attributes on the mount element or a JSON block in the markup that the script reads at startup, and anything React changes has to travel back out through an event or an API call rather than by reaching into DOM the old page still owns. Watch the bundle while doing it: several small roots built separately each ship their own copy of the React runtime, so building them together, or loading React once and marking it external, is what keeps the total download from multiplying.
What has to already exist before front-end work can start?
An API contract that can be agreed and mocked: the endpoint list, field names and types, how pagination is expressed, and what an error response looks like. Work can begin against a mock of that contract long before the API is finished, but it cannot begin against an undecided one without producing rework at the point the shapes change. Designs need to be settled far enough that states other than the happy path are specified as well, because empty lists, loading, validation failures, long text, and small screens are where most of the real component work turns out to be.
What decisions does the customer have to make rather than the developers?
Whether any content has to be readable by a crawler or a link preview that does not execute JavaScript, since that answer changes the rendering model and is expensive to revisit later. Which accessibility standard the product is held to, because a target such as WCAG 2.1 AA implies keyboard operation, contrast ratios, and focus management that are built into components rather than added to them afterward. And who owns upgrades after handover: a React front end is not finished software, it is software with a dependency tree that keeps moving, and someone has to be accountable for keeping up with it.
How does a React build actually get deployed?
The build emits an index.html plus asset files whose names contain a content hash, and the two do not get the same caching policy. Hashed assets can be cached for a year because any change produces a new filename; index.html must not be, or returning visitors hold a stale shell pointing at chunks the deploy has already removed. Environment values are read at build time and baked into the output, so one build belongs to one environment unless the app fetches a small runtime config file at startup instead. Source maps deserve a decision too, since uploading them to the error tracker gives readable stack traces without publishing your source to anyone who opens developer tools.
What does implementing a design system in React actually involve?
Two artifacts rather than one: the tokens, meaning color, spacing, type scale, and radii, and the components that consume them. Tokens defined as CSS custom properties are readable by anything on the page, including parts of the product that are not React; tokens defined only as JavaScript objects are not, which matters as soon as a server-rendered section or an email template needs the same values. The component library also needs a version number and a release process, because every application depending on it pins a version, and a change that looks obvious to the library author is a breaking change to three consumers at once.