DEV Community

Cover image for Ditch Redux Already?! Building Scalable State with Recoil & React that Doesn’t Drive You Insane
Yevhen Kozachenko 🇺🇦
Yevhen Kozachenko 🇺🇦

Posted on • Originally published at ekwoster.dev

Ditch Redux Already?! Building Scalable State with Recoil & React that Doesn’t Drive You Insane

Ditch Redux Already?! Building Scalable State with Recoil & React that Doesn’t Drive You Insane

If you’ve ever wrestled with Redux in a large-scale React app and found yourself knee-deep in boilerplate hell and repetitive actions/reducers/selectors—this post is for you. We're about to investigate a state management solution that might finally let you sleep well at night: Recoil.

This isn’t just another “use Recoil instead of Redux” post—it's a guided breakdown of real-world patterns using Recoil to manage global and derived state, async data, and how all of it scales beautifully. We'll look at how Recoil fixes pain points Redux doesn’t even acknowledge exist, and why this could be the future of React state management.


💩 The State Management Sh*tshow

React brought us components. Then came Context for global state. Then came Redux for even more complex global state. But all of these solutions were either too local, too global, or too verbose.

Here's the pain:

  • You wanted one tiny global variable, and suddenly you wrote 50 lines of Redux boilerplate.
  • You needed to derive a value from your state, and now you’re drowning in re-selects and memoized selectors.
  • You needed a component to subscribe to part of the state, and unintentionally triggered rerenders on your entire app.

Enter: Recoil. The React team kind of quietly introduced it. Yet it solves 90% of these scenarios with barely any learning curve.


🚀 What is Recoil?

Recoil is a state management library for React created by folks at Facebook. It offers:

  • Atoms: units of shared state.
  • Selectors: derived or computed state with built-in memoization.
  • Async Selectors: handle async calls directly as state, avoiding ugly async-thunk middleware.
  • Fine-grained reactivity: only touches the components that need to update.

Conceptually, Recoil feels like having a shared version of React’s useState, but with data-driven dependency graphs.

Let's jump into some code to show how it really works.


💡 Recoil Crash Course (With Real-World Examples)

1. Install Recoil in 10 seconds

npm install recoil
Enter fullscreen mode Exit fullscreen mode

Then wrap your app in the provider:

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
);
Enter fullscreen mode Exit fullscreen mode

2. Global State (Atoms)

// atoms/todoListAtom.ts
import { atom } from 'recoil';

export const todoListState = atom({
  key: 'todoListState',
  default: [],
});
Enter fullscreen mode Exit fullscreen mode

Consume and update it in any component:

import { useRecoilState } from 'recoil';
import { todoListState } from './atoms/todoListAtom';

function AddTodo() {
  const [todos, setTodos] = useRecoilState(todoListState);

  const addTodo = () => {
    setTodos((old) => [...old, { id: Date.now(), text: 'New todo' }]);
  };

  return <button onClick={addTodo}>Add Todo</button>;
}
Enter fullscreen mode Exit fullscreen mode

3. Computed State (Selectors)

// selectors/todoStats.ts
import { selector } from 'recoil';
import { todoListState } from '../atoms/todoListAtom';

export const todoStatsState = selector({
  key: 'todoStatsState',
  get: ({ get }) => {
    const todos = get(todoListState);
    const total = todos.length;
    const completed = todos.filter((t) => t.completed).length;
    return {
      total,
      completed,
      percentCompleted: total === 0 ? 0 : (completed / total) * 100,
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

Use it anywhere, without triggering render on unrelated parts:

const stats = useRecoilValue(todoStatsState);
return <p>{stats.percentCompleted}% done</p>;
Enter fullscreen mode Exit fullscreen mode

4. Async Recoil State 🤯

// selectors/userProfile.ts
import { selector } from 'recoil';

export const userProfileQuery = selector({
  key: 'userProfileQuery',
  get: async () => {
    const response = await fetch('/api/me');
    const data = await response.json();
    return data;
  },
});
Enter fullscreen mode Exit fullscreen mode

And then:

const profile = useRecoilValueLoadable(userProfileQuery);

if (profile.state === 'loading') return <Spinner />;
if (profile.state === 'hasError') return <ErrorBoundary />;

return <h1>Hello, {profile.contents.name}</h1>;
Enter fullscreen mode Exit fullscreen mode

🧠 Why Recoil Just Clicks

Recoil feels like it belongs inside React rather than bolted onto it. Key benefits:

  • ✅ It respects React Mental Models.
  • ✅ Derived state is just data–not a separate world.
  • ✅ It makes fetch calls as easy to handle as local state.
  • ✅ It scales—nest atoms, combine selectors, no prop drilling, no redux store nightmare.

🛠️ A Practical Use Case: Building a Theming System

Let’s say you want your app to support light/dark themes, and share that across multiple components.

// atoms/themeAtom.ts
export const themeState = atom({
  key: 'themeState',
  default: 'light',
});
Enter fullscreen mode Exit fullscreen mode

Hook it up to a toggle:

function ThemeToggle() {
  const [theme, setTheme] = useRecoilState(themeState);

  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
    Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
  </button>;
}
Enter fullscreen mode Exit fullscreen mode

And theme your components accordingly:

function ThemedHeader() {
  const theme = useRecoilValue(themeState);

  return <header className={theme === 'dark' ? 'bg-gray-900 text-white' : 'bg-white text-black'}>Welcome</header>;
}
Enter fullscreen mode Exit fullscreen mode

No context, no custom providers. Just atomic state.


🙅‍♂️ When NOT to Use Recoil

While Recoil is great, it's not a universal hammer. Don’t use Recoil when:

  • ❌ You have a very simple app. Just use useContext().
  • ❌ You’re okay with server state being managed entirely by React Query.
  • ❌ You need to support a strict Flux architecture in an enterprise setting.

But when state shape grows, and async logic gets tied to global UI—you'll love Recoil.


🎯 Final Verdict

Cut the boilerplate, drop the middleware, and stop treating state like a second job. Recoil brings sanity to modern React.

You’ll:

  • Write less code
  • Get better performance
  • Think in data, not code gymnastics

Next time you reach for Redux in your React project, ask yourself: Do I want shipping speed and joy? Or do I want boilerplate, ceremony, and chaos?

Recoil isn’t experimental anymore—it's the spiritual successor to the state we were promised when React began.

Feel the Recoil 🔫


🧪 Resources

If you're still unsure—clone the example and play for 10 minutes. You’ll never look back.

💻 If you need help building scalable, modern frontend UIs with clean state management, we offer frontend development services.

Top comments (0)