Whenever I start a new Webapp, I pretty much have the exact same set of libraries that I go to. React and Redux are high on that list.
However, a lot of the time I find that the Redux store is being used for things that are very specific to one particular area of the UI, and not for more global state. As such, recently I decided to try a different approach. Namely, the Context API and RxJS.
Now, I've barely even started, but it already feels like it has potential.
My first task here was authentication. It's an app that you must be logged in to be able to do anything, so this was quite important. And to help streamline things, I've gone for the approach of separating the Email Address entry from the Login / Register forms, so that the system can detect if you're already registered or not and show the correct form.
What this means is that I've got the following React component hierarchy:
EmailEntry component displays a simple form that asks for an Email Address. When the user submits one, it triggers an action to look up the email in the server, and then causes the
LoginRegister component to render either the
Register components as appropriate. In short, the state transitions are:
- undefined => EmailEntry
- PENDING => EmailEntry, but with the loading indication to show that it's working
- EXISTS => Login
- UNKNOWN => Register
So, this all went into Redux and it all worked. The
EmailEntry component dispatched the
checkEmailAddress action. This caused Redux Saga to trigger, which:
- Dispatches the
- Makes the API call
- Dispatches the
checkEmailAddress_SUCCESSaction with the payload of
- Dispatches the
Reducers are then set up for the
checkEmailAddress_SUCCESS actions to update the store values for
emailStatus as appropriate.
LoginRegister component is then set to react to the
emailStatus value and render as appropriate.
This is all very simple Redux. But it's also a lot of code. And almost all of this is very specific to this specific hierarchy of components. Nothing else in the application cares about the fact that we're checking an email address, what the email address is or what the status of the check is. And yet, it's in the global store for everything to see.
So, I re-wrote it. I ripped Redux out entirely and instead wrote the following:
- A simple module called
checkEmailServicethat has a single method -
checkEmail. This takes the email address and returns an
Observablefor the result.
- When the form on the
EmailEntryform is submitted we then:
- Update local state to show that the form is pending
- Call the
checkEmailmethod with the entered address
- Subscribe to the returned
Observable. When it resolves we call a callback provided from
LoginRegisterwith the email address and the result of the API call
- When the
LoginRegistercallback is triggered we update local state with the provided email address and status of it
LoginRegistercomponent then uses this local state to determine which component to render.
This means that:
- The pending flag is local only to the
- The email address and status are local only to the
- There is no global state at all
That already feels cleaner. We've got rid of any global state, which is a huge plus (We all know how bad global variables are. Why is global state any better?)
Sometimes though we do have values that are important to more of the application. For example, the Current User might be important, or the Authenticated Access Token. I've not yet implemented these, but I have two approaches for them in mind.
For the actual global values, I'm going to use a
Subject - specifically a
BehaviorSubject - instead of an
Observable. The service calls can then update this as and when needed, and anything can subscribe to the current value. Access Token is one such value - it starts
undefined, but on authenticating it will be given a value. Anything that needs the current value will then be able to get it from the
getValue, or can subscribe to get notified whenever it changes.
For UI-centric concerns, I'm considering coupling this with the Context API and have a component in the appropriate part of the component tree act as the Provider and subscribe to the
Subject. Whenever the
Subject changes, this component updates it's local value and passes it into the Context API. Anything lower down that needs it can then access it from the Context API without needing to know about the API calls that generated it. This means that there's only a single subscriber to the
Subject that needs to do the updates, and React handles the rest.
All of this seems to give me the majority of Redux functionality without any need for Redux itself.
The bit that's missing is orchestration. The fact that a single dispatched action can cause multiple bits of the store to react. This is also relatively simple to achieve by simply having service APIs that call other service APIs. For example, the act of authentication is:
- Send the Email and Password to the Server, and get back an Access Token and User ID
- Store the Access Token
- Store the User ID as the Current User ID
- Call the server to get the User Details for the Current User ID
Redux allows a lot of this to happen by different parts of the store reacting to the same actions. For example:
authenticate_SUCCESScauses the Access Token Reducer to store the Access Token
authenticate_SUCCESScauses the Current User Reducer to store the User ID
authenticate_SUCCESScauses a Saga to dispatch the
getUseraction with the given User ID
getUser_SUCCESScauses the User Details Reducer to store the user details
All chained off of a single action. That works, but it's difficult to trace through it in the code. Instead, I'm planning on having an
- Calls the
accessTokenServiceto get the Access Token
- Calls the
currentUserServiceto store the User ID
- Calls the
getUserServiceto get (and cache) the User Details
This gives very readable orchestration, and makes debugging and testing it very simple.
Will it work? I don't know yet.
Will it be better than Redux? I don't know yet.
But I fully intend to see how it goes.