Better Programming

Advice for programmers.

Follow publication

Decoupling Your Concerns With Dependency Injection, the Repository Pattern, React, and TypeScript

Build loosely-coupled scalable applications

Leigh White
Better Programming
Published in
6 min readFeb 28, 2022
Photo by Igor bispo on Unsplash

Have you ever been frustrated by the baked-in application logic? Had to rewrite entire components or projects when a provider has changed? Wished that your frontend code wasn’t reliant on a specific data implementation? Been annoyed by relentless questions at the start of an article?

In this post, I’m going to walk through an example of how you can build loosely-coupled applications capable of scale and adapting to future change. We’ll implement a technical design pattern known as the Repository pattern with Typescript, and use dependency injection to build an implementation-agnostic React frontend.

Let’s start by talking about Repositories.

Repositories are classes or components that encapsulate the logic required to access data sources.

You can think of them as a middle man in between your data store and your frontend. They are a representation of your data operations and serve as a contract between the frontend and the data store. You typically have a Repository per application entity and define a set of common methods and their agreed return values. This provides a generic interface for interacting with a data store without specifying any concrete information on how to interact with that data store.

We can use dependency injection tooling to inject the repositories we make into our frontend code.

In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service).

It’s easier to understand dependency injection with examples.

Imagine 💭 you had a React component that fetched some data from some API. You make the call and then if it’s successful you parse it and store it in React state. You pluck out the relevant bits you need to display on the frontend, maybe you pass entire nested objects to other React components and let them handle the data internally.

Now imagine 💭 you need to call an entirely different API (let’s say the company you work for has switched providers and they now store this information in a different data store) and the data structure has completely changed. You shed a tear at all the TypeErrors as your frontend implodes in on itself. You google how to write a letter of resignation.

Now imagine (this is the last time I promise) that your frontend was completely agnostic of this logic.

You agree that a call needs to be made but you don’t care where it’s made to.

You agree the structure of the entities you’re dealing with and you use dependency injection to inject a generic repository that exposes a generic getter.

There’s no concrete logic to be seen, meaning that when something has to change we just need to write another concrete service that adheres to the same rules and the frontend is already built to handle.

You close your google tab and breathe a sigh of relief.

Let’s dive into some code and see it working in a practical example.

Assume that we have an application that is user-facing and we need to build a profile page a user will be met with after they have logged in. We’ll also assume we have one entity we’re concerned with, a User , and we need to display their full name, gender, and date of birth (and for simplicity these are always available).

For a data store, we will assume we have a user service that we can query to get a user by ID. I will use this free API as our first pretend concrete user service and then demonstrate how simply we could switch it out for a second.

I have set up a create react app project with TypeScript , installed inversify and reflect-metadata and have enabled experimental decorators and decorator metadata properties in my tsconfig:

{
"compilerOptions": {
...,
"lib": ["dom", "dom.iterable", "es6", "esnext"],
"types": ["reflect-metadata", "node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
...
}

First thing first is to define some types. We know what data we need from a User so we define:

Now let’s define a really simple Repository generic with a get by id:

Notice how this repository isn’t tied to our entity User in any way — this is the beauty of a generic! When implemented and passed the User type it will map out a data access pattern that looks a little something like:

If we needed another entity type in our application, let’s say orders, we could define another implementation like so:

interface OrderRepository implements IRepository<Order> {
get(id: string): Promise<Order | null>;
}

Now we have a common, agreed data access pattern across all entities, we can build the frontend around this rather than around anything concrete. Before we look at building the repository we need one more type to help us out — a type that represents the shape of the data in the data store.

Going back to our mock API from randomuser.me we can outline the following:

Note that this is a different shape to the User type we defined earlier, this is completely fine as we will build a simple parser that maps data from the concrete shape to the desired shape.

Now we have our types laid out we can implement the IRepository generic and build a concrete user service!

The repository exposes a get method which makes a simple fetch to our API, and parses the data if successful which maps it to our expected User type.

In a real-life application, this would house all of our other data access methods too! We have marked our repository with a decorator from inversify which allows us to inject our repository into the frontend in a moment!

We need one more thing first: a unique identifier for our repository:

InversifyJS supports Symbols as service identifiers — this is useful to avoid naming collisions as Symbols are unique and immutable.

We can now set up our inversion of control container with inversify!

Unpacking the above; we first instantiate a new container that we have imported from inversify. We then bind our unique service key to our concrete implementation. As we want to define the instance of this service once and only once we use the inSingletonScope method (without this a new service would be created every time this dependency is requested).

Okay so now we have a container ready to be consumed let’s make a simple react app. We’ll render a button that says GET USER which when clicked then makes a request to the API and displays it on the screen.

I promise I am actually a professional frontend dev

As per the docs before we need to import the reflect-metadata polyfill into our index.tsx file to work with inversify. We then have an app that looks something like this:

We now can import our container and use the repository we built earlier:

We use our service identifier to ask our container for a User repository but our app doesn’t need to know of the specific implementation, that is all injected at the container level. Once we’ve grabbed our service we know that each repository exposes an async get method and we can use this to pull a user from the API! This design makes it possible to define business logic agnostic frontends — pretty cool right?

To demonstrate how easily we can adapt to change let’s switch out our concrete implementation for another and move our fake data store over to a different provider https://random-data-api.com.

We’ll first look at the shape of the entity in the new datastore:

and define a new parser:

We can now implement IRepository and make a new concrete service like so:

Now heading to our container we can inject this implementation instead:

And the front end is none the wiser!

Hopefully, the usefulness of dependency injection and the repository pattern has been made clear —it could help to eliminate any future headaches before they happen and just generally keep your frontend application easier to maintain and responsive to change.

Thanks for reading!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Leigh White
Leigh White

Written by Leigh White

Senior Frontend Engineer @ Gymshark 🦈

Responses (1)

Write a response