Do we really need Redux or @ngrx/store

About this article

If you build applications with Angular, Vue, or React, you’ve likely worked with or at least seen the Redux pattern in action. Redux gives us a structured, immutable approach to state management — that’s a clear win — but using it correctly means dealing with a fair amount of boilerplate. In this piece, I want to dig into when Redux is the right choice and why. The code samples and plugins I reference are Angular-specific. That said, the core ideas translate across frameworks, which is also why I’ll be using the @ngrx/store package here rather than the plain Redux package.

To Redux or not to Redux?

It’s crucial to start by acknowledging that Redux only addresses a specific set of problems. If you aren’t facing those exact issues, there’s a strong chance that the Redux pattern adds unnecessary complexity to your app.

A key question to start with is: Does my app even have state? State could be as simple as remembering a pager’s current value or whether a sidebar is collapsed. It could also be a cached dataset from your backend, or user details your whole app depends on. Even a single value you want to hold in memory while navigating between routes counts.

Here’s a breakdown of cases where the Redux pattern might genuinely prove useful:

  • Persisting state — for example, keeping a search filter value so a grid shows the same results when the user navigates back
  • Cross-component state for components on separate routes, where there’s no parent to pass data down via inputs or properties
  • Optimistic updates: See this article
  • Real-time data updates: See this article
  • Undo/redo functionality
  • Full tracking of state changes paired with solid debugging tools, like Redux DevTools
  • A structured approach to handling session storage or localstorage. Check out this plugin

Working with immutable data is a recommended practice in Angular apps. That enables us to leverage the OnPush change detection strategy Angular provides, which improves performance and reduces unpredictable outcomes. For React, a Pure component would play a similar role. So, for now, let’s take it as given that immutable data structures are the right path and our app depends on them.

That said, Redux does force immutability — but let’s still question whether it’s necessary for every app. If our state management needs are simple and we’re just storing a few values, a straightforward state service could easily do the job instead of Redux.

@Injectable()
export class UsersService {
    private _users$ = new BehaviorSubject([]);

    get users$(): Observable<User[]> {
        return this._users$.asObservable();
    }

    // IMPORTANT: since we use an immutable dataflow
    // we have to make sure users is a new instance
    setUsers(users: User[]): void {
        this._users$.next(...users);
    }
}

Consider a scenario where, by invoking the setUsers() method, we assign a basic array of user objects, committing that data to a BehaviorSubject which is then exposed as an observable. Through the spread operator (...), we generate a fresh instance of the users collection, guaranteeing immutable operations. The approach here is remarkably straightforward, eliminating the need for Redux or the extensive boilerplate it typically introduces. When the application's state consists of only a handful of straightforward fields, adopting the Redux pattern could be considered excessive.

However, consider a scenario where our requirements extend beyond merely assigning a value to users. What if we also need to insert and delete entries from that stream while preserving immutability? Take a look at the subsequent illustration to see how we'd tackle that.

@Injectable()
export class UsersService {
    private _users$ = new BehaviorSubject([]);

    get users$(): Observable<User[]> {
        return this._users$.asObservable();
    }

    setUsers(users: User[]): void {
        this._users$.next(...users);
    }

    addUser(user: User): void {
        // We cannot use array.push because we only want to
        // pass immutable data to the streame
        // for the OnPush strategy remmber?
        this._users$.next([...this._users$.getValue(), user])
    }

    removeUser(id: string) {
        // Again, we have to create a new Array instance to not break the
        // immutable dataflow
        this._users$.next(this._users$.getValue().filter(v => v.id !== id));
    }
}

The above approach is getting a bit awkward—it feels like reducer logic is creeping into our state service just to ensure immutability. We’ve also built a custom observable layer to keep track of state changes. If this is the only piece of state in the app, pulling in redux might be unnecessary, but things can get messy fast when we’re juggling multiple or nested states.

Consider caching as another case. Many reach for redux to handle cached data, but a simple shareReplay operator could serve the same purpose.

fetchUsers(): Observable<User[]> {
    ...
    return this.httpClient.get('').pipe(shareReplay(1));
}

Summary

My advice is to hold off on adopting Redux until it becomes necessary. Looking back, many of the apps I've built eventually reached a point where Redux was the right call. On the flip side, I've also worked on plenty of projects—like basic CRUD apps—that got by just fine without it.

Whether you bring Redux into the mix is your call, but whatever you do, keep your application state immutable. That discipline will pay off when you're tracking down bugs and will enable you to squeeze out performance from Angular's change detection.

Angular forms course