The React ecosystem offers thousands of libraries, making it overwhelming to choose the right tools for your project. This guide covers the essential libraries I recommend for different use cases, updated for 2024.
React state management falls into two main categories:
🏆 Top Pick: Zustand
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Why Zustand?
Alternatives:
🏆 Top Pick: TanStack Query (formerly React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function Posts() {
const queryClient = useQueryClient();
const {
data: posts,
isLoading,
error,
} = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then((res) => res.json()),
});
const createPost = useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Why TanStack Query?
Alternatives:
🏆 Top Pick: Tailwind CSS
function Card({ title, description }) {
return (
<div className="max-w-sm rounded-lg border border-gray-200 bg-white p-6 shadow-md dark:border-gray-700 dark:bg-gray-800">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{title}
</h5>
<p className="mb-3 font-normal text-gray-700 dark:text-gray-400">
{description}
</p>
<button className="inline-flex items-center rounded-lg bg-blue-700 px-3 py-2 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Read more
</button>
</div>
);
}
Why Tailwind CSS?
🏆 Top Pick: Styled Components
import styled from 'styled-components';
const Button = styled.button`
background: ${(props) => (props.primary ? '#007bff' : 'white')};
color: ${(props) => (props.primary ? 'white' : '#007bff')};
border: 2px solid #007bff;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
&:hover {
background: ${(props) => (props.primary ? '#0056b3' : '#f8f9fa')};
}
`;
function App() {
return (
<div>
<Button>Default Button</Button>
<Button primary>Primary Button</Button>
</div>
);
}
Modern Alternatives:
🏆 Top Pick: React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z
.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data) => {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(data),
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('password')} type="password" placeholder="Password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing up...' : 'Sign Up'}
</button>
</form>
);
}
Why React Hook Form?
Alternatives:
🏆 Top Pick: Radix UI
import * as Dialog from '@radix-ui/react-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
function DialogDemo() {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="Button violet">Edit profile</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
<Dialog.Title className="DialogTitle">Edit profile</Dialog.Title>
<Dialog.Description className="DialogDescription">
Make changes to your profile here. Click save when you're done.
</Dialog.Description>
<Dialog.Close asChild>
<button className="IconButton" aria-label="Close">
×
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Why Radix UI?
🏆 Top Picks:
🏆 Top Pick: Framer Motion
import { motion, AnimatePresence } from 'framer-motion';
function AnimatedList({ items }) {
return (
<ul>
<AnimatePresence>
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{ duration: 0.3 }}
>
{item.text}
</motion.li>
))}
</AnimatePresence>
</ul>
);
}
Alternatives:
🏆 Top Pick: Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('increments counter when button is clicked', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
const counter = screen.getByText('0');
await user.click(button);
expect(screen.getByText('1')).toBeInTheDocument();
});
Complete Testing Stack:
The React ecosystem continues to evolve rapidly. Focus on libraries that:
Remember: start simple and add complexity only when needed. Many projects can get by with just React's built-in state management and a good CSS framework.