Singletons, Instances, Streams, Oh My!

Tl;Dr: https://stackblitz.com/edit/react-ts-rv66f4?file=index.tsx

What is a Service?

Services are simply an instance of something that persists for the lifetime of your application. You can think of them as background processes which your presentational layer consumes.

Services are normally objects that hold their own state and expose behaviours via an API to affect their own state.

Redux actually fits the description of a service. It’s just a single service that you hold all of your state inside.

Why use Services?

Holding a portion of an application’s state and exposing an API which allows you to interact with that specific piece of state is very readable.

Every time I work with Redux, following the trail of events that lead back to state changes is never as straightforward as ctrl + click .

Having the capability to see exactly what your state looks like and inspect all the actions you can use against that state within the same file is incredible.

Show me.

In the example below, I have a factory function that creates an object. This object has an array for its state and an add method which pushes an entry to the state array. The returned object instance can be used as my “service”.

But wait, changing the state here wouldn’t trigger a re-render in React. In fact most frameworks wouldn’t be able to effectively run change detection over the todos array as it’s a reference type.

For this to work, we need to make sure we replace the array with a new array and also need to send out a callback to notify anyone watching this bit of state that it’s updated.

We could build our own machinery to push out callbacks, or we could use a tool like rxjx which provides callbacks-as-a-service (basically like Bluebird but for pub/sub actions).

From here I can consume this with

In fact, this is essentially a mini Redux. It follows the same flux pattern.

You’ll notice that we haven’t introduced any React yet. This is because this is (obviously) just a regular old JavaScript object. The object is responsible for holding some state and exposing an API consume/modify that state.

Nothing about this even relates to front end and that’s actually important. This is testable by itself. It’s so decoupled from anything that I could even publish it as an npm package without consequence.

This service is so decoupled from the presentational layer that it could be consumed by any library. React, Vue, Angular or Svelte. Whatever.

Sure, so how would we consume it in React?

Using hooks, you can see that that on init of my component, I subscribe to my state. When the component is destroyed, I unsubscribe to my state.

You can also use a package like use-subscribable to simplify this somewhat.

We don’t like services though, right?

We use services all the time. Both MobX and Redux classify as “services”. The point of contention is dependency injection.

Angular comes with a DI framework. It’s a magic system that figures out where your services are wanted and puts them there. This is what most people criticise.

React comes with no such system, in fact dependency management is very tricky in React.

If you look critically at the challenges things like Redux aim to solve, you’ll see that (ignoring debugging tools and time travel) it’s about empowering nested components to trigger behaviours elsewhere in the application.

Redux does this by allowing anything in your application to call functions via strings.

Essentially rather than saying

todoInstance.add('Take out the garbage')

You say

store.dispatch({ type: 'TODOS_ADD', payload: 'Do a thing' })

To ensure type safety, we use action creators.

store.dispatch(todos.createAddAction('Do a thing'))

We call functions via a proxy. Anything can call anything. This gives every corner of your application…

Limiting API surface area is good

If your component imports a concrete implementation of something, it’s easy to see what it’s responsible for.

Having the ability to ctrl + click on a function/method and having your IDE show you exactly what it’s doing improves productivity and workflow.

More broadly, limiting responsibility and only including behaviours deliberately, as you need them is a great way to reduce the surface area for bugs.

Being so explicit also improves testability and encourages quality abstraction / code decoupling.

You can do achieve similar outcomes with Redux, but you end up writing so much code.

Last part, React Context

Getting your services into your nested components is not trivial. Property injection is not practical, and there is not DI framework to use to inject things.

React has introduced Context, and we can use that to pass our dependency container around our application.

Check out the following repo to see a more complete, working example:

https://github.com/alshdavid-edu/react-dependency-container

Thanks for reading! If you have feedback please share it. I will amend my article with your input.

Thanks,

Dave