Make services a natural part of redux architecture

dprovodnikov
ITNEXT
Published in
7 min readNov 17, 2018

--

The approach I am going to describe here is nothing more than a small extension to the default redux architecture. Even though it does not bring a lot of changes to the data flow it does make a meaningful improvement.

What am I trying to achieve?

The main thing I’ve struggled with throughout my engagement with redux is where to put the business logic. As far as your application remains simple you may not come across this issue at all. Redux actually does it’s job pretty well when it comes to relatively small apps. However, as your application grows, things start to get a bit more complicated.

At first, you make asynchronous API requests inside your action creators. This approach is good enough until your API responses have to be parsed in some way. Or, let’s say, you get a data that requires a bunch of transformations before it gets to be displayed. Or you have to make a chain of http requests to get the result you need.Those are signs telling you that it’s time to start decoupling you application. The goal is to move everything that doesn’t have to do with redux flow to a separate place. So the main purpose of what I’m presenting here is to define that place.

An idea of services

Action creators should only dispatch actions. If it is a thunk, it should get data somewhere and then dispatch an action. Action creators do not have to build urls, send http requests, handle responses and the rest of the stuff people tend to put inside them. That is not what they are meant for. Instead, I offer to add one more layer that is going to incapsulate all that. I call it services.

Here’s what the default redux data flow looks like:

Redux flow as we are all used to it

Here’s what it’s going to look like when we add services there:

Redux flow extended with a new services layer

Not that much of a change right? Turns out it’s not that simple as it seems.

What does it take to add another layer?

It seems like an easy task. And it is up to a certain point. You can put you logic somewhere else rather than in action creators, and then import it there for the further usage.

importing the service to use it inside the action creator

Looks fine, but this action creator isn’t testable at all, as there is no way to mock the service.

Second reason why this approach isn’t good enough is because you may have services that need to be initialized before they can be used. Usually the initialization takes some time. This fact itself does not allow you to use an approach like this anymore. Here’s why.

The fact that some services have to be configured asynchronously means that you have to run the configuration somewhere. The place to do it is obviously the entry point of your application.

Configuring services inside of the entry point file

The first weird thing you notice here is that you export stuff from you entry point, which is not a good thing to do. Another thing that we forgot here is that configureServices has to be asynchronous in order to wait until all the services are done. If you make it asynchronous you won’t be able to export services at all.

So what’s the solution?

Let’s address one issue at a time. How to make action creators testable and still receive services from outside? I bet you’ve heard of dependency injection.

src/modules/user/actions.js

Let’s see what is different here. We don’t export action creators right away, instead, we export a function that receives service as a dependency. Now your action creators are completely testable.

It’s not clear if you need your services to be injected to reducers. For example you might want to fill INITIAL_STATE by some data that is taken out from some caching service. You might as well think of another case for that.

To be a bit more consistent we should implement the same DI wrapper for reducers as well.

src/modules/user/reducer.js

Just a plain reducer, but instead of exporting the reducer itself you export a function that returns it. Now it’s time to see the bigger picture.

Project structure

I like how minimalist it looks.

  • src/components. All views are stored there.
  • src/context. It’s a place all globally accessible stuff is imported from. We are going to dive into this a bit later.
  • src/modules. Everything redux-related (actions, reducers, selectors etc.)
  • src/services. A place for your business logic.
Project structure

Let’s take a look inside the entry point.

src/index.js

We wait until services are configured, then inject them in modules. As a result of configuring modules we get the redux store, which, I think, makes a lot of sense. Then we render the view part of the application. We also use a dynamic import to load the root component as the app takes time to initialise, so we have to wait until that process is over.

Modules

I like to think of everything that has to do with redux directly as a module. Here we have only one module — user module.

modules directory that holds all modules divided by folders

As you can see I created a folder named user and put all the redux-related stuff that has to do with users in there. As these parts are closely related to each other I think it’s a good decision to store them together. I’m not going to explain what’s in utils, or what types.js is for. I think you can figure it out by yourselves. Let’s have a quick look inside index.js though.

src/modules/user/index.js

Everything that is returned from here is modules public API. You can add types here if you want to make them accessible from outside.

Now let’s gather all the modules together to make it all configurable from entry point (remember how we called configureModules up above)? So this is that very function.

Just ignore that “registerActions” part for now, we will get back to it later on.

Services. Extracting business logic

Services is an additional layer intended to hold your business logic. You are free to organize you logic here as you like. All you get is a mechanism to make it accessible globally.

src/services

As we did with modules, we are going to call configureServices from the entry point. Here is what that function looks like.

We aren’t going to dive into the implementation of these particular services as I believe it’s easy to get the idea behind them from here. Once again, please, ignore the “registerServices” part.

Context. How things go global?

Let’s recall what the entry point code looks like.

Everything has to make sense right now. If it does not, you’re better off taking a few steps back and look through the explanations one more time. Next thing you’re going to do is you get to meet registries.

Global registries.

Registries enable access to your logic throughout the application. I published an npm package that implements the concept. This package is going to be used in further examples, so it would be better if you took a quick look at it first.

So this is the hard part. Please, take your time to go through this.

Registry solves a specific problem. It allows to delegate the registering process out of the place the registered stuff is exposed at. Sounds like gibberish? Just take a look.

src/context/actionRegistry.js

You create a registry instance. Then you initialise it. As a result you receive a function populateRegistry. This function then is exported out, it will be used to register stuff to the registry.

So nothing gets registered here, you delegate it somewhere else, BUT everything that will eventually be registered is going to be available from this very place.

Here is the service registry. It is literally the same.

src/context/serviceRegistry.js

So these registries represent the context.

src/context folder

Context is an abstraction that holds everything that is available globally. Here is the index file of the folder.

It just takes everything that the registries have exposed and reexports it. Now when any piece of your application needs an access to anything that has been registered it imports it from context.

Here a component imports actions as it would in case with regular redux flow.

Users component imports actions from context

I guess we missed the part where actions actually get registered. Remember I asked to ignore that part when we were looking through the modules? Let’s get back there and deal with it.

Registering actions in src/modules/index.js

You take actions of each module you’ve configured and put into a single object. Then the object gets passed to registerActions. Let’s see the code.

src/modules/register.js

PopulateActionRegistry is linked to a registry instance. It’s just a wrapper to provide you with access to “register” function. This is the only place where you can register stuff. You never get access to the registry instance itself. You either get to register stuff (as here), or to access what’s been already registered (as in context). It’s never both.

Services get registered in the same way

src/services/register.js

That’s it.

We’ve been through all parts of this architecture. Here is what the structure looks like.

This project is available on GitHub.

Thanks for taking time to read this. Let me know what you think. I am very grateful for any feedback. Contact me on dp.wireden@gmail.com.

--

--