If you are a React developer, you might have encountered some challenges when it comes to managing the state of your application. As the application grows bigger and new features are added, you are bombarded with several challenges related to the state management of your React App. Like how do you ensure that your components are in sync with the data? How do you avoid prop drilling and unnecessary re-rendering? How do you handle complex scenarios like async actions, caching, and undo/redo?
Architectural patterns like MVC solved these issues for small apps, but as an application grows, new models, views and controllers are added. This means that a single interaction on a View, multiple paths of execution can happen. Tracing a bug in such an application using MVC becomes harder over time and can slow down implementation of new features.
This is where Flux Architecture comes in.
Say Hello To Flux
Flux is not a library or a framework, but rather it's a design pattern that helps you organize your code and data flow clearly and consistently. It suggests to split the application into four main parts: actions, dispatcher, stores, and views.
Facebook used to build its UI applications using MVC, but in 2014, during the Hacker Way conference, the Facebook team announced that MVC was not working for them anymore, and they would be switching to their newly created architecture - Flux. It was introduced as an alternative to MVC, that can be used for building client-side web applications.
Let's understand the four main parts of Flux, by taking an example of building a To-Do app.
Actions
Actions are plain objects that describe what happened in the application. They usually have a type
property that identifies the action type, and a payload
property that contains any additional data needed for the action. For example:
// An action that adds a new todo item
{
type: "ADD_TODO",
payload: {
id: 1,
text: "Learn Flux"
}
}
// An action to mark the a todo as completed
{
type: "MARK_TODO_AS_COMPLETED",
payload: {
id: 1
}
}
Actions can be created by any part of the application that needs to change the state or trigger some side effects. For example, actions can be created by user interactions on views, by network requests, by timers, or by other sources. Actions are then dispatched to the central dispatcher, which broadcasts them to all registered stores.
Dispatcher
Dispatcher acts as a hub for all actions in the application. It allows stores to register themselves to the dispatcher and provide a callback function that will receive all actions. When an action is dispatched, the dispatcher invokes all registered callbacks with the action as an argument. The dispatcher ensures that all callbacks are invoked in a deterministic order, and that no callbacks are invoked while another callback is still executing.
The dispatcher is also responsible for handling dependencies between stores. Sometimes, one store may need to wait for another store to update before it can process an action. For example, if we have a UserStore
that keeps track of the current user, and a TodoStore
that keeps track of the todo items for that user, we may want to wait for the UserStore
to update before we update the TodoStore
.
The dispatcher is usually implemented as a singleton object that is shared across the application. There is no need to create multiple dispatchers for different parts of the application. The dispatcher does not contain any application logic or state - it only serves as a mediator between actions and stores.
Stores
Stores are objects that contain and manage the application state for a specific domain or feature. They are similar to models in MVC architectures, but they are not responsible for fetching or saving data to a backend - they only deal with the current state of the application.
Stores register themselves with the dispatcher and provide a callback function that will receive all actions. Inside the callback function, stores use a switch statement based on the action type to decide how to update their state. The state in a store must only be mutated by responding to an action. Hence it is advised to use immutable update patterns to avoid mutating their state directly.
Stores also emit change events whenever their state changes. This allows views to subscribe to store changes and re-render themselves accordingly. Stores are also responsible for exposing getter methods that allow views to access their state or derived data.
Stores are usually implemented as singleton objects that are shared across the application. There can be multiple stores in an application, each managing a different aspect of the state. For example, we can have a UserStore
that manages the current user’s information, a TodoStore
that manages the todo items for that user, and a FilterStore
that manages the filter criteria for displaying the todo items.
Views
Views are React components that render the user interface and handle user interactions. They are similar to views in MVC architectures, but they are not responsible for fetching or saving data to a backend - they only deal with the current state of the application, at a given point in time.
Views subscribe to store changes and re-render themselves accordingly. Views can also dispatch actions to the dispatcher in response to user events, such as clicks, inputs, or forms. Views can be further split into two types: container views and presentational views.
Container views are components that are connected to the stores and the dispatcher. They listen for store changes and provide the data for presentational components. They also dispatch actions to the dispatcher when needed. Container views are often found at the top of the component hierarchy, and they act as “controller-views” that coordinate the data flow between the stores and the views.
Presentational views are components that are not connected to the stores or the dispatcher. They only receive data and callbacks as props from their parent components, and they render them accordingly. Presentational views are often reusable and stateless, and they can be written as functional components.
Key takeaways of the Flux Architecture :
Flux enforces a unidirectional data flow, which means that data can only flow in one direction, i.e. from actions to dispatcher -> dispatcher to stores -> from stores to views. This makes the data flow more clear and predictable, and avoids problems such as circular dependencies, cascading updates, or inconsistent state.
The stores are immutable, meaning that they cannot and should not be modified directly. Instead, you have to dispatch an action that describes what happened and how the state of the store should change. All control should reside in the stores, where the state is managed. Stores are not acted upon but rather informed by actions.
Separation of concerns: The components of your application have different roles and responsibilities. The view is responsible for rendering the data and handling user interactions. The action is responsible for capturing the user intent and dispatching it to the store. The store is responsible for updating the state and emitting changes. The dispatcher is responsible for coordinating the actions and the stores.
To Summarise...
Flux is an architectural pattern for building data layer for React applications. It consists of four main parts: actions, dispatcher, stores, and views. It enforces a uni-directional data flow, which makes the data flow more explicit and predictable, and avoids problems such as circular dependencies, cascading updates, or inconsistent state.
Flux can help you create more maintainable, scalable, and testable React applications. It can also improve the performance of your app by minimizing unnecessary re-rendering and optimizing the data fetching.
Flux is not a framework or a library - it is a set of guidelines and best practices that you can follow to structure your application. There are many implementations of Flux available, such as Redux, MobX, or Reflux. You can also create your own Flux implementation using plain JavaScript objects.
Reading about Flux in 2023 might not seem to be that relevant, but almost all the state management libraries that we use w/ Javascript and its frameworks have been built upon the core principles of Flux Architecture. So, it's crucial to know where it all started and where we have reached now.