Upsides

There are a vast number of upsides to using an explicit state management tool, arguably a larger number of upsides than downsides in a lot of situations.

Externalisation

Externalising your state into a state-specific layer, keeps your data agnostic from your UI framework.

While it is unlikely you would ever migrate your UI framework of choice while attempting to keep your state layer untouched, externalising your state does give you a number of other benefits.

Testing

Using a state management tool frees you from rendering and possibly mocking UI components purely for verifying your state management code. Some people might argue that you don't even need tests for your state layer, and all you need is tests against your UI components which utilise your state. But this can undeniably make it much harder to setup and mutate state in tests, if you are forced to do so through a component interface instead of a state interface.

By state vs UI interface, I mean that to setup state in your application with a component interface, you would be required to simulate user inputs to create data entities. Whereas with a state interface to a redux store, you can create and change entities directly with your action creators.

I would agree that integration tests using the component interface approach is good practise in general, but often it is also good practise to unit test your data layer in isolation from your view layer, to ensure you cover all the important use cases.

Reusability

A tendency can be to simply chuck everything in a bit of react state. This works and is probably the quickest way to develop something initially, but problems can and do arise where you want to re-use state management logic you have built into a component elsewhere in your application for other reasons.

Using a state management tool helps avoid this issue in the first place, as your state will already have been separated from any view logic that uses your state.

Forcing good patterns

Writing code that is easy to understand is the most valuable attribute of code in larger organisations. With tens or even hundreds of developers touching and contributing to a codebase over a product's lifetime, it becomes very important that common patterns and good practises are followed vs a project that is only maintained by a small number of developers.

In frontend applications, you most commonly will have two kinds of state. State that has come from a backend service, and state that lives entirely within your application for rendering purposes.

I have seen cases where these types of state get mixed up, which can cause problems in the long run. If you start adding new fields to entities that came from an API originally, when you update these entities with fresh data from your backend API, then you will likely want to preserve these fields, but also merge in new data into entity objects. Doing this can start off being simple, but will grow in complexity as your application grows.

For example:

// Original entity store in redux state
{
  user: {
    id: 123,
    name: 'Some name',
    email: 'some email,
    createdAt: '2021-01-01T00:00:00Z',
    updatedAt: '2021-01-01T00:00:00Z',
  }
}

// With UI flags
{
  user: {
    id: 123,
    name: 'Some name',
    email: 'some email,
    createdAt: '2021-01-01T00:00:00Z',
    updatedAt: '2021-01-01T00:00:00Z',
    // State purely for the frontend application,
    // which never gets stored by a backend service
    isVisible: true,
  }
}

// Now to merge some new user data,
// preserving our UI flag in a `user` reducer...
const userReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_USER': return {
      ...state,
      action.payload,
    };
  }
};

It can also hurt your performance with unnecessary React re-renders. If you have made an entire object-entity available in your React component through mapStateToProps with React, then React will rerender your component when your object gets recreated by default as the new object is not referencially equal. By adding new fields to your object for UI purposes, you have made it more likely that more components will rerender when this happens, as there are more fields to consume.

Example:

const MyComponent = ({ user }) => {
  // Only consumes user.isVisible,
  // but will rerender whenever `user`
  // gets recreated due to referential comparisons.
  //
  // Connecting entire objects vs individual fields
  // of an object is another hot topic, but it does
  // sometimes help keep things simpler connecting
  // an entire entity.
  if (user.isVisible) {
    return 'Showing';
  }
  return 'Not Showing';
};

export default connect(state => ({
  user: state.user,
}))(MyComponent);

Using a state management tool makes it far simpler to enforce strict patterns where UI state is kept separate from API entity state. With objects embedded in your components, there is cognitive overhead for other developers reviewing your code to work out which objects in your components are API data, and which are UI associated.

Keeping these concerns explicitly separate will make it far easier to maintain and remain consistent over a long period of time.

UI state vs persisted data is one example that can benefit from state management tools. There are many others you will likely encounter which can benefit too.

Abstraction

Abstraction of complex state can be a good thing.

Say you are storing an array of entities directly in your state, but then want to refactor your state such that they are now stored in a map or a plain JavaScript object for performance reasons elsewhere in your application. This would mean refactoring a large amount of associated code to use a map instead of an array in a naive application.

If you have used redux, and also used the common "selector" pattern for retrieving state, then this schema change can be hidden entirely behind your selectors, without needing to change any components' code to account for this.

For example:

// Original state
const myRootState = {
  todos: [
    { id: 1 },
    { id: 2 },
  ],
};

// becomes
const myRootState = {
  todos: {
    1: { id: 1 },
    2: { id: 2 },
  },
};

// Using a selector, you can
// completely hide this schema change
const mySelector = (state) => {
  const result = Object.values(state.todos);
  result.sort(
    ... // Sort based on
        // whatever you want,
        // id, date etc
  );
  return results;
};

This benefit of state management is quite easy to visualise, as the data you consume in your components can be entirely different to state you maintain in a redux store. You can compute new data types, hide schema changes, create arrays of data from maps and so on.

Common interface

Using state management makes it easier for developers to transition between products and applications within an organisation.

If you work somewhere that developers move between different teams frequently, or where developers need to touch multiple repositories to enable some changes, using a common methodology for state management can make it easier to switch between applications quickly.

If multiple different applications use a vastly different technology stack, there is some cognitive overheard in adjusting to a new application if you have been working on one for a while. Using a common statement management tool can help reduce some of this cognitive overhead.

Using a common methodology also eliminates the possibility of home-grown state management evolving organically to the point where different teams can't recognise the common core anymore. Different applications can use the tool in different ways with different plugins of course, but the common core is still there and still the same, with the same documentation remaining relevant.

Keeping your options open

Given enough time, you or someone else in your organisation will inevitably want to improve or change your initial product. The only other option is that your product will die and never be used again.

Changes in your application can happen in a few different areas and ways. The most common and simplest to deal with is adding new features within your existing set of non-functional requirements.

Another type of change is increasing the scope of your non-functional requirements for an existing application. This is often far harder to deal with if you hadn't given yourself enough options and escape hatches to begin with.

Your simple online only SPA which calls some APIs, might now need to become an application with more progressive web app type requirements such as optimistic updates or offline compatible features.

If you had implemented your statement management using simple constructs such as using useState to store the results of API calls in your React components, you would now be in a difficult position where you would need to refactor your entire application to enable these scope changes.

Offline capable products require some form of state persistence, as well as a form of network queue. If you had used state management such as redux from the start, refactoring your application to enable these features becomes far easier to manage.

There are plugins for Redux which help you persist redux state, and there are plugins and tools for managing your network request queue(s). With a sensibly implemented redux store, the majority of work in enabling this scope change would be around adding error handling and refactoring the way you make network requests, rather than requiring you to completely refactor your application.

A common and good piece of advice is to keep things simple. Adding the overhead of redux purely for this reason might not be a good idea. If there's a good chance things will change further down the line, the small amount of overhead might also be well worth it.

It's all a fine balancing act for your specific project and project size, with your specific requirements, with your specific processes, team members and other stakeholders, and if you think there is a good chance that scope change might happen at a later date.

Developer experience

An often overlooked area in developing an application is developer experience.

Debugging state in an application can be hard. You end up having to log what's going on, use your debugger at specific points and keep a map in your head of what's changed in a sequence with respect to application state. State management tools like Redux have got plugins to aid with debugging.

There is a chrome extension for redux, which by default tracks every single state change in your application as you use it, tracking the contents of your actions, showing you what's changed in your redux store as a result, as well as the entire contents of your store at every step. This kind of thing in my experience has made debugging some issues associated with application state far easier to debug.

Teams often end up only looking at how an application should function for an end user. While this is obviously incredibly important and also likely the most important factor to consider in a project, it is also important to consider how the application will be developed.

Applications which developers can only make changes to through a long and arduous build process before getting any feedback, can take orders of magnitude longer to develop, costing companies far more money and time than is really needed.

There are other hidden costs in this approach to development other than just peoples' time. Keeping developers and other stakeholders happy is very important in reducing turnover in an organisation. If staff aren't happy, they can and will leave, forcing an organisation to hire replacements.

From a quick google search, there is an article detailing a basic survey done of how long it takes for new developers to fully ramp up and become fully effective after joining, which was quoted as between 3-9 months on average, varying between different organisations.

Obviously take my quick google searches with a pinch of salt, but based on this article, developers are not fully effective for up to 9 months. 9 months of paying someone a full salary, to likely not be as effective as the person they replaced. If the process that caused the previous developer to leave remains the same, there is a high chance that this replacement will also leave within a short period of time, restarting the cycle.

Another angle to this is that using an outdated or inefficient technology stack will make it harder to attract good developers in the first place. So not only does the application take ages to create, but developers are highly likely to leave, and it is hard to replace them with someone competent.

Debugging within state management in an application is of course a relatively small slice of the pie when it comes to developer experience for creating applications, but many small slices of pie add up to an entire pie.

Community support and documentation

Using a widely used tool such as redux gives you access to a wide array of well written and battle tested documentation. It becomes far more likely that issues you face, will already have been faced and solved by someone else with examples available online. The burden of maintaining your state management tool is almost removed, assuming there is a community or organisation maintaining your state management tool of choice.

All of these benefits are removed if you end up using a homegrown solution. Of course if you aren't using a homegrown solution, and instead use things like useState hooks or context in React, then this isn't so much of an issue. But using a ready-made solution, which is free, well tested, well documented, simple to use, extensible and most importantly fit for purpose, is almost always the better option over creating your own version.

Downsides and when it doesn't make sense

I've spoken about some of the biggest upsides to using a state management tool. There are some downsides however, which could sway you in the direction of deciding to not use a state management tool at all.

Your application is simple

I recently came across a situation where it was suggested to use a typical react-redux architecture for a new JavaScript based project.

OK great, nothing wrong here... Until I saw what the application actually does.

It was an almost entirely read-only application, which consumes static JSON as a REST api from a filesystem.

There is simply no need to encapsulate this read only state in a tool such as redux. Nothing will ever change in the application beyond some simple UI state for toggling views and other controls. The data itself in the application does not change.

The overhead of encapsulating state changes and network calls to a static JSON api, will give you no benefit in a situation like this. It will simply add complexity with state being separated from the components that use it, as well as now having to write and maintain action creators and reducers for this static state.

There isn't that much state

Another situation which doesn't benefit from using state management is where your state isn't really that complex.

Your application itself could be very complex, with complex visualisations over a set of data, but the state itself might only be a single array of entities from a single microservice.

In this situation, you likely wouldn't benefit much from using state management, as there is only one source of data, with likely only one network call at all. If the state doesn't change very often, you can probably get away with using something like React context for making state available globally across your application.

Your application is small

One more situation that might not make sense to use redux, is a small application which is owned by only one or two developers. If the codebase remains small, and will only be maintained by a couple of people at most, it is far easier to agree on a pattern for statement management, which doesn't use a state management tool, than it is for a large team or large application.

Small applications are also more likely to remain simple enough that you wouldn't need to use state management tools for performance reasons. Redux helps with managing how often your React components rerender, but if your application only has a few levels of nested components, it is far less likely excessive re-rendering will cause a problem.

In this situation you can get away without a state management tool, removing the overhead of using the tool, as well as managing to maintain common and consistent patterns for your codebase.

Conclusion

There are many factors to consider when choosing which state management tool to use, or if you should even use a tool at all.

Some factors aren't even related to technology, and are more related to the people and teams in a big organisation.

Hopefully this overview will help you decide if you need redux, or at least make you pause and think about if you actually need more complexity when the simple solution might do just fine.