Chapter 13: The Ecosystem: 47 Ways to Build the Same Thing
The React ecosystem is like a buffet where every dish is spaghetti, but each one is tangled differently. You have 47 choices for every decision, each claiming to be “the best way,” and by the time you’ve chosen, three new options have appeared and yours is deprecated.
Create React App vs Next.js vs Gatsby vs Vite vs…
Remember when starting a React project was a choice?
Create React App (2016-2023, RIP)
npx create-react-app my-app
# 5 minutes later...
# 1,247 packages installed
Facebook’s official solution, now officially abandoned. “We recommend using a framework.” Thanks for nothing.
Next.js: When React Isn’t Complicated Enough
npx create-next-app@latest
# Would you like to use TypeScript? Yes
# Would you like to use ESLint? Yes
# Would you like to use Tailwind CSS? Yes
# Would you like to use `src/` directory? Yes
# Would you like to use App Router? Yes
# Would you like to customize import alias? Yes
#
# 20 questions later...
Next.js: Because client-side React wasn’t confusing enough, so let’s add:
- Server-side rendering
- Static generation
- API routes
- Middleware
- Edge functions
- Image optimization
- Font optimization
- Three different routing systems
Your simple blog now requires understanding distributed systems.
Gatsby: Static Sites with 400 Plugins
// gatsby-config.js
module.exports = {
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-sass',
'gatsby-plugin-image',
'gatsby-plugin-sharp',
'gatsby-transformer-sharp',
'gatsby-plugin-manifest',
'gatsby-plugin-offline',
// ... 47 more plugins
]
}
Want to add Google Analytics? There’s a plugin. Want to add a favicon? Plugin. Want to breathe? Plugin.
Vite: Actually Fast (For Now)
npm create vite@latest my-app -- --template react
# Done in 3 seconds
# Only 87 packages!
Vite is fast because it’s not made by Facebook. Give it time.
Remix: Full-Stack React (Again)
Because we needed another full-stack React framework. This one’s different though! It uses… checks notes… the web platform. Revolutionary.
CSS-in-JS: Because Regular CSS Wasn’t Complicated Enough
Remember when CSS was in CSS files? Those were dark times. Now we have:
styled-components: CSS in Your JavaScript
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'white'};
color: ${props => props.primary ? 'white' : 'blue'};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid blue;
border-radius: 3px;
&:hover {
background: ${props => props.primary ? 'darkblue' : 'lightblue'};
}
`;
Your CSS is now JavaScript that generates CSS. Performance? What’s that?
Emotion: styled-components but Different
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const style = css`
color: hotpink;
`
<div css={style}>This is hotpink</div>
Same idea, different syntax, incompatible with styled-components.
CSS Modules: Almost Normal
/* Button.module.css */
.button {
background: blue;
}
import styles from './Button.module.css';
<button className={styles.button}>Click</button>
Too simple. React developers rejected it.
Tailwind: Inline Styles with Extra Steps
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full transform transition-all duration-500 hover:scale-110 active:scale-95 shadow-lg hover:shadow-xl">
Button
</button>
Your entire CSS in your className. Adam Wathan is laughing all the way to the bank.
Form Libraries: Solving Problems You Didn’t Know You Had
HTML forms too simple? React’s got you covered:
React Hook Form: Forms with Hooks
import { useForm } from 'react-hook-form';
const { register, handleSubmit, watch, formState: { errors } } = useForm();
const onSubmit = data => console.log(data);
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: true })} />
{errors.name && <span>This field is required</span>}
</form>
Formik: Forms but Complicated
<Formik
initialValues=
validate={values => {
const errors = {};
if (!values.email) {
errors.email = 'Required';
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
}) => (
<form onSubmit={handleSubmit}>
{/* Your form here */}
</form>
)}
</Formik>
Remember <form onsubmit="submitForm()">
? Those were simpler times.
React Final Form: The Final Solution (It Wasn’t)
Another form library. Because we needed another one.
The NPM Audit Security Theater
$ npm install
found 1,746 vulnerabilities (1,698 low, 47 moderate, 1 high)
run `npm audit fix` to fix them
$ npm audit fix
fixed 0 of 1,746 vulnerabilities
1,746 vulnerabilities remain
$ npm audit fix --force
+ updated 247 packages
+ broke your entire application
+ good luck
Security theater at its finest. Those “vulnerabilities” are usually:
- Regex DoS in a dev dependency
- Prototype pollution in a package you don’t use
- A vulnerability in a test file
- Something that requires local access to exploit
But that red text sure is scary!
State Management Libraries: Redux Wasn’t Enough
Redux too complicated? Try these “simpler” alternatives:
MobX: Magic State
class TodoStore {
@observable todos = [];
@action addTodo = (text) => {
this.todos.push({ text, done: false });
}
}
Decorators that aren’t in JavaScript yet. Magic that works until it doesn’t.
Recoil: Facebook’s Second Attempt
const textState = atom({
key: 'textState',
default: '',
});
const charCountState = selector({
key: 'charCountState',
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
Atoms and selectors. Because Redux wasn’t abstract enough.
Zustand: Actually Simple (Suspicious)
const useStore = create(set => ({
bears: 0,
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
Wait, this is actually simple. There must be a catch. There’s always a catch.
Data Fetching: Because useEffect Wasn’t Enough
React Query (now TanStack Query)
const { data, error, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5,
cacheTime: 1000 * 60 * 10,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});
Caching, refetching, retrying, background updates… for a GET request.
SWR: React Query but by Vercel
const { data, error, isLoading } = useSWR('/api/user', fetcher)
Same idea, different API, incompatible ecosystem.
Apollo: GraphQL or Die
const { loading, error, data } = useQuery(gql`
query GetTodos {
todos {
id
text
completed
}
}
`);
Now you need to learn GraphQL too. Your REST API weeps.
Routing: Because URLs Are Hard
React Router: The Standard That Changes Every Version
v5:
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
v6:
<Routes>
<Route path="/about" element={<About />} />
<Route path="/" element={<Home />} />
</Routes>
Every major version breaks everything. It’s tradition.
TanStack Router: Type-Safe Routing
Because regular routing wasn’t complicated enough. Now with TypeScript!
Reach Router: Dead but Influential
Merged with React Router. RIP.
Animation Libraries: Making Things Move Slowly
Framer Motion: Animations with 100KB
<motion.div
initial=
animate=
exit=
transition=
>
Content
</motion.div>
100KB to fade in a div. Worth it?
React Spring: Physics-Based Animations
const styles = useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
})
Your animations now have mass and tension. Your bundle has mass too.
Component Libraries: Because HTML Is Hard
Material-UI (now MUI): Google’s Design, React’s Complexity
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
// 200KB of imports
Ant Design: Enterprise Components
Beautiful components that all look the same on every website.
Chakra UI: The Modular One
<Box as="section" pt={8} pb={12}>
<Stack spacing={8} direction="row">
<Feature icon={<Icon as={FcAssistant} />} />
</Stack>
</Box>
Everything is a Box. Divs are too mainstream.
The Package.json of Doom
After a year of development:
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^14.0.0",
"redux": "^4.2.0",
"react-redux": "^8.0.0",
"@reduxjs/toolkit": "^1.9.0",
"react-router-dom": "^6.8.0",
"axios": "^1.3.0",
"@tanstack/react-query": "^4.24.0",
"styled-components": "^5.3.0",
"@emotion/react": "^11.10.0",
"framer-motion": "^10.0.0",
"react-hook-form": "^7.43.0",
"yup": "^1.0.0",
"date-fns": "^2.29.0",
"lodash": "^4.17.21",
"@mui/material": "^5.11.0",
"react-beautiful-dnd": "^13.1.0",
"react-select": "^5.7.0",
"react-table": "^7.8.0",
"recharts": "^2.5.0",
// ... 147 more
},
"devDependencies": {
// ... another 200 packages
}
}
Your node_modules folder is now larger than Windows 95.
The Reality Check
The React ecosystem is:
- Fragmented: Multiple solutions for everything
- Incompatible: Libraries don’t work together
- Constantly changing: Today’s best practice is tomorrow’s anti-pattern
- Overwhelming: Decision fatigue is real
- Necessary: You need these libraries to be productive
The Path Forward
You’re going to use too many libraries. You’re going to have dependency conflicts. You’re going to spend more time configuring than coding. You’re going to question why you need 500MB of JavaScript to display a form.
But you’ll do it anyway because that’s the React way. Every problem has a library, and every library creates new problems that need more libraries.
In the next chapter, we’ll look at alternatives to React - frameworks that promise to be simpler, faster, better. Spoiler: they have their own ecosystems of complexity.
But first, take a moment to run npm ls --depth=0
and marvel at your 200 direct dependencies for what should be a simple web application.
The ecosystem isn’t just big - it’s infinite. And growing.