If you’ve worked on a React app that has survived more than one product manager and at least two redesigns, chances are there’s a big Redux store quietly sitting at the center of it.
You know the pattern:
A single store wired up in index.js.
A pile of reducers stitched together with combineReducers.
Action types that read like an incident log: USER_FETCH_SUCCESS, CART_ADD_ITEM, MODAL_TOGGLE, FEATURE_X_TEMP_FLAG.
A state tree where server data, UI flags, and random one-off booleans all live side by side.
For a while, this felt like the way to do React state management. Redux gave us structure, predictability, and very cool devtools. But the React ecosystem didn’t stand still. We got hooks, Suspense, server components, query libraries for server state, and now signals and lightweight stores that promise finer-grained reactivity and less boilerplate.
So here’s the uncomfortable question a lot of teams are asking in 2025:
“If Redux isn’t the default pattern anymore, how do we move away from it without rewriting our entire React app?”
This post is a practical answer to that question. We’ll look at what signals and modern stores bring to the table, how they differ from Redux, and—most importantly—how you can incrementally migrate complex state from Redux to signals/stores without a big-bang rewrite.
Why Redux Feels Heavy in 2025
Let’s be fair to Redux for a moment. When it arrived, it solved real problems:
It offered a single source of truth for your React app.
It enforced pure functions for state changes via reducers.
It gave us time-travel debugging and an explicit log of everything that happened.
It made complex state management at scale possible when React itself was much more limited.
If you’re building a large, enterprise-style React app, those things still matter. Redux is battle-tested, well-documented, and widely understood.
But day-to-day, many teams now feel the pain:
Boilerplate fatigue – For simple changes, you’re touching actions, reducers, selectors, maybe sagas or thunks.
Global-state-by-default – It’s too easy to throw random UI state into the global store “just this once”.
Coarse-grained updates – A small state change can cause a large part of the tree to re-render.
Cognitive overhead – New developers must learn your custom Redux patterns before they can ship features.
Meanwhile, React grew up. We now have:
Hooks for local and shared state.
Context for scoped global-ish values.
Query libraries (like TanStack Query or RTK Query) for server state and caching.
Signals and stores that focus on fine-grained reactivity and ergonomics.
The result is that Redux is no longer the default hammer. It’s one tool in a much bigger toolbox.
Signals and Stores: A Different Mental Model
Before talking about migration, we need to understand what we’re migrating toward.
What is a signal?
A signal is a small reactive value. Conceptually:
It holds a value, like count = 0.
When you read that value inside a reactive context (component, derived value, effect), the context “subscribes” to that signal.
When you update the signal, only the things that depend on it re-run.
The key idea is fine-grained reactivity. Instead of saying “something in this big Redux state object changed, let’s re-render everything that subscribes to it,” signals track dependencies on a more granular level.
You can think of a signal as:
const count = createSignal(0);
// read
const value = count();
// write
count(value + 1);
Now imagine dozens or hundreds of these tiny reactive cells, each only updating the UI that actually depends on it. That’s very different from a single giant Redux store.
What is a store?
A store is a small state module built on top of primitives like signals (or a similar mechanism):
It owns some state.
It exposes methods to update that state.
It might expose derived values (computed from the underlying state).
It usually focuses on a single domain or bounded context: authStore, cartStore, settingsStore, etc.
This is a big mental shift from a monolithic Redux store:
Instead of one global state tree, you have multiple small stores.
Instead of dispatching action objects, you call methods on these stores.
Instead of selectors scattered across files, you have store APIs that encapsulate both state and logic.
Redux vs signals/stores at a glance
Redux
Centered around a global event log (dispatch(action)).
State updates are pure reducer functions.
Selectors read from a large central state object.
Great for global reasoning and devtools, sometimes heavy for everyday work.
Signals + Stores
Centered around reactive values and small, focused modules.
State updates are direct mutations via store methods.
Dependencies are tracked at a fine-grained level.
Great for performance and local reasoning, different debugging story.
Neither is objectively “right” in all cases. But signals and stores offer a path to modernize without recreating Redux-style boilerplate.
Step 1: Take Inventory of Your Redux State
You can’t migrate what you don’t understand. Before pulling in a signals library or writing a store, take a deep breath and classify what’s living in your Redux store today.
Most real-world Redux trees break down into three broad categories.
1. Server / cache state
This is the data that mirrors your backend:
Lists of entities (users, orders, products).
Pagination, filters, sort keys.
Loading and error flags.
In 2025, this kind of state is usually better handled by a query library:
Automatic caching and refetching.
Stale-while-revalidate logic.
Built-in loading and error management.
If you’re writing actions like FETCH_USERS_REQUEST and FETCH_USERS_SUCCESS by hand, you’re essentially rebuilding what these libraries already give you.
2. Global app state
This is state that really is global, or at least widely shared:
Current user and permissions.
Feature flags and A/B test variants.
App-wide configuration.
Session-level data that multiple routes care about.
This is ideal territory for small stores:
authStore for user/session data.
featureFlagsStore for flags.
appLayoutStore for sidebar, theme, etc.
You still want structure and testability here. You just don’t need to funnel everything through one giant global reducer.
3. UI and ephemeral state
This is where Redux tends to get abused:
“Is this modal open?”
“Which tab is active on this screen?”
“What’s the current step in this wizard?”
A lot of this has no business being global. It can live in:
Local component state (useState, useReducer).
Context scoped to a subtree.
A tiny store owned by a feature or page.
Just doing this classification is incredibly clarifying. You’ll start to see:
Which parts of Redux can be replaced by a query library.
Which parts deserve their own store.
Which parts should move closer to the components that actually use them.
Step 2: Decide Upfront That You Won’t Rewrite
This might be the most important decision you make: you are not doing a rewrite.
Rewrites are seductive. You get to throw away old patterns and design things “properly.” You also get:
Months of parallel work.
A long period where both old and new systems must coexist anyway.
A ton of risk: deadlines slip, bugs creep in, and eventually someone suggests putting it all back into Redux because “at least that worked.”
Instead, you’re going to use a strangler pattern:
Keep Redux running.
Add signals and stores alongside it.
Gradually route more logic and state through the new system.
Retire old Redux slices only when they’re fully migrated.
Think of Redux as the old city center and stores/signals as new neighborhoods that appear at the edges. Over time, the new neighborhoods take over more of the traffic, but people can still get where they need to go while construction is happening.
Step 3: Pick One Slice and Model It as a Store
Don’t start by trying to migrate your most complex, mission-critical slice. Pick something smaller and less risky:
A settings panel.
A notifications sidebar.
A “preferences” slice.
Look at the reducer for that slice and translate it into a small store.
For example, say you have a Redux slice like this:
type SettingsState = {
theme: ‘light’ | ‘dark’ | ‘system’;
language: string;
enableBeta: boolean;
};
Reducers:
SET_THEME
SET_LANGUAGE
TOGGLE_BETA
In a store-based approach, you might do:
const settingsStore = createSettingsStore();
settingsStore.setTheme(‘dark’);
settingsStore.setLanguage(‘en’);
settingsStore.toggleBeta();
Internally, that store might use signals (or another primitive) to hold the state:
One signal for the entire settings object, or
Separate signals like themeSignal, languageSignal, betaSignal.
The important part isn’t the exact implementation. It’s that you end up with:
A single place that owns the state.
A clear API for changing it.
A way for React components to subscribe to updates.
You’ve just recreated what Redux gave you for that slice, but in a much smaller, self-contained package.
Step 4: Bridge Existing Components to the New World
Now comes the part teams usually worry about: “We have useSelector scattered across 200 files. Do we have to change everything?”
No. You bridge.
There are a few patterns that work well:
Pattern A: Store as the source, Redux as a mirror (for a while)
Let the new store be the real source of truth.
After the store updates its signals, mirror the current state back into the Redux slice.
Existing components that rely on useSelector keep working.
New components can read directly from the store.
This gives you time to gradually replace selectors with store-based hooks, one component at a time.
Pattern B: New hooks that hide the implementation detail
Create hooks like:
function useSettings() {
// internally read from the store
// return the values and setters that components need
}
Start using useSettings() in new code.
Whenever you touch an old component, consider migrating it from useSelector to useSettings.
At some point, the Redux slice stops being used at all and can be removed.
Either way, the goal is the same: React components shouldn’t care whether the data comes from Redux or a store. They just want data and a way to update it.
Step 5: Move the Writes, Then Retire the Slice
Once components can safely read from the new store, you can start moving writes.
Instead of:
dispatch(setTheme(‘dark’));
You move to:
settingsStore.setTheme(‘dark’);
During the transition, the store method can:
Update its internal signals.
Optionally dispatch a Redux action that keeps the legacy slice in sync.
At some point, you’ll see that nothing reads that Redux slice anymore, and nothing truly depends on those actions. That’s your moment to:
Delete the slice from combineReducers.
Remove its actions and selectors.
Remove any now-dead code paths.
You’ve successfully migrated one piece of complex state from Redux to a store without breaking the app or doing a rewrite.
Step 6: Handling the Messy Realities
Of course, not everything is as tidy as a settings panel. A few common complications will show up.
Cross-slice dependencies
Maybe your user slice and permissions slice are tightly coupled. When you migrate them, you may realize they are actually one conceptual module: session or auth.
Instead of mechanically turning each Redux slice into a store, you can step back and ask:
“If we were designing this today, would this be one store or several?”
“What is the natural boundary here?”
It’s often cleaner to create one authStore that owns user, roles, and permissions together, even if they used to be separate Redux slices.
Effects and async flows
Redux often centralizes async logic in sagas, thunks, or extra reducers. When you move to signals/stores:
Decide where async logic lives now:
In store methods (e.g., authStore.login() that does the fetch and updates signals).
In separate “service” modules that your store calls.
Keep error handling explicit. Don’t hide it in a reactive black box.
Signals help with reactivity, not with I/O semantics. You still need clear patterns for fetching data, handling failures, retries, and so on.
Testing
The good news: stores are usually easy to test.
Instantiate a store.
Call its methods.
Assert on its state and any derived values.
If you have existing reducer tests, you can often port them to store tests with minimal changes. The goal is the same: given an initial state and some inputs, assert that the state transitions are correct.
Step 7: Measuring the Outcome
Migration should not be an act of faith. After you move a slice or two, ask some pragmatic questions:
Is the UI faster or at least no slower?
Signals and small stores should help reduce unnecessary re-renders.
Is the code easier to read and maintain?
Can a new developer open a store file and understand the feature without chasing actions across multiple files?
Did we lose any important Redux ergonomics?
Maybe you miss time-travel debugging. Maybe logging store method calls is enough. You can often build a lighter-weight version of the devtools you actually use.
If the answers are positive, you now have a working pattern you can repeat across the rest of your Redux state. If not, you’ve only migrated a small part and can adjust your approach.
You Don’t Have to Break Up With Redux Overnight
The real mindset shift isn’t “Redux vs signals.” It’s:
“Use the right level of state management for each kind of state.”
That might mean:
Keeping Redux for a few critical flows where its event log model still shines.
Using signals and stores for most new state and performance-sensitive paths.
Relying on query libraries for server data.
Keeping UI state as local as realistically possible.
Over time, as more of your complex state lives in well-designed stores backed by signals, Redux becomes a smaller, more targeted part of your React architecture. Maybe it eventually disappears. Maybe it sticks around in a few places where it still earns its keep.
Either outcome is fine—as long as you arrive there incrementally, with the app still serving users the whole time.
You don’t need a heroic rewrite. You just need a steady series of small, intentional migrations from “one global hammer” to a thoughtful mix of Redux, signals, and stores that matches the kind of React app you’re actually building today.
Frequently Asked Questions
1. Why does Brigita recommend migrating from Redux to signals or modern stores in 2025?
Brigita suggests this migration because signals offer finer-grained reactivity, faster UI performance, and reduced boilerplate. For global projects across India, UAE, Dubai, GCC, and the USA, signals help teams build more scalable, modern React applications.
2. Can Brigita help migrate a React application from Redux to signals without a full rewrite?
Yes. Brigita follows an incremental, SEO- and AEO-friendly migration approach, allowing React teams to shift from Redux to signals/stores without rewriting the entire codebase—ideal for enterprise apps running across India, UAE, GCC, Dubai, and the USA.
3. What Redux state should be moved first when Brigita starts the migration?
Brigita usually begins with low-risk UI and settings slices. This ensures safe, gradual migration while maintaining stability for global deployments in regions like India, UAE, Dubai, GCC, and the USA.
4. Does Brigita recommend replacing all Redux logic with signals and stores?
Not always. Brigita uses a hybrid approach—signals for UI/domain state, query libraries for server data, and Redux only where event logs truly matter. This balanced method ensures strong performance and SEO-friendly architecture across all target regions.
5. How does Brigita ensure performance improvements during the migration process?
Brigita uses signals to reduce unnecessary React re-renders, improving speed and user experience for audiences across India, UAE, Dubai, GCC, and the USA. With fine-grained reactivity and optimized state boundaries, apps load faster and rank better in search.
Search
Categories