<p align="center">
  <img src="https://user-images.githubusercontent.com/84136/83958267-1c8f7f00-a8b3-11ea-9725-1d3530af5f8d.png" alt="react-magnetic-di logo" height="150" />
</p>
<h1 align="center">react-magnetic-di</h1>
<p align="center">
  <a href="https://www.npmjs.com/package/react-magnetic-di"><img src="https://img.shields.io/npm/v/react-magnetic-di.svg"></a>
  <a href="https://bundlephobia.com/result?p=react-magnetic-di"><img src="https://img.shields.io/bundlephobia/minzip/react-magnetic-di.svg" /></a>
  <a href="https://codecov.io/gh/albertogasparin/react-magnetic-di"><img src="https://codecov.io/gh/albertogasparin/react-magnetic-di/branch/master/graph/badge.svg" />
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
  <!--a href="CONTRIBUTING"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" /></a-->
</p>

A new take for dependency injection / dependency replacement in React for your tests, storybooks and even experiments in production.

- Close-to-zero performance overhead on dev/testing
- **Zero** performance overhead on production (code gets stripped unless told otherwise)
- Works with any kind of functions/classes (not only components) and in both class and functional components
- Replaces dependencies at any depth of the React tree
- Allows selective injection
- Enforces separation of concerns, keeps your component API clean
- Just uses Context, it does not mess up with React internals or modules/require

## Philosophy

Dependency injection and component injection is not a new topic. Especially the ability to provide a custom implementation of a component/hook while testing or writing storybooks and examples it is extremely valuable. `react-magnetic-di` takes inspiration from decorators, and with a touch of Babel magic and React Context allows you to optionally override "marked" dependencies inside your components so you can swap implementations only when needed.

## Usage

```sh
npm i react-magnetic-di
# or
yarn add react-magnetic-di
```

### Adding babel plugin (or using macro)

Edit your Babel config file (`.babelrc` / `babel.config.js` / ...) and add:

```js
  // ... other stuff like presets
  plugins: [
    // ... other plugins
    'react-magnetic-di/babel-plugin',
  ],
```

If you are using Create React App or babel macros, you don't need the babel plugin: just import the methods from `react-magnetic-di/macro` (see next example).

### Using injection replacement in your components

Given a component with complex UI interaction or data dependencies, like a Modal or an Apollo Query, we want to easily be able to integration test it. To achieve that, we "mark" such dependencies in the `render` function of the class component:

```jsx
import React, { Component } from 'react';
import { di } from 'react-magnetic-di';
// or
import { di } from 'react-magnetic-di/macro';

import { Modal } from 'material-ui';
import { Query } from 'react-apollo';

class MyComponent extends Component {
  render() {
    // that's all is needed to "mark" these variables as injectable
    di(Modal, Query);

    return (
      <Modal>
        <Query>{({ data }) => data && 'Done!'}</Query>
      </Modal>
    );
  }
}
```

Or on our functional component with hooks:

```jsx
function MyComponent() {
  // "mark" any type of function/class as injectable
  di(Modal, useQuery);

  const { data } = useQuery();
  return <Modal>{data && 'Done!'}</Modal>;
}
```

### Leveraging dependency replacement in tests and storybooks

In the unit/integration tests or storybooks we can create a new injectable implementation and wrap the component with `DiProvider` to override such dependency:

```jsx
import React from 'react';
import { DiProvider, injectable } from 'react-magnetic-di';
import { Modal } from 'material-ui';
import { useQuery } from 'react-apollo-hooks';

// injectable() needs the original implementation as first argument
// and the replacement implementation as second
const ModalOpenDi = injectable(Modal, () => <div />);
const useQueryDi = injectable(useQuery, () => ({ data: null }));

// test-enzyme.js
it('should render with enzyme', () => {
  const container = mount(<MyComponent />, {
    wrappingComponent: DiProvider,
    wrappingComponentProps: { use: [ModalOpenDi, useQueryDi] },
  });
  expect(container.html()).toMatchSnapshot();
});

// test-testing-library.js
it('should render with react-testing-library', () => {
  const { container } = render(<MyComponent />, {
    wrapper: (p) => <DiProvider use={[ModalOpenDi, useQueryDi]} {...p} />,
  });
  expect(container).toMatchSnapshot();
});

// story.js
storiesOf('Modal content', module).add('with text', () => (
  <DiProvider use={[ModalOpenDi, useQueryDi]}>
    <MyComponent />
  </DiProvider>
));
```

In the example above we replace all `Modal` and `useQuery` dependencies across all components in the tree with the custom versions. If you want to replace dependencies **only** for a specific component (or set of components) you can use the `target` prop:

```jsx
// story.js
storiesOf('Modal content', module).add('with text', () => (
  <DiProvider target={[MyComponent, MyOtherComponent]} use={[ModalOpenDi]}>
    <DiProvider target={MyComponent} use={[useQueryDi]}>
      <MyComponent />
      <MyOtherComponent>
    </DiProvider>
  </DiProvider>
));
```

In the example above `MyComponent` will have both `ModalOpen` and `useQuery` replaced while `MyOtherComponent` only `ModalOpen`. Be aware that `target` needs an **actual component** declaration to work, so will not work in cases where the component is fully anonymous (eg: `export default () => ...` or `forwardRef(() => ...)`).

The library also provides a `withDi` HOC in case you want to export components with dependencies alredy injected:

```jsx
import React from 'react';
import { withDi, injectable } from 'react-magnetic-di';
import { Modal } from 'material-ui';
import { MyComponent } from './my-component';

const ModalOpenDi = injectable(Modal, () => <div />);

export default withDi(MyComponent, [ModalOpenDi]);
```

`withDi` supports the same API as `DiProvider`, where `target` is the third argument of the HOC `withDi(MyComponent, [Modal], MyComponent)` in case you want to limit injection to a specific component only.

### Configuration Options

#### Enable dependency replacement on production (or custom env)

By default dependency replacement is enabled on `development` and `test` environments only, which means `di(...)` is removed on production builds. If you want to allow injection on production too (or on a custom env) you can use the `forceEnable` option:

```js
// In your .babelrc / babel.config.js
  // ... other stuff like presets
  plugins: [
    // ... other plugins
    ['react-magnetic-di/babel-plugin', { forceEnable: true }],
  ],
```

## Eslint plugin and rules

In order to enforce better practices, this package exports some eslint rules:

| rule                | description                                                                              | options                  |
| ------------------- | ---------------------------------------------------------------------------------------- | ------------------------ |
| `order`             | enforces `di(...)` to be the top of the block, to reduce chances of partial replacements | -                        |
| `exhaustive-inject` | enforces all external components/hooks being used to be marked as injectable.            | `ignore`: array of names |
| `no-duplicate`      | prohibits marking the same dependency as injectable more than once in the same block     | -                        |
| `no-extraneous`     | enforces dependencies to be consumed in the scope, to prevent unused variables           | -                        |
| `sort-dependencies` | require injectable dependencies to be sorted                                             | -                        |

The rules are exported from `react-magnetic-di/eslint-plugin`. Unfortunately Eslint does not allow plugins that are not npm packages, so rules needs to be imported via other means for now.

## Current limitations

- Does not support Enzyme shallow ([due to shallow not fully supporting context](https://github.com/enzymejs/enzyme/issues/2176)). If you wish to shallow anyway, you could mock `di` and manually return the array of mocked dependencies, but it is not recommended.
- Does not support dynamic `use` and `target` props (changes are ignored)
- Officially supports injecting only functions/classes. If you need to inject some other data types, create a simple getter and use that as dependency.
- Does not replace default props (or default parameters in general): so dependencies provided as default parameters (eg `function MyComponent ({ modal = Modal }) { ... }`) will be ignored. If you accept the dependency as prop/argument you should inject it via prop/argument, as having a double injection strategy is just confusing.

## Can it be used without Babel plugin?

Yes, but you will have to handle variable assignment yourself, which is a bit verbose. In this mode `di` needs an array of dependencies as first argument and the component, or `null`, as second (to make `target` behaviour work). Moreover, `di` won't be removed on prod builds and ESLint rules are not currently compatible with this mode.

```js
import React, { Component } from 'react';
import { di } from 'react-magnetic-di';
import { Modal as ModalInj } from 'material-ui';
import { useQuery as useQueryInj } from 'react-apollo';

function MyComponent() {
  const [Modal, useQuery] = di([ModalInj, useQueryInj], MyComponent);

  const { data } = useQuery();
  return <Modal>{data && 'Done!'}</Modal>;
}
```

## Contributing

To test your changes you can run the examples (with `npm run start`).
Also, make sure you run `npm run preversion` before creating you PR so you will double check that linting, types and tests are fine.
