What’s the purpose of actions, state, and selectors?
If you can remember the first time you tried to understand Redux, it’s likely a memory of an initial failure to grasp the flow of seemingly arbitrary boilerplate. Eventually you’d understand how to apply the Redux formula, but conceptually, you might still be at a loss as to why Redux requires what it does and if so, you likely resent the seemingly meaningless effort Redux requires. Redux has a purpose and no one ever explained it to me well, so I’d like to share what is actually meaningful in the Redux pattern with regard to actions, state, and selectors.
Overview
For starters, we should always keep one thing in mind when it comes to front end applications: reactive state variables at scale are insane and makes writing bug free code difficult. Some seemingly innocuous state update in one corner of our application may very well have unintended consequences in some totally different part of our complicated app. In the front end world, skillfully handling state is just part of the game — we can’t avoid it without devoting ourselves to exclusively simple applications. Redux aims to simplify this problem and the way we speak about actions, state, and selectors are aimed towards properly handling the three categories to lessen the impacts of the unpredictably in stateful front end applications.
At a high level, the relationship between actions, state, and selectors is this:
- ACTIONS: interpret events in terms of their impacts on state.
- STATE: is a representation of the base mutable internals of a UI system
- SELECTORS: interpret state in terms of impacts on the rest of the front end application.
Here’s an example we’ll reference throughout this article: the user clicks a button to fetch data. An action tells state to update a variable called isFetching to true, and two selectors interpret isFetching as 1) selectIsButtonDisabled is true and 2) selectIsSpinnerDisplayed is true.
If this still seems arbitrary, sit tight. We’ll dig into the distinctions more as we go starting with state.
It starts with State — What is it really?
The first distinction from our example that’s important to understand is the difference between the stateful variable, isFetching, and its interpreted values: selectIsButtonDisabled and selectIsSpinnerDisplayed. Why should whether or not we’re fetching data be represented as state and not whether or not the button is disabled?
The answer to that is because fetching data is the actual change prompted by the user. A disabled button or displaying a spinner is a consequence of fetching data. When I click a login button, I (as the user) mean to change the state of the application by making a network request to authenticate my credentials. The fact that the login button becomes gray or a big spinner displays on the page are the UI’s interpretation of the state in which I placed the page and therefore are best represented by selectors.
Another quick note about state: isFetching is actually a poorly named state variable because we often have more than one fetch request. It’s a bad practice to have many events share the isFetching variable and instead we should have more specific state variables such as isFetchingUserData and isFetchingLoginAuthentication. If the same spinner happens to show for both conditions, the selector should represent this logic by including both variables as conditions to show the spinner. In this way, each fetching state variable represents no more than one application state. Using isFetching for all API requests or conflating state in some similar way makes unpredictable bugs likely.
For clarity, let’s look at some examples of what is state vs what is an interpretation of state (and should be represented as a selector):
- Displaying a side bar
a. State: isMenuVisible
b. Selectors: selectIsBackgroundOpaque, - Collecting input data
a. State: usernameField, isSubmitted
b. Selectors: selectDisplayErrorMessage - Displaying a modal
a. State: showModal
b. Selectors: selectIsBackgroundOpaque, selectHideScrollbar
Selectors as interpretations of state
From our example, we know that isFetching belongs to our state and selectors interpret isFetching to mean that we should display a spinner and disable a button. There are significant benefits to thinking of selectors in this way which are:
- The selector state now semantically has meaning with regard to what it does. Using isFetching directly to disable a button is not as good as creating a selector called selectIsButtonDisabled which most clearly describes the button.
- In a similar manner, if some other piece of state has an impact on whether or not the button is disabled, we can add it to the selector. This gives us added flexibility to modify behavior later with predictable outcomes because the selector deals solely with whether or not the button should be disabled.
- The relationship between state and its impacts are well defined and is better than creating two state variables (isFetching, isButtonDisabled) where the relationship between the two is not codified.
- The selector is testable based on the state. Give certain state or combination of states, we can verify the outcome of our selectors through unit testing.
As an additional note, it is better to have a selector that 1) represents whether or not the spinner displays and 2) a separate selector that represents whether or not the button is disabled even if they have the same conditions. This allows for predictable outcomes to future modifications of the selectors.
Actions as interpretations of Events
Finally, actions are:
- Declarative interpretations of events in terms of their impact on the state of an application.
- The sole source of change for what is otherwise an immutable store.
First declarative interpretation means that actions describe the event and its changes to state by their name rather than some hard coded state modifications. This simplifies the way we reason about state updates because we’ve now named the action which is now associated with one or more state variables to update. When we read our code, it is simpler to have a name for what is interpreted into state rather than trying to figure out what a series of hard coded singular state updates means.
Here’s a fun piece of information/side note: the dispatch function associated with actions seems to loosely emulate algebraic data types in other languages. The most obvious similarity is between dispatch and discriminated unions in TypeScript (which can be applied to give type safety to your reducers by the way). Discriminated unions and dispatch both are a JavaScript-esque way of expressing a type for a given variant. In a TypeScript discriminated union, we’d say { type: “updateUsername”, payload: string }, but something like ReScript has a nicer syntax and would have something like updateUsername(string);
Second, actions create a canonical set of valid updates that can be made to state. Nothing else in our application can update state which makes complicated state easier to manage.
All in all we’ve learned that
- Actions interpret events into state.
- State is represents the most base, mutable parts of the internals of our application.
- Selectors interpret state to create meaning for the rest of the application.
I hope that what I’ve shared is a reasonable understanding of these three concepts. I’d love feedback or better ways to think about state. :)