recontainer

Simple dependency injection for TypeScript and JavaScript React web applications.

Usage no npm install needed!

<script type="module">
  import recontainer from 'https://cdn.skypack.dev/recontainer';
</script>

README


recontainer


Light and simple dependency injection for TypeScript and JavaScript applications.


gzip size

Can be used in any Node.js or browser applications. Built-in integration with React.

Why?

In most cases, ES6 modules are efficient way to share dependencies throughout application.

There are some situations though, where you need a single point of configuration to achieve better decoupling between multiple components. One of these cases is server-side rendering, in which you might want to change implementation that depends on browser APIs to something more suitable for a Node.js application.

Moreover, using global instances imported as ES6 module can lead to memory-leaks and bugs on server-side. Take for example a global instance of event publisher, to which application subscribes on each request. Since it is not disposed after each request, any failure to unsubscribe will cause a linear leak of memory.

See server side example in JavaScript

server.jsx

import React from 'react';
import express from 'express';
import { renderToString } from 'react-dom/server';
import { createContainer } from 'recontainer';
import { createInject, ContainerProvider } from 'recontainer/lib/react';

class Greeting extends React.Component {
  render() {
    return (
      <h1>{this.props.greeting}</h1>
    )
  }
}

const inject = createInject();
const GreetingContainer = inject('greeting')(Greeting);

const greetingFactory = container => `Hello, ${container.get('name')}!`;
const app = express();

app.use((req, res, next) => {
  const container = createContainer({ // Container gets disposed after each request
    name: () => 'John',
    greeting: greetingFactory,
  });

  const html = renderToString(
    <ContainerProvider container={container}>
      <GreetingContainer /> {/* <h1>Hello, John!</h1> */}
    </ContainerProvider>
  );

  res.send(html);
});

app.listen(3000, () => {
  console.log(`Listening on port 3000.`);
});

Why not Redux?

Redux is great at distributing state throughout application and decoupling your React components. Although you could share almost any object via store, it is generally a good practice to keep the state serializable. Many redux-related libraries (eg. next-redux-wrapper) will not work if the state couldn't be serialized.

Functions and object instances (eg. Promise) are not serializable, thus we should keep them elsewhere.

Features

  • 100% Type-safe

    When used with TypeScript, recontainer protects you from type errors and allows you to extract the full potential of your editor's code completion features

  • Light-weight

    Total module size is 914 bytes gzipped

  • Simple

Examples

TypeScript

types.ts

export interface User {
  id: string;
  name: string;
}

container.ts

import { ContainerConfig } from 'recontainer';
import { createInject, createContainerHook } from 'recontainer/lib/react';
import { User } from './types';

export interface Dependencies {
  user: User;
  greeting: string;
  greetingProvider: (user: User) => string;
}

const user = {
  id: 'john-doe',
  name: 'John Doe',
};

export const config: ContainerConfig<Dependencies> = {
  user: () => user,
  greetingProvider: () => (user: User) => `Hello ${user.name}!`,
  greeting: container => {
    const greetingProvider = container.get('greetingProvider');
    const user = container.get('user');

    return greetingProvider(user);
  },
};

export const useContainer = createContainerHook<Dependencies>();
export const inject = createInject<Dependencies>();

Greeter.tsx

Using useContainer hook
import * as React from 'react';
import { useContainer } from './container';

const Greeter: React.FunctionComponent = () => {
  const { greeting, user } = useContainer('greeting', 'user');
  
  return (
    <div>
      <h1>{greeting}</h1>
      ID: {user.id}
    </div>
  );
}

// OR
const Greeter: React.FunctionComponent = () => {
  // Note that useContainer returns container instance when called without arguments
  const container = useContainer();
  
  return (
    <div>
      <h1>{container.get('greeting')}</h1>
      ID: {container.get('user').id}
    </div>
  );
}

export default Greeter;
Using inject
import * as React from 'react';
import { inject } from './container';

interface GreeterProps {
  greeting: string;
  user: User;
}

const Greeter: React.FunctionComponent<GreeterProps> = ({
  user,
  greeting,
}) => (
  <div>
    <h1>{greeting}</h1>
    ID: {user.id}
  </div>
);

export default inject('greeting', 'user')(Greeter);
Using withContainer
import * as React from 'react';
import { withContainer, ContainerProps } from 'recontainer/lib/react';
import { Dependencies } from './container';

interface GreeterProps extends ContainerProps<Dependencies> {
  
}

const Greeter: React.FunctionComponent<GreeterProps> = ({
  container
}) => (
  <div>
    <h1>{container.get('greeting')}</h1>
    ID: {container.get('user').id}
  </div>
);

export default withContainer(Greeter);

App.tsx

import * as React from 'react';
import { createContainer } from 'recontainer';
import { ContainerProvider } from 'recontainer/lib/react';
import { config } from './container';
import Greeter from './Greeter';

const container = createContainer(config);

export const App: React.FunctionComponent = () => (
  <ContainerProvider container={container}>
    <Greeter />
    {/* 
        <div>
          <h1>Hello John Doe!</h1>
          ID: john-doe
        </div>
    */}
  </ContainerProvider>
);
JavaScript

container.js

import { createInject, createContainerHook } from 'recontainer/lib/react';

const user = {
  id: 'john-doe',
  name: 'John Doe',
};

export const config = {
  user: () => user,
  greetingProvider: () => user => `Hello ${user.name}!`,
  greeting: container => {
    const greetingProvider = container.get('greetingProvider');
    const user = container.get('user');

    return greetingProvider(user);
  },
};

export const useContainer = createContainerHook();
export const inject = createInject();

Greeter.jsx

Using useContainer hook
import React from 'react';
import { useContainer } from './container';

const Greeter = () => {
  const { greeting, user } = useContainer('greeting', 'user');

  return (
    <div>
      <h1>{greeting}</h1>
      ID: {user.id}
    </div>
  );
}

export default Greeter;
Using inject
import React from 'react';
import { inject } from './container';

const Greeter = ({
  user,
  greeting,
}) => (
  <div>
    <h1>{greeting}</h1>
    ID: {user.id}
  </div>
);

export default inject('greeting', 'user')(Greeter);
Using withContainer
import React from 'react';
import { withContainer } from 'recontainer';

const Greeter = ({
  container
}) => (
  <div>
    <h1>{container.get('greeting')}</h1>
    ID: {container.get('user').id}
  </div>
);

export default withContainer(Greeter);

App.jsx

import React from 'react';
import { createContainer, ContainerProvider } from 'recontainer';
import { config } from './container';
import Greeter from './Greeter';

const container = createContainer(config);

export const App = () => (
  <ContainerProvider container={container}>
    <Greeter />
    {/* 
        <div>
          <h1>Hello John Doe!</h1>
          ID: john-doe
        </div>
    */}
  </ContainerProvider>
);

Installation

With yarn

$ yarn add recontainer

With npm

$ npm install recontainer

Please note that types are included, thus there is no need to install @types/recontainer package

Requirements

Docs

Container methods

get
 const container = createContainer({
   logger: () => message => console.log(message),
 });

 const log = container.get('logger');

 log('Hello World!'); // Console output: Hello World!
getAll
 const container = createContainer({
   logger: () => message => console.log(message),
   message: () => 'Foo bar',
 });

 const { logger, message } = container.getAll();

 logger(message); // Console output: Foo bar