react-injectables

Explicitly inject Components into any part of your React render tree.

Usage no npm install needed!

<script type="module">
  import reactInjectables from 'https://cdn.skypack.dev/react-injectables';
</script>

README

Explicitly inject Components to any part of your React render tree

Travis npm MIT License Codecov Maintenance

This is a teeny tiny React library - it's almost unnoticable when gzipped.

What is this for?

Placeholders, Modals, etc.

Overview

Envision you have the following component tree:

<App>
    <Sidebar />
    <Main>
        <Header />
        {renderedRouteContent}			
        <Footer />
    </Main>
</App>

A fairly standard configuration, essentially you have a master application template which each of your routes get rendered in to. This is handy as you get to share things like your Header, Menu, Footer across all your rendered routes without having to repeat all that code. But what if you wanted to extend your base template with additional content that is specific to one of the routes being rendered?

For example, you'll notice the base template holds a handy little Sidebar component. Perhaps you would like a MyBasketSummary to be rendered in there whilst the user is viewing the ProductsRoute? Or maybe you would like to inject a new Button or Image into the Header for one of your routes?

How could you solve these seemingly simple problems?

One option would be to use react-routers native capability to pass down multiple named components for each of the routes into the base template. For the simple cases we recommend this approach, however, with complex applications having deeply nested routes and component structures this approach may be difficult to manage and could end up in complex props pass-throughs.

Enter Injectables.

Injectables aims to provide you with a mechanism to explicity define Injectable and Injector Components. An Injector produces a Component that gets injected into an Injectable.

With Injectables you can easily inject a Component into the Sidebar when your ProductPage Component gets mounted. Here is a partial example of this:

import { SidebarInjector } from '../InjectableSidebar';
import MyBasket from '../MyBasket';

const MyBasketSidebarInjection = SidebarInjector(MyBasket);

class ProductPage extends Component {
  render() {
    return (
        <div>
         {/* MyBasket will get injected into Sidebar! */}
         <MyBasketSidebarInjection /> 
        
         <h1>Product Page</h1>
         ...
        </div>
    );
  }
}

export default ProductPage;

Now every time the ProductPage Component is mounted the MyBasket Component will be injected into Sidebar Component. Ok, there is a bit of additional setup required in the Sidebar Component, but the above is a basic overview of how easy it is to define your injections after the initial configuration.

Yes, a bit of fairy dust is involved, but we attempt to keep things as un-magical as possible, pushing for explictness whilst maintaining ease of use.

Usage

First install the library.

npm install react-injectables

Quick Start

To get going there are only 3 steps:

  1. Wrap your application with our InjectablesProvider.
  2. Wrap a Component you would like to receive injected content with our Injectable helper. e.g. Injectable(MainScreen)
  3. Wrap a Component you would like to inject with our Injector helper. e.g.: Injector({ into: MainScreen })(MyModal)

Full Tutorial

Ok, here's a more detailed run through with example code.

Step 1

Somewhere very low down in your application wrap your component tree with our InjectablesProvider. This is the engine that will do all the heavy lifting for you. For example:

import React from 'react';
import ReactDOM from 'react-dom';
import { InjectablesProvider } from 'react-injectables';
    
ReactDOM.render((
    <InjectablesProvider>
        <Router>
            ...
        </Router>
    </InjectablesProvider>
 ), document.getElementById('app'));

Step 2

Now you need to create an Injectable Component. Let's say that you would like to create a Sidebar component that you could inject in to. You would do the following:

import React, { PropTypes } from 'react';
import { Injectable, Injector } from 'react-injectables';

// Note the 'injections' prop.  This will contain any injected elements.
function Sidebar({ injections }) {
  return (
    <div className="header">
     {injections}
    </div>
  );
}

// We wrap our Sidebar component with Injectable. This does all the wiring up for us.
const InjectableSidebar = Injectable(Sidebar);

// Create a default Injector configuration for our injectable sidebar.
// Our Components can use this helper to create injections for the sidebar.
// NOTE: We are exporting this helper, it will come in handy for the next step.
export const SidebarInjector = Injector({ into: InjectableSidebar });

export default InjectableSidebar;

Notice we also create a default Injector configuration for our InjectableSidebar called SidebarInjector. This has been exported to allow our other Components to easily import and use this pre-configured helper - it saves us having to repeat this configuration thereby reducing errors.

We recommend naming your component files appropriately to indicate that it is indeed an injectable component. In the above case we named our component file as InjectableSidebar.js. Forming your own conventions around the naming of your injectables and injectors will help.

Step 3

Ok, so now you have an InjectableSidebar Component and a SidebarInjector helper function. Next up you need to declare a Component that will cause an injection into the Sidebar to occur.

import React from 'react';
import { SidebarInjector } from './InjectableSidebar';
import MyBasketView from './MyBasketView';

// Use the SidebarInjector helper to create a Component that will inject the
// MyBasketView Component into our InjectableSidebar Component.
const MyBasketViewSidebarInjection = SidebarInjector(MyBasketView);

class ProductPage extends Component {
   ....
   
   render() {
     return (
        <div>
          {/* The injection happens here, i.e. when the ProductPage gets mounted. 
              Nothing actually gets rendered at this location, the Component gets sent to
             our target Injectable.  In this case it means that MyBasketView will
             be injected into the Sidebar.
             Notice how you can also pass down props into your injected component too. */}
         <MyBasketViewSidebarInjection focusOnProductId={this.props.productId} />
        
         <h1>Product Page</h1>
         
         ...
        </div>
     );
   }
}

export default ProductPage;

And that's it. Any time the ProductPage is mounted it will inject the MyBasketView Component into the Sidebar. When the ProductPage unmounts, it's respective injected Component will be removed from the Sidebar.

As you can see it's all explicit, so you can follow the import references to find out any relationships.

Properties of Injectables and Injectors

Here are a few basic properties you should be aware of:

  • All injection happens within the initial render cycle by react-dom. Injection does not cause a double render to occur on your Injectables. This is a result of us trying to intentionally keep injection as "input to output" as possible.

  • You can have multiple instances of an Injectable rendered in your app. They will all recieve the same injected content from their respective Injectors.

  • You can create multiple Injectors Components targetting the same Injectable component. For example, you may want to pass in action buttons from different components into an InjectableActions component.

  • If an Component that is hosting Injector is unmounted then the injected Components will automatically be removed from the Injectable target.

  • Any new Injectors that are rendered into the tree will automatically have their injected Components passed into any existing Injectable targets. i.e. a props update.

  • Injectors are allowed to be mounted before any Injectables. Once their target Injectable Component is mounted then any Components from existing Injectors will automatically be passed into the newly mounted Injectable.

Examples

At the moment there is only one example, using react-router. Check out the examples folder. I wouldn't recommend running it yet as I have yet to add any style to it, but it will execute if you try. :)

Some other considerations.

I am using redux or another flux-like library

Then perhaps you should try and use their respective action flows in order to control the "injection" of your content in a manner that follows their uni-directional flows.