React Tooling 2024: Stop Using the Wrong Shit

The React ecosystem in 2024: 90% noise, 10% signal
I'm tired of "React best practices" articles written by people who've built three todo apps.
Here's what I actually use after shipping React to millions of users this year. And more importantly, what I stopped using and why.
Routing: Just Use Next.js Already
The Only Three Options That Matter
1. Next.js App Router โ
Stop debating. If you're starting a new project in 2024, use Next.js 14 with App Router. Yes, even for "SPAs."
Why? Because:
- Server Components actually work now
- Built-in data fetching that doesn't suck
- Route-based code splitting for free
- SEO without the PhD
// This is all you need. No setup. No config hell.
// app/products/[id]/page.tsx
export default async function Product({ params }) {
const product = await getProduct(params.id)
return <ProductView product={product} />
}
2. Tanstack Router ๐ค
Only if you absolutely can't use Next.js. It's what React Router should have been. Type-safe routes, built-in search params handling, actually good.
3. React Router โ
Legacy projects only. v6 broke everything good about v5. The docs still suck. Loader patterns are half-baked.
I migrated three apps away from React Router this year. Zero regrets.
Stop Calling React Query a Router
React Query (TanStack Query) isn't routing. It's data fetching. Different problem. Great library, wrong category.
State Management: You Probably Don't Need Redux
The Real Hierarchy (Use in This Order)
1. useState + Context ๐ฏ
You don't need a library for state management. You need to learn React.
// This handles 80% of "state management" needs
const ThemeContext = createContext()
const CartContext = createContext()
const UserContext = createContext()
// That's it. You're done.
2. Zustand โ
When Context gets messy (around 3-4 contexts), use Zustand. Not before.
// Entire store in 10 lines
const useStore = create((set) => ({
user: null,
cart: [],
setUser: (user) => set({ user }),
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
}))
That's it. No providers. No boilerplate. No DevTools extension breaking your Chrome.
3. Redux Toolkit ๐
Only if:
- You have complex async flows
- Time-travel debugging actually matters
- Your team already knows Redux
- You hate yourself
I removed Redux from two projects this year. Replaced with Zustand. Code deleted: ~2000 lines. Bugs fixed: 12.
The State Management Lies
"You need Redux for large apps" - False. Shopify uses Zustand. Netflix uses Zustand.
"Redux DevTools are essential" - I haven't opened them in 2 years. console.log works fine.
"Normalized state is critical" - For 99% of apps, just fetch fresh data. Your users have gigabit internet.
Server State: There's Only One Choice
TanStack Query. Period.
Stop evaluating options. Use TanStack Query (formerly React Query).
// This replaces 500 lines of useEffect garbage
function Products() {
const { data, error, isLoading } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minutes
})
if (isLoading) return <Skeleton />
if (error) return <Error />
return <ProductList products={data} />
}
Done. Caching, refetching, error handling, loading states, background updates. All handled.
Why Not RTK Query?
RTK Query only makes sense if you're already trapped in Redux. Otherwise it's like buying a forklift to move a couch.
Why Not SWR?
SWR is fine. TanStack Query is better. More features, better DevTools, actually maintained.
The Server State Rules
- Never store server data in client state - That's what the cache is for
- staleTime is your friend - 5 minutes is fine for most data
- Optimistic updates > loading spinners - Users don't care about your "data integrity"
// Optimistic update that actually works
const mutation = useMutation({
mutationFn: updateProduct,
onMutate: async (newProduct) => {
await queryClient.cancelQueries(['products'])
const previous = queryClient.getQueryData(['products'])
queryClient.setQueryData(['products'], old =>
old.map(p => p.id === newProduct.id ? newProduct : p)
)
return { previous }
},
onError: (err, newProduct, context) => {
queryClient.setQueryData(['products'], context.previous)
},
})
Forms: React Hook Form or GTFO
Formik is Dead
Literally. Check the GitHub. Last real update: 2021. Still using Formik? You're maintaining abandonware.
React Hook Form: The Only Option
// This is the entire form logic
const { register, handleSubmit, formState: { errors } } = useForm()
const onSubmit = async (data) => {
await saveUser(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: true })} />
{errors.email && <span>Email required</span>}
<button type="submit">Save</button>
</form>
)
No providers. No wrapper components. No 10KB of validation schemas.
Validation: Zod + RHF
Stop writing validation twice. Use Zod schemas for both frontend and backend:
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
})
// Frontend
const { register, handleSubmit } = useForm({
resolver: zodResolver(schema)
})
// Backend (same schema!)
const validated = schema.parse(req.body)
The Form Anti-Patterns
โ Controlled inputs for everything - RHF uses uncontrolled. It's faster. Deal with it.
โ Validation on every keystroke - Users aren't done typing. Chill.
โ Custom form abstractions - I've seen 5000-line form "frameworks." Just use RHF.
The Actually Hot Takes
CSS-in-JS is Dying
Styled-components and Emotion are dead. The bundle size isn't worth it.
Use Tailwind or CSS Modules. Yes, even for "dynamic" styles.
TypeScript Everywhere
If you're not using TypeScript in 2024, you're writing bugs on purpose. No exceptions.
Monorepos Are Usually Overkill
Unless you're Google, you don't need Nx/Turborepo/Lerna. You need better folder structure.
Testing: Write Less, Test Smarter
- E2E tests for critical paths only
- Integration tests for complex logic
- Unit tests for utilities
- Skip testing UI components (Storybook is enough)
Your 2024 React Stack
Stop overthinking. Use this:
{
"framework": "Next.js 14",
"styling": "Tailwind CSS",
"state": "Zustand (if needed)",
"server-state": "TanStack Query",
"forms": "React Hook Form + Zod",
"testing": "Vitest + Playwright",
"deployment": "Vercel"
}
That's it. This stack has handled everything from MVPs to enterprise apps.
Stop reading "best practices" and ship something.