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:
- App
- HomePage
- LoginRegister
- EmailEntry
- Login
- Register
The 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 Login
or 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
checkEmailAddress_STARTED
action - Makes the API call
- Dispatches the
checkEmailAddress_SUCCESS
action with the payload oftrue
orfalse
- Dispatches the
checkEmailAddress_FINISHED
action
Reducers are then set up for the checkEmailAddress_STARTED
and checkEmailAddress_SUCCESS
actions to update the store values for emailValue
and emailStatus
as appropriate.
The 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
checkEmailService
that has a single method -checkEmail
. This takes the email address and returns anObservable
for the result. - When the form on the
EmailEntry
form is submitted we then:- Update local state to show that the form is pending
- Call the
checkEmail
method with the entered address - Subscribe to the returned
Observable
. When it resolves we call a callback provided fromLoginRegister
with the email address and the result of the API call
- When the
LoginRegister
callback is triggered we update local state with the provided email address and status of it - The
LoginRegister
component then uses this local state to determine which component to render.
This means that:
- The pending flag is local only to the
EmailEntry
component - The email address and status are local only to the
LoginRegister
component - 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 Subject
using 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_SUCCESS
causes the Access Token Reducer to store the Access Token -
authenticate_SUCCESS
causes the Current User Reducer to store the User ID -
authenticate_SUCCESS
causes a Saga to dispatch thegetUser
action with the given User ID -
getUser_SUCCESS
causes 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 authenticationService
which:
- Calls the
accessTokenService
to get the Access Token - Calls the
currentUserService
to store the User ID - Calls the
getUserService
to 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.
Top comments (3)
Thanks. I used RxJS. I think it simple than Redux. Because you need to be too much thing if you're working with the Redux. :)
I added my reading list :) Thanks again.
If be interested in knowing how your transition to rxjs went... Any updates?
I'm about to start a new React app, and I'm seriously considering trying this after experiencing the benefits on a large Angular app.
And he never came back..