
Best Practices for React.js Development

React.js is mature enough that most teams have strong opinions about it. This post is for engineers who have shipped production React apps and want a sharper checklist, not a tutorial.
How Should You Actually Structure a React Project?
Folder structure arguments consume more engineering time than they should. The answer is: structure by feature, not by file type.
The classic components/, hooks/, utils/ split feels organised until a feature spans eight folders and you're jumping around constantly. Group by domain instead.
src/
features/
auth/
AuthForm.tsx
useAuth.ts
auth.api.ts
auth.types.ts
dashboard/
...
shared/
components/
hooks/
utils/
shared/ holds genuinely reusable code. If something lives in features/auth/ and you're tempted to import it from features/dashboard/, move it to shared/ consciously rather than creating cross-feature imports.
Co-locate tests and stories
Keep AuthForm.test.tsx and AuthForm.stories.tsx next to AuthForm.tsx. Searching for test files in a mirrored directory tree is friction you don't need. Co-location also makes it obvious when a component has no tests.
Barrel files: use with caution
Barrel files (index.ts that re-exports everything) make imports cleaner but slow down bundlers and TypeScript's language server on large projects. If you use them, do so at the feature boundary only, not inside a feature folder.
State Management: What Actually Belongs Where?
This is the question teams get wrong most often. Not every state needs to be global.
| State type | Right tool |
|---|---|
| Server data (fetched, cached) | TanStack Query (React Query) v5 |
| Global UI state (modals, themes) | Zustand or React Context |
| Local ephemeral state | useState / useReducer |
| URL-driven state (filters, pagination) | URL search params via React Router |
Using Redux for server cache state is the most common over-engineering mistake in React codebases built before 2021. TanStack Query handles cache invalidation, background refetching, stale-while-revalidate, and optimistic updates. Redux does none of that by default. If you're still reaching for Redux Toolkit for API state, benchmark whether you actually need it.
Zustand weighs roughly 1 kB gzipped and has a simpler mental model than Redux for UI state. It doesn't require a Provider and doesn't push you towards boilerplate. That said, if your team knows Redux and your app is already on it, rewriting is rarely worth the cost.
Performance: Where Are the Real Bottlenecks?
The React DevTools Profiler is your first stop, not your last. Before optimising, measure.
Memoisation is not free
React.memo, useMemo, and useCallback add complexity and have their own comparison cost. They pay off when:
- A child component is expensive to render
- A value passed as a dependency to
useEffectwould otherwise cause unnecessary re-runs - A callback is passed to a child component that is itself memoised
Wrapping every component in React.memo by default is not a performance strategy. It's noise that makes diffs harder to read.
Code splitting
Use React.lazy with Suspense for route-level splits. A typical single-page app's initial bundle should stay under 200 kB gzipped. Run webpack-bundle-analyzer or Vite's rollup-plugin-visualizer before you ship and after every major dependency addition. Bundle bloat is almost always third-party libraries, not your own code.
Concurrent features in React 18
useTransition and useDeferredValue are worth knowing for input-heavy UIs, search interfaces, and large list filters. They let React defer non-urgent renders so the UI stays responsive. They're not a substitute for virtualisation on very long lists; use @tanstack/react-virtual or react-window for that.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
What Does a Solid Component API Look Like?
A component's API is its prop interface. Treat it like a public API.
Keep prop surfaces small. If a component accepts 12 props, it's almost certainly doing too much. Split it.
Prefer composition over configuration. Rather than a Button that accepts a leftIcon prop and a rightIcon prop and a isLoading prop and a tooltipText prop, use a slot pattern or render props where complexity grows.
// Prefer
<Button>
<Spinner />
Save changes
</Button>
// Over
<Button isLoading loadingText="Saving..." />
The slot pattern is more flexible and doesn't require the Button component to know about Spinner.
Type your props strictly. Avoid any. Use discriminated unions when a component behaves meaningfully differently based on a prop value.
type Props =
| { variant: 'link'; href: string }
| { variant: 'button'; onClick: () => void };
This catches misuse at compile time rather than runtime.
Testing: What's Worth Writing and What Isn't?
Unit tests for pure utility functions are cheap to write and cheap to maintain. Write them.
Component tests with React Testing Library should test behaviour, not implementation. Query by accessible role and label, not by class name or data-testid unless there's no accessible alternative. RTL's philosophy aligns with what users actually experience.
Integration tests that cover a critical user journey (sign-in, checkout, form submission) catch regressions that unit tests miss entirely. These are worth the investment.
End-to-end tests with Playwright or Cypress are expensive to maintain. Keep the suite small and focused on paths where a failure would be catastrophic. Running 400 e2e tests on every PR is usually a sign that confidence in unit and integration tests is low.
Don't test third-party libraries. Mock network calls with MSW (Mock Service Worker) rather than mocking fetch directly. MSW intercepts at the network level, which means your component and data-fetching layer both get exercised.
Conclusion
The patterns above are not new. Most of them have been stable for two to three years. The value is in applying them consistently across a team, not in discovering them.
If you're auditing an existing codebase, start with the bundle analyser and the React DevTools Profiler. Data from those two tools will surface the highest-impact problems faster than any architectural review.
If you're starting fresh, lock down your folder structure, state management choices, and testing conventions in the first sprint. Retrofitting these decisions onto a 50,000-line codebase is painful.
FAQ
Is React still the right choice for new projects in 2024? React's ecosystem depth is hard to beat. It has the widest hiring pool, the most mature tooling, and active maintenance from Meta. Next.js 14 and React Server Components extend its range to full-stack. For most web applications, it remains a sensible default unless you have specific constraints that push you towards Svelte or SolidJS.
Should I use TypeScript with React? Yes. TypeScript catches a significant class of bugs at compile time, makes refactoring safer, and improves IDE support substantially. The setup cost is low with Vite or Create React App. The only honest exception is very small throwaway prototypes.
When is Context the wrong choice for state management? React Context re-renders every consumer when the context value changes. If you put frequently-updating state (a mouse position, a live data feed) into Context, you'll see performance problems. Use Context for low-frequency state like themes, auth, or feature flags. For anything that updates often, use Zustand or a selector-based library.
What's the difference between useMemo and useCallback?
useMemo memoises a computed value. useCallback memoises a function reference. Use useCallback when passing a function to a memoised child component so the child doesn't re-render on every parent render. Use useMemo when a calculation is genuinely expensive and its inputs change infrequently.
How do you manage forms in React without excessive re-renders? React Hook Form is the standard answer. It uses uncontrolled inputs internally, which means it doesn't trigger a re-render on every keystroke. For complex, multi-step forms with dynamic fields, pair it with Zod for schema validation. Formik is the older alternative but re-renders more aggressively and has been largely superseded.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
