Web application development is in a constant state of flux. While there are many front-end libraries and frameworks that focus on component-based architecture, Vue, Angular and React are, without a doubt, some of the most popular options. Of these, React is one of the most widely used client-side component/view libraries.
Using components is a great way to structure web applications because they make it easier to reuse code. However, as the number of components grows, managing the communication and data flow between them becomes complex.
How Data Flows Through React Nested Components
Data flow can be complicated to manage, especially with a deeply nested tree of React components. That’s because data passes through every nesting level, including components that don’t need it. This pattern is called prop drilling and it can become a challenging architectural problem for developers to tackle.
There are two primary tools In the React ecosystem that offer solutions to this challenge: Redux and Context API. In this article, we’ll unpack both technologies to understand how they manage data flow and determine if there is a clear choice for using one over the other.
What Is Redux?
Redux is a library for managing and updating the application state using events called actions. It’s a centralized store that’s shared across the entire application and ensures that the state gets updated in a predictable fashion.
The patterns and tools provided by Redux make it easier to understand when and how the state in the application is updated and how its logic will behave when the update occurs. With this, Redux makes it easier to write predictable and testable code.
Also, Redux leverages React Context. In earlier versions of React, Context was still an experimental feature and intended to manage the global state, just like Redux state.
How Does Redux Work?
Redux is an implementation of Flux and consists of four key parts organized as a one-way data pipeline:
The View dispatches actions that describe what happened. Then, the Store receives these actions and determines what state change should occur. After the State updates, the View is rendered with the new state.
The Store is where the state is managed centrally. It’s responsible for maintaining the state and receiving actions from the View.
The Store handles the state updates with a function called Reducer (a function that receives a state and a dispatched action from the view to return a new state specified by the action). It’s important to note that reducer functions must be pure, meaning the State has to be treated as an immutable object and can’t be manipulated directly.
The View needs to re-render after the update because the State is being modified outside of React. The store implements the observer pattern with an array that uses a subscribe method to add a new listener and call each listener function whenever the state changes.
In the View, we can subscribe to our component when it mounts. The listening function that we pass to subscribe() will call this.force_update() and will trigger a re-render of the component. The view mentioned above is the one in Flux architecture, but when we put it in the React ecosystem the view is contained in the React component.
Benefits of using Redux
If you choose Redux for your project, some key benefits are:
- It increases the predictability of a state: Since reducers are pure functions, they always produce the same result when the same action or state is passed to them.
- It’s highly maintainable: The structure of any Redux application is relatively standardized since code organization follows strict guidelines with this library.
- It prevents re-renders: The state is treated as immutable. The new state is derived from the old one using a shallow copy. This reduces the probability of re-renders substantially, therefore having a positive impact on performance.
- It makes debugging easier: By logging actions and the state, Redux makes it easy to have insight into what happens during the lifetime of an application. It has excellent DevTools that allow us to time-travel actions, persist actions on page refresh, etc.
- It’s useful in server-side rendering: The usefulness and effectiveness of Redux in server-side rendering are also well-proven. Handling the initial render of an application is relatively easy with Redux.
- It’s easy to test: Redux relies on pure reducer functions. It’s easy to test pure functions since they always return the same output given the same input.
What Is Context API?
Context API is a different approach to tackling the data flow problem between React’s deeply nested components. Context has been around with React for quite a while, but it has changed significantly since its inception. Up to version 16.3, Context was a way to handle the state data outside the React component tree. It was an experimental feature not recommended for most use cases.
Initially, the problem with legacy context was that updates to values that were passed down with context could be “blocked” if a component skipped rendering through the shouldComponentUpdate lifecycle method. Since many components relied on shouldComponentUpdate for performance optimizations, the legacy context was useless for passing down plain data.
The new version of Context API is a dependency injection mechanism that allows passing data through the component tree without having to pass props down manually at every level.
The most important thing here is that, unlike Redux, Context API is not a state management system. Instead, it’s a dependency injection mechanism where you manage a state in a React component. We get a state management system when using it with useContext and useReducer hooks.
How Does Context API Work?
Context API is quite simple. You only need to create a state context using the function createContext and it will return a provider and a consumer. The provider wraps the component tree where you expect the descendants to consume the state. The consumer is the wrapper for the location where the state data is used.
Benefits of using Context API
Here are some key benefits of choosing Context API for your project:
- It’s scalable. Context API can be used for any size of web application.
- It’s less complex than Redux. The workflow is much simpler than Redux. It doesn’t involve the additional parts or boilerplate that Redux requires.
- It has a lower implementation cost. In cases where we only use it to avoid prop drilling, we can put aside the implementation of reducers.
- There’s no need to pass data to the children at each level. The Consumer component can access all the data provided by the Provider Component at any level. This prevents prop drilling.
- It’s easy to maintain and very reusable. As no prop drilling takes place, if we remove a component from the tree or place it to another level, those components below will not be affected.
- Integration of React’s modules is seamless. Because it’s part of the core React library, we don’t need to install, import or maintain any additional libraries.
Context API vs Redux: When to Use One Over the Other
As Context API and Redux are ultimately used to build web applications, the three main goals are:
- A fast response time
- Easy to develop
- Easy to maintain
Let’s break down how these technologies perform for each of these goals.
An important aspect of response time is the time spent on initial load. That time is directly proportional to the amount of data sent from the server and the speed of the network. If we had two apps with similar code running on the same server and the same network, one using Context API and the other Redux, the app using Redux would take longer because it requires external libraries to function. The actual difference is only about 2 kilobytes.
Another response time component is the time spent to respond to user actions. In this case, the number of operations and the speed of the network are the two most important parameters. Redux requires more computational operations to be performed to respond to a user request. However, this difference is negligible.
Therefore, it’s safe to assert that when it comes to response time there’s not a major difference between the two.
Verdict: No major difference in response time.
Ease of Development
When it comes to ease of development, the most important aspect is the number of lines of code that have to be written to carry out an operation. Let’s look at some specific scenarios to get an estimate for this metric.
Scenario 1: Sharing State with Nested Components in React
Let’s imagine a scenario where we want to make some value available to any component in a given React tree without prop drilling. To solve this problem using Context API, we have to create a context with a default value, wrap a high-level component with a provider for that context and use it in one of their children. To compare with Redux, we have to implement the store by creating the object with the initial state, implement the reducer where we handle each action, return the new state, subscribe to the state change and dispatch the action. And so, Redux would require significantly more lines of code to be written to perform a similar action compared to Context API.
Scenario 2: Building an App with State Management
Another scenario is where we have a moderately complex state management needs within a specific section of our application. In this case, we could combine Context API with getContext and useReducer hooks. The lines of code would then be determined by the implementation of the reducers. Both Redux and Context API have to subscribe to the state in order to re-render the components that have been modified and they both require a place to manage the state. The lines of code required are similar in both cases.
Scenario 3: Building an App with Undo and Redo Functionality
Consider building an app with Undo and Redo functionality. With Redux, implementing undo functionality is rather easy. This is because the state is immutable and mutations are already described as discrete actions, which is close to the undo stack mental model.
For any scenario, we simply need to reshape the state into past, present and future values. Here, past and future are stacks where values are popped and pushed with undo and redo actions in the reducer. With Context API, it’s not possible to do this unless we augment the API with libraries or custom code that supports this functionality.
Verdict: ease of development with Redux or Context API depends on what you’re building.
Ease of Maintenance
It’s necessary to understand what happens in an application to easily maintain it. Developer tooling can be very useful for gaining this insight. For example, Redux enables you to inspect every state and action payload, go back in time by “canceling” actions and identify and analyze the error if a reducer throws one. These capabilities means less time is spent understanding the application and fixing bugs. Unfortunately, Context API is not as strong as Redux in this regard.
Application size and complexity are other important aspects to consider when comparing ease of maintenance. While it’s difficult to classify application complexity by using the number of components or nested elements as a metric, it’s important to note that small and simple applications will perform just fine with Context API. This is because the application state can generally be managed by passing it through components. That said, larger and more complex apps might require you to leverage the getContext and useReducer hooks alongside with the Context API.
Verdict: Redux provides better support for troubleshooting and testing large and complex applications
Redux and Context API are great solutions for managing data flow through React’s nested components. They both follow a similar philosophy: the state is taken outside the component tree and consumed as needed. In the case of Redux, external dependencies need to be installed, configured and maintained. Context API is integrated into the React core and requires minimal configuration.
Redux is a complete state manager capable of allowing an app to undo/redo actions and provides advanced developer tooling for debugging. Context API is designed as a dependency injection mechanism that allows making data available through the component tree without being manually passed.
For this reason, Redux is far more complex and abstract, with more concepts to learn, while Context API is simpler to learn, has fewer concepts, and is more intuitive. It is great for encapsulating data for specific contexts and not managing it globally. It avoids the inefficiencies of prop drilling while also being significantly more straightforward to implement than Redux. This table summarizes the comparison of context API and Redux:
Suppose you just wanted a simple data communication mechanism. In that case, Context API could be considered a better choice. At the same time, Redux is better if working on an app with many components, complicated inter-dependencies, dealing with unique features like undo/redo and requiring advanced debugging capabilities.