Dependency Injection with React Context

Introduction to Dependency Injection and React Context

Erin Zimmer begins by introducing the main topics of the discussion: dependency injection and React context, focusing on the encapsulation benefits of writing React components. She explains the customization challenges that arise from encapsulating everything within components and sets the stage for discussing solutions, like the use of props and dependency injection as a method for providing variables to components.

Understanding Dependency Injection

The speaker delves deeper into dependency injection, explaining it as the process of giving components their local variables, akin to giving objects their instance variables in object-oriented programming. Erin highlights the simplicity and elegance of props as a solution for dependency injection and quotes James Shore on the concept being a "fancy term for a simple concept." She clarifies how not all props qualify as dependencies and elaborates on different examples to distinguish these relationships.

Beyond Props: Other Methods of Dependency Injection

Erin discusses alternatives to props for dependency injection, such as using properties on function objects to avoid "prop drilling." She explains the issues with prop drilling, where props are passed needlessly through multiple layers of components, and illustrates how other methods can streamline dependencies. Despite such tactics, she outlines the downsides, including lack of central control and unconventional code-writing styles, and segues into a more integrated solution with React context.

Designing a Centralized Dependency System with React Hooks

The speaker introduces a more React-style approach to dependency management using hooks. She outlines creating a centralized container component for dependencies, which can be accessed via hooks by the child components. Erin provides a walkthrough of code designed to achieve this setup, leading to an "aha" moment where she reveals this mimics React context. This section closes with a light take on "reinventing" React context.

Leveraging React Context for Dependency Injection

Erin continues to explain React context as a natural mechanism for dependency injection within React applications. She points out the benefits of using built-in support, while also highlighting the potential issue of discoverability when contexts are misapplied. Solutions for mitigating these risks, alongside a discussion on third-party libraries like Obsidian and Wox Inject for added functionality, are explored.

Handling Dependency Discoverability and Testing

The speaker suggests best practices for managing discoverability in React context. By avoiding default values during context creation, developers can catch missing providers during runtime tests. Erin also introduces libraries for dependency injection in testing environments, focusing on React Magnetic DI. She details how this tool enables developers to mock dependencies efficiently during testing and emphasizes its utility without altering production code.

Conclusion and Key Takeaways

Erin Zimmer summarises the key points discussed during the presentation. She reiterates how dependency injection aids in customizing and encapsulating React components. Erin confirms React context's role as a method of dependency injection—advocating its use despite the discoverability challenges—and highlights how React Magnetic DI presents an elegant solution for testing scenarios. The talk concludes with an invitation to access the slides and reach out for further discussion.

Hi.

So yeah, we're gonna talk about dependency injection and React context.

We'll start with dependency injection.

So when we're writing React, we write components and we write components because we like encapsulation.

So here we have three components and alert a button.

And the feed component here is a social media feed, because I didn't wanna write a Netflix clone.

So it's just a list of posts and because the behavior of these components is completely encapsulated, this makes it easier to reason about them.

All of the irrelevant details are abstracted away.

It's easy to think about them, and it also means that we can use these components anywhere in our app that we want.

We don't have to worry about they're gonna, whether they're gonna work or not.

'cause they have everything they need encapsulated.

So that's pretty good.

The downside of this is that if you encapsulate everything in your component, then it's really hard to customize.

And there's a bunch of reasons we might wanna customize these components.

With our alert component, we might wanna change the color, so we want it to be red when it's an error and blue and it's info.

Or we might wanna change the text so that we can reuse the same alert with multiple different messages.

With our button, we might wanna change the onClick behavior so that we don't have to create a new button component every time we wanna click something.

We also might wanna change the way that the data fetching works.

So with our feed component, we might wanna fetch the data differently in different situations.

So if the user isn't logged in, we might wanna show them a curated list of publicly available posts.

And if the user is logged in, we might wanna do something wild and just show them a list of posts from people they've subscribed to in reverse chronological order.

Does anyone remember when social media did that madness?

We also don't wanna change the behavior, some of the child components.

So again, with our social media feed components, when the user is logged in and they click on, then we wanna update the state.

We wanna like the component, but if they're not logged in, we probably wanna pop a login prompt so that they can do it.

We might also wanna customize non-functional requirements.

So this is particularly important if our component is being used, is part of a library and could be used in multiple applications.

We might be using completely different libraries for logging in different applications, so we need to be able to customize that.

But even if our component's only being used in one app, we're still going to wanna send different analytics depending on whereabouts it's used.

Finally, we might wanna customize things for testing.

So for instance, when we're running our component in a unit test environment, we need to customize the way that we handle HTTP requests 'cause that's not gonna work.

So if we completely encapsulate all the component, all the behavior of our component, we can't do any of this.

The good news is, though, that we have a solution for this.

We have props.

If we wanna customize a thing, we can just externalize it.

We can expose it via props, and then we can customize it as much as we like.

That's it.

Problem solved.

Also, this is just dependency injection.

We're done.

So James Shaw, in his article Dependency Injection Demystified says, dependency injection means giving an object.

Its instance variables.

So dependency injection comes from object-oriented programming, which is why it's referring to objects, in essence, variables.

We tend not to do object oriented programming and React these days.

But I'm sure James wouldn't mind if we fudged his definition a little bit and said dependency injection means giving a component its local variables.

So any situation where we're passing something into the component is dependency injection.

If this seems to you like, we're using a very complicated term for something very simple, James agrees.

He says it's a $25 term for a 5 cent concept, which sure.

It's a little bit more complicated than that though, for a couple of reasons.

So firstly, generally speaking, not all props are considered dependencies.

This one's more just a terminology thing.

But usually we'd only consider something dependency if it's gonna affect stuff outside the component.

So with our alert component here, we have a status prop, which is gonna change like the color and the icon that we show.

And we have a children prop, which is gonna control the text that's displayed.

And both of these things are really just concerns of the component itself.

Nothing outside cares about them, so we wouldn't call these dependencies.

Similarly with our button component, it has a type property which is gonna control what it looks like, the color borders, hover states, that kind of thing.

It has a children property, which is gonna control the text that's on it.

And it has this onClick property, which is gonna be the function that gets called when it's clicked.

All of these things are, again, just concerns of the component, but, the onClick handler might do something that affects things outside the component.

It might make an API call or something, so it has a dependency, but it's not really a dependency.

I, you could go either way on that one.

And finally, our feed component.

This one is, this is all dependencies, right?

The theme is something that's coming from outside the component.

All of the theme needs to be the same across the whole application.

The list of posts is gonna be coming from an API call, or GraphQL or a Redux or some kind of store somewhere.

The like and comment actions are going to update the state of the posts, or they're gonna change the state of the app because we're gonna show different components.

And the analytics service is gonna be making HTTP calls all over the place.

These are all dependencies.

The, thing is though, that dependency injection isn't just like fancy props because not all dependency injection is done via props.

We can also do it with something like this.

So here we're passing the dependencies in as a property, on the function object.

And then we can access it just via normal dot notation.

So this is not props, it is still dependency injection via things that aren't props.

It does kind raise the question like, why, would you do this?

This is weird.

And the answer to that is mostly prop drilling.

So if we look at our feed component here, you can see that it takes five props, but inside the code it uses one prop and the other four props all just get passed to its children.

And we then look at one of the child components like the the post component there.

You can see that it also takes five props.

It doesn't use any of them, it just passes all of them to its children.

So if we have even a moderately complex component tree, we end up passing these props around just so that we can pass them around so that we can pass them around some more.

So this makes our component, our code messy.

It makes it hard to reason about, and it breaks encapsulation because suddenly our components need to know about the implementation details of their children so that they know which props to pass to them.

So something like this solves that problem.

We can now just pass the dependencies that each component needs directly to that component, and we don't need to do all that prop drilling.

This particular solution has a couple of downsides though.

So firstly, the dependencies are all just managed on a component by component basis.

There's no sort of central thing controlling all of the dependencies together, which means that each component is just gonna get whatever dependencies it gets given.

So for instance, here, this theme dependency, this needs to be the same in all of the components.

And if we had something central controlling that, we can ensure that.

But the way it's written here.

There's nothing ensuring that those are the same.

There's not even anything suggesting that they should be the same.

The other issue with this is that it's weird.

This is not the way that you normally write React, right?

Unless you write prop types.

Who still writes prop types?

Nobody.

Okay, so this is weird.

Nobody writes code like this.

Use Typescript.

So what we want is, instead a more Reactive solution.

We want something that means that it's gonna, we can have a central place controlling all of our dependencies, and we want a "Reacty" way to be able to access those dependencies.

And what could be more Reacty than hooks?

So we wanna build something that gives us a central place to control our dependencies and a way for our components to access the dependencies via a hook.

So we want it to end up looking something like this, is our goal, this is what we're aiming for.

We've got our container at the top here that controls our dependencies.

We're gonna pass the dependencies in as props to that container.

And then inside the container, all of the, child components and like all of the descendant components, can access the dependencies via some kind of hook, right?

So we wanna write code that lets us do this.

So the first thing that we're gonna need is a function that can create the container component and also expose the prop, the value of the dependencies so that our hook can use it.

So that function looks something like this.

It is got a local variable here, which is gonna store the value of our dependencies, and then it's gonna return this object and this object is gonna return that dependencies object and it's also gonna return our container component.

The container component, as we saw, takes the dependencies as props.

Then it just updates the local variable, right?

So we need to do this bit of indirection where we're using the value property so that when we return the dependencies, variable in the object, when we update the value, it's also gonna be updated in the dependencies, the return value there.

Then we return the children.

Once we've got this, then we can create our hook, which looks something like this.

So this is gonna take one argument.

The argument is gonna be the object that's returned from our initial function, and then it's just gonna return that value.

Cool.

So when we go to use it, oh, we got some the things I just said, but with the code highlighting synchronized.

So when we go to use it, we're gonna first create our dependencies object by calling createDependencies.

And then we can use the container component that we get back and we can pass in whichever dependencies we need for the particular situation that we're using this in.

For instance, here, this is our public feed component.

So we're gonna pass it the public posts and the login prompts, but we could also just use it in another, the, chronological feed, the theoretical chronological feed, where we pass it, the actual post that the user wants to look at, and we allow them to like and comment.

Then inside of our components we call our use dependencies hook, and we pass it that same object that we, passed to the, that we got back from the function originally, and we get access to our dependencies.

Cool.

We did it right.

It works.

Thing is though, if we maybe squint a little bit, maybe changed in variable names.

Like maybe instead of calling this createDependencies, what have we called it createContext?

And instead of calling this a container, what have we called it?

A provider.

And what if instead of calling this useDependencies, we then called that useContext and we just invented React context.

And if you're wondering, yes, this is a high res image of Pikachoo that I have blurred with CSS so that it fits with the slides.

'cause that's what I do with my spare time.

So React context is dependency injection.

It is a way of getting variables into a component.

So it's dependency injection.

It turns out you were probably doing it all along, whether you like it or not.

So there are some big advantages to using React context to do your dependency injection.

The main one is that it's built in, it just works.

It's part of React.

You already know how to do it.

You don't have to learn anything new.

You don't have to import a library.

It all just works fine.

The big downside is that it's got really bad discoverability, There's nothing to stop you from doing this, which is rendering our feed component without rendering the provider around it.

React won't throw an error.

It'll just work right up until the point where it doesn't work, which in this particular case will be when you try to make an HTTP call, but you don't know when it's gonna break unless you know what's in the context and you don't know if this component has like descendant components that also rely on other contexts, that might also break at some point, but you don't know when, so you can't rely on your tests anymore.

You don't know if they're accurately reflecting what's going on.

So what can we do about that?

There's a couple of options.

The first one is.

Don't use context.

There, like I said, there's a bunch of different ways that you can do dependency injection and there are, I wanna say a bunch of libraries out there to help you do that.

I found three and one of them hadn't been touched in seven years and didn't seem like it worked.

So I found two libraries that you can use to potentially do fantasy injection and they are Obsidian and Wox Inject.

So let's have a quick look at how they work.

Obsidian uses, annotations.

So to mark your dependencies, so every dependency in your application, you need to wrap it in a class and then you need to add the class to the dependency graph, which is all annotated with annotations to say that's the dependency graph.

Then to use it, you need to do a couple of things.

So you can inject things into hooks or components.

But when you do, you need to make sure that you destruct the props so that the names are available 'cause it's the name of the variable that it's gonna match on.

Then when you export your component or your hook, you need to wrap it in this call to inject hook or inject component, which is just gonna match up the dependencies with the props.

It's kinda like a higher order component, but a higher order hook, which sounds very Christmasy like hoe, no.

So the docs claim that this approach helps reduce the amount of boilerplate code required by developers, which I feel is a bold claim for something that requires that you wrap every dependency in a class, but you can make that decision for yourself.

It does absolutely solve our problem of discoverability, though, because we just have a single dependency graph.

We can see what's in it.

We know which things we need to inject.

Problem solved.

The downside is that it reduces the flexibility, right?

We can't do what we were trying to do earlier.

If we wanted to like sometimes pass in the public post and sometimes pass in the user posts, we kind need to make two separate dependencies for those two things and then have some kind of config that figures out which one we want to use and switch between them.

I, they didn't seem like a good way to do this that I could find.

Flexibility though is a two-edged sword.

For my particular use case, the app I work in, the flexibility is important.

This isn't gonna work for me, but for you, the static, the fact that it's statically analyzable and you can see what's going on easily, there might outweigh the loss of flexibility.

So it could go either way with that one.

I just don't like it.

There's also no built in solution for replacing mocks during or like for, mocking dependencies.

In testing you need to use in, jest mocks.

I hate jest mocks for reasons we'll get into, but if, you like them.

Have at it?

So this could be a good solution if you have a dependency tree that you particularly want to, have it static to be able to see what's going on there, to have everything really explicit.

Or if you just really mis writing angular code and you want more annotations in your code.

The other library I found is called Wox Inject.

I just say I am deeply suspicious of this naming situation because Obsidian is created by Wix, like WIX, and this one is Wox and neither of those things are words.

So, my story that I my like head canon on this is that there was a massive fight in the Obsidian community and there was a big rift and the developers ran off and created ox.

I couldn't find any evidence to back that up.

I did look so you can make up your own story if you want, but that's mine.

You're free to use it.

So Wox also uses annotations to mark dependencies.

So they're also wrapped up in classes like this.

You don't need to put them all into a big graph though.

You're just marking individual dependencies.

And then when you want to use it, you just use this use resolve hook.

And it does some magic reflection behind the scenes and figures out how to inject things.

So it's a lot less boiler plate I might say than Obsidian.

So this one though, it doesn't really solve our problem of discoverability.

'cause these dependencies could still be anywhere and you don't know where they're coming from.

I couldn't figure out from reading the docs how to customize it like we wanted to do in this situation.

If we wanna pass in a different handler for the button in different dependencies, different situations.

I don't know how that's supposed to work.

Maybe it's in the docs somewhere.

It does have a nice test bed so you can easily replace dependencies for, testing.

But it also relies on sort of other libraries.

So if you're using TypeScript, the reflection doesn't work outta the box and you've gotta set that up yourself.

It also only works with Vite, so that's fine.

I guess if you're using Overall this one felt to me like it wasn't quite prod ready.

It is still being actively developed though, so maybe it's good.

Maybe you have a look at it, but it seems like something that could work for you.

But I don't use it.

So what do I do instead?

To solve the problem of having context potentially all over the code, I just do something which I think is considered as best practice with context in general, and that is that when you're creating your context initially, don't pass it at default value.

Pass it some kind of non-value.

I like to pass undefined.

But if you prefer, you can use null.

I know some people have strong feelings about that.

But whichever works fine.

Once you do that, then when you use the, when you call, useContext inside your component, if it hasn't been wrapped in the appropriate provider, then it'll return undefined and you can just add a check in there that's gonna say, Hey, we're not inside a provider.

And you can throw an appropriate error.

And it'll just tell you what's going on.

If you are using TypeScript, this works particularly well because it'll force you to do that check.

It's not quite like compile time checking to tell you that things are missing, but it does mean that there's no way that you can run your tests with missing providers and not know about it.

You might have to run them a couple of times.

You get all the providers if you've got a lot, but you will get there in the end.

So that's how I do things.

So that does just leave one question.

Which is, what about dependencies that aren't exposed via context or props or properties on function objects or whatever.

So I'm talking about this kind of situation.

So here we have our login component, and it relies on this dependency here, the login service.

And this isn't exposed anywhere.

So I can't write this test, right?

I've got no way to get access to that login service, so I can't test that it's being called correctly.

There are a couple of things we could do here.

One is that we could expose it as a dependency, as a prop or as a context.

And if we're using Obsidian, that's what they say is absolutely the way that you should do it.

Everything should just be exposed.

To me, that feels a icky.

There is no reason for this login service to change anywhere in the code.

You're always gonna be calling the same service.

It's not like it's gonna be different in a logged in scenario versus a not logged in situation because you're not gonna call it when you're logged in.

That would be weird.

So like adding it as a prop or as context, that feels gross to me.

I wouldn't do that.

The second option is that we can just usjest mocksks to, to mock it.

And like I said, I hate writing jest mocks.

I can never remember how it works.

And I always end up wanting, like to declare a variable before the mock.

And you can't, the mocks have to be the first thing in the file.

And thinking about it now, maybe I just need to use copilot more and it will remind me how to do it.

But, assuming that's not the answer, there is another solution and it is another library.

This one's called React Magnetic Di.

So React Magnetic DI is a little bit different to the other two libraries we looked at in that, first of all, I have actually used this one.

I use this every day.

And we use it in testing at Atlassian for products like Jira.

All across, all around the place.

The second thing is that this library is only intended to be used in a test scenario, so you can use it when you're writing your unit tests.

You can use it for creating storybook stories.

You can even use it if you need to do something weird to get your local dev environment working properly.

But you can't use it in production.

You can use it in production.

There's a special flag that you have to override that's don't do this.

So you can, but you shouldn't.

So how does it work?

So when we're writing our test and we want to mock a dependency.

We just need to call, we need to wrap it in this call to injectable.

So we pass in two arguments.

The first one is the thing that we want to replace.

So in this case, the login service.

And the second thing is what we wanna replace it with.

So in this case, just the Jest mock function.

Then when we render the component, we need to wrap it, we need to pass it this wrapper.

The wrapper is gonna be this dependency injection provider, which is just comes from the library and we pass it the list of our mocks that we're injecting.

And then we can just run our test.

We can just test that the mock was called with the correct arguments and everything works great.

It's very cool.

So the way that this works under the hood is that at build time, the library goes through and it finds all of the dependencies.

So in this case, our login service, and it replaces them with a call to this underscore di function.

And then it just calls the return from that function instead.

Then at runtime, when we call injectable in our test, what this does is adds the arguments to a map.

So the login service is gonna be the key and the fun, the, mock is gonna be the value that's returned from the map.

Then when our component runs, it calls underscore di, which looks up the map.

So it's going to pass in the actual value of the login service, and it's gonna get back the Jest function.

So then it's just gonna call the Jes function and our test is gonna work the way we want, which I think is very clever, but.

A little bit hacky, which is why we only use this in test environments, but it is pretty handy.

So it does rely on build time tooling out of the box.

It works with Babel, but you can customize it to work with other build tools.

It can replace anything that you export, so you can use it to replace dependencies like this.

You can also use it to replace components.

If you have a complicated render tree in a test and a bunch of it isn't relevant, you can probably speed up your test by just mocking out the components that you don't care about.

It does generally work best with classes and functions.

You can replace primitive values, but you need to remember that if you replace it in one place, it's gonna replace it everywhere.

So if you replace the number four, it's gonna replace all of the number fours.

So it'd probably be okay with strings 'cause those are generally pretty unique.

But I would highly recommend not replacing false, for instance.

Generally it's recommended that you use it with functions and if you have values to create a getter function that you can then mock.

The best thing about it though is that it doesn't require any library specific code.

Obviously it requires library specific code in your tests, but if you have an existing code base, you can start writing tests using this library without making any changes to your production code, which I think is super neat.

Okay, that was everything I wanted to cover today.

So we talked about dependency injection is a way of allowing us to customize our components while still maintaining good encapsulation and anything that means passing a value or variable into a component, we can, call it dependency injection.

By that definition, React context is dependency injection, but there are a bunch of other ways you can do it too.

React context is a good way to do it though because it's built in.

You don't have to learn anything new, no additional downloads.

It does have the downside though, of being, having that problem with discoverability, which we can solve by making sure that we always initialize our context with some kind of non-value.

Oh, that didn't get the syntax highlight, did it?

Oh And finally, if you have dependencies in your libraries that aren't exposed anywhere, or honestly, even if they are React Magnetic DI is a super easy way to mock dependencies.

Yeah, so if you wanna grab the slides, they're at the QR code, or you can do it the old fashioned way at di.ez.codes.

If you wanna get in touch with me, I don't know, LinkedIn, you come, you could come talk to my face, if that's, if you're into that kind of thing.

And yeah, thank you very much.

ENCAPSULATION

<Alert />
<Button />
<Feed />

ENCAPSULATION

<Alert />
<Button />
<Feed />
Image of a young person giving a thumbs-up next to a computer screen.

CUSTOMISATION

  • Change the colour or text
  • Change the onClick behaviour
  • Change data fetching or state management
  • Change behaviour of child component
  • Change logging/analytics
  • Replace services for testing
<Alert status="error">Something went wrong</Alert>
<Button type="primary" onClick={...}>Save</Button>
<Feed
  theme="dark"
  posts={fetchPublicPosts()}
  onLike={showLoginModal}
  onComment={showLoginModal}
  analyticsService={analyticsService}
/>
Dependency injection means giving an object its instance variables.

Dependency injection means giving an object a component its instance local variables.

- James Shore, Dependency Injection Demystified

Dependency injection means giving an object a component its instance local variables.

a 25-dollar term for a 5-cent concept

NOT ALL PROPS ARE DEPENDENCIES

NOT ALL PROPS ARE DEPENDENCIES

<Alert status="error">Something went wrong</Alert>

NOT ALL PROPS ARE DEPENDENCIES

<Alert status="error">Something went wrong</Alert>
Illustration of a warning icon next to a code snippet with error alert.

NOT ALL PROPS ARE DEPENDENCIES

<Alert status="error">Something went wrong</Alert>
<Button type="primary" onClick={...}>Save</Button>
Two code snippets illustrating usage of 'Alert' and 'Button' components displayed on a dark background with an emoji crossing arms beside the first snippet.

NOT ALL PROPS ARE DEPENDENCIES

<Alert status="error">Something went wrong</Alert>
<Button type="primary" onClick={...}>Save</Button>
Slide with two code snippets illustrating alerts and buttons. Beside each code block is an emoji indicating confusion or error.

Not All Props Are Dependencies

<Alert status="error">Something went wrong</Alert>
<Button type="primary" onClick={...}>Save</Button>
<Feed
  theme="dark"
  posts={fetchPublicPosts()}
  onLike={showLoginModal}
  onComment={showLoginModal}
  analyticsService={analyticsService}
/>
Image with code snippets in boxes and two emoji characters, one crossing arms and another shrugging.

NOT ALL DEPENDENCY INJECTION IS PROPS

function Feed() {
const { theme, posts, onLike,
onComment, analyticsService } = Feed.dependencies;
// ...
}
Feed.dependencies = {
theme: getTheme(),
posts: fetchPublicPosts(),
onLike: showLoginModal,
onComment: showLoginModal,
analyticsService
}
function Feed({ theme, posts, onLike,
    onComment, analyticsService }) {
    return (
        <FeedContainer theme={theme}>
            {posts.map(post => (
                <Post
                    post={post} theme={theme}
                    onLike={onLike} onComment={onComment}
                    analyticsService={analyticsService}
                />))}
        </FeedContainer>
    )
}
function Post({ theme, onLike, onComment, analyticsService, post }) {
  return (
    <PostContainer theme={theme}>
      <Content theme={theme} post={post} />
      <LikeButton
        theme={theme} onLike={onLike}
        analyticsService={analyticsService} />
      <CommentButton
        theme={theme} onLike={onLike}
        analyticsService={analyticsService} />
    </PostContainer>
  )
}
function Post() {
// ...
}
Post.dependencies = { theme: getTheme() }
function LikeButton() {
// ...
}
LikeButton.dependencies = {
theme: getTheme(),
analyticsService,
onLike: showLoginModal
}

HOOKS

function Feed() {
  const {
    theme,
    posts
  } = useDependencies();
  // ...
}
function LikeButton() {
  const {
    theme,
    onLike,
    analyticsService
  } = useDependencies();
  // ...
}
const feedDependencies = createDependencies()
const feedDependencies = createDependencies() export function PublicFeed() {
    return (
      <feedDependencies.Container
        deps={{
          theme: getTheme(),
          posts: fetchPublicPosts(),
          onLike: showLoginModal,
          onComment: showLoginModal,
          analyticsService
      }}
      >
        <Feed />
      </myDependencies.Container>
    )
  }
export function ChronologicalFeed() {
  return (
    <feedDependencies.Container
      deps={{
        theme: getTheme(),
        posts: fetchUserPosts(),
        onLike: postLike,
        onComment: postComment,
        analyticsService
      }}
    >
      <Feed />
    </myDependencies.Container>
  )
}
export function LikeButton() {
  const {
    theme,
    onLike,
    analyticsService
  } = useDependencies(feedDependencies)
  // ...
}
const feedDependencies = createDependencies();
export function LikeButton() {
  const {
      theme,
      onLike,
      analyticsService
  } = useDependencies(feedDependencies)
  // ...
}
export function LikeButton() {
  const {
    theme,
    onLike,
    analyticsService
  } = useContext(feedDependencies)
// ...
}
A slightly blurred illustration of Pikachu with yellow fur and red cheeks.

REACT CONTEXT IS DEPENDENCY INJECTION

React Context

  • Built in
  • Discoverability
it('foo', () => { render(<Feed />) //... })

LIBRARIES

Obsidian
wox-inject

OBSIDIAN

@Singleton() @Graph()
class ApplicationGraph extends ObjectGraph {
    @Provides()
    httpClient(): HttpClient {
        return new HttpClient();
    }
    @Provides()
    biLogger(httpClient: HttpClient): BiLogger {
        return new BiLogger(httpClient);
    }
}

OBSIDIAN

const useButtonClick = ({ biLogger }: Injected): UseButton => {
  const onClick = useCallback(() => {
    biLogger.logButtonClick();
  }, [biLogger]);
  return { onClick };
};
export const useButton = injectHook(
  useButtonClick,
  ApplicationGraph
);

This approach helps reduce the amount of boilerplate code required by developers

- The Obsidian Docs

discoverability   
> flexibility  
Jest mocks  
@Singleton() @Graph()
class Feed extends ObjectGraph {
    @Provides()
    publicPosts(): Post[] {
        return new PublicPosts();
    }
    @Provides()
    userPosts(): Post[] {
        return new BiLogger(httpClient);
    }
    @Provides()
    appConfig(): AppConfig {
        return new AppConfig();
    }
}
@Provides()
appConfig(): AppConfig {
    return new AppConfig();
}
@Provides()
posts(): Post[] {
    return appConfig.algorithm === user ?
        publicPosts() :
        userPosts();
}
  • discoverability
  • flexibility
  • > test via Jest mocks

WOX-INJECT


@Injectable()
class GreeterService {
    greet(val: string) {
        console.log(val);
    }
}
function App() {
    const greeterService = useResolve(GreeterService);
    return (
        <button
            type="button"
            onClick={() => greeterService.greet('hello!')}
        >)
}
@injectable()
class GreeterService {
    greet(val: string) {
        console.log(val);
    }
}
function App() {
    const greeterService = useResolve(GreeterService);
    return (
        <button
            type="button"
            onClick= { () => greeterService.greet('hello!') }
        
@injectable()
class GreeterService {
  greet(val: string) {
    console.log(val);
  }
}
function App() {
  const greeterService = useResolve(GreeterService);
  return (
    <button
      type="button"
      onClick={
        () => greeterService.greet('hello!')
      }
    />
  );
}
discoverability  
> customisation  
nice testbed  
requires other libraries
not quite prod-ready?  
@Injectable()
class Posts {
    onLike(postId: string) {
        return doLike(postId);
    }
}
function LikeButton() {
    const handleLike = useResolve(Posts);
    return (
        <button onClick={handleLike}>❤️</button>
    );
}

doesn't solve discoverability
customisation?
nice testbed
requires other libraries
> not quite prod-ready? ‍♂️

const feedDependencies = createContext({
    theme: getTheme(),
    getPosts: fetchPublicPosts,
    onLike: showLoginModal,
    onComment: showLoginModal,
    analyticsService
});
const feedDependencies = createContext({
 theme: getTheme,
 getPosts: fetchPublicPosts,
 onLike: showLoginModal,
 onComment: showLoginModal,
 analyticsService
 });
const feedDependencies = createContext(undefined)
Image showing two blocks of code with a red X over the first block. The top block initializes a constant with several functions and themes, while the bottom block initializes it with undefined.
function LikeButton() {
  const dependencies = useContext(feedDependencies);
  if (!dependencies) {
    throw new Error(
      'LikeButton requires DependenciesContext'
    )
  }
  const {
    theme,
    onLike,
    analyticsService
  } = dependencies;
  // ...
}

What about dependencies that aren't exposed?

function LoginForm() {
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")
  const onSubmit = () => {
    if (username && password) {
      login(username, password)
    }
  }
  return (
    <form onSubmit={onSubmit}>
      <input value={username} onChange={setUsername} />
      <input type="password" value={password} onChange={setPassword} />
    </form>
  )
}
it("calls the login service", () => {
  render(<LoginForm />)
  // fill in form
  expect(login).toHaveBeenCalledWith(username, password)
})

REACT-MAGNETIC-DI

injectable, DiProvider
} from "react-magnetic-di";
import { login } from './services'
it("calls the login service", () => {
  const loginDi = injectable(login, jest.fn())
  render(<LoginForm />, {
    wrapper: (props) =>
      <DiProvider use={[loginDi]} {...props} />,
  })
  // fill in form
  expect(loginDi).toHaveBeenCalledWith(username, password)
})
function LoginForm() {
//...
const onSubmit = () => {
if (username && password) {
login(username, password)
}
}
//...
}
function LoginForm() {
    //...
    const onSubmit = () => {
        if (username && password) {
            login(username, password)
        }
    }
    //...
}
function LoginForm() {
    //...
    const onSubmit = () => {
        if (username && password) {
            const loginDi = _di(login)
            loginDi(username, password)
        }
    }
}
const loginDi = injectable(login, jest.fn())
const loginDi = injectable(login, jest.fn())
const loginDi = _di(login)
loginDi(username, password)
  • Build time (Babel)
  • Can replace any export
  • Best with classes & functions
  • No library-specific code

SUMMARY

Customisation + Encapsulation
> React context == DI
createContext(undefined)
react-magnetic-di

SUMMARY

Customisation + Encapsulation

React context == DI

createContext(undefined)

> react-magnetic-di

THANK YOU

di.ez.codes
A QR code on the right side of the slide.
  • fetch
  • onClick
  • click handler
  • Redux
  • dependencies prop
  • createContext
  • useContext
  • HTTP call
  • jest mocks
  • useContext
  • Babel