DEV Community

loading...

React without Redux, or How I learnt to embrace RxJS

grahamcox82 profile image Graham Cox ・4 min read

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 of true or false
  • 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 an Observable 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 from LoginRegister 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 the getUser 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.

Discussion (3)

Collapse
itachiuchiha profile image
Itachi Uchiha

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.

Collapse
jakehopking profile image
Jake Hopking

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.

Collapse
moshyfawn profile image
moshyfawn

And he never came back..

Forem Open with the Forem app