Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
react-magnetic-di
Advanced tools
A new take for dependency injection / dependency replacement for your tests, storybooks and even experiments in production.
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 / globals allows you to optionally override "marked" dependencies inside your components so you can swap implementations only when needed.
npm i react-magnetic-di
# or
yarn add react-magnetic-di
Edit your Babel config file (.babelrc
/ babel.config.js
/ ...) and add:
// ... 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).
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:
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:
function MyComponent() {
// "mark" any type of function/class as injectable
di(Modal, useQuery);
const { data } = useQuery();
return <Modal>{data && 'Done!'}</Modal>;
}
In the unit/integration tests or storybooks we can create a new injectable implementation and wrap the component with DiProvider
to override such dependency:
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:
// 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 already injected:
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 and capabilities 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.
When you have the same dependency replaced multiple times, there are two behaviours that determine which injectable will "win":
DiProvider
wins. So you can declare more specific replacements by wrapping components with DiProvider
or withDi
and those will win over same type injectables on other top level DiProvider
suse
array wins. So you can define common injectables but still override each type case by case (eg: <DiProvider use={[...commonDeps, specificInjectable]}>
The usage outside React is not much different, aside from the different way of clearing the replacements.
import { fetchApi } from './fetch';
export async function myApiFetcher() {
// "mark" any type of function/class as injectable
di(fetchApi);
const { data } = await fetchApi();
return data;
}
In the tests, you can use runWithDi
, which will setup and clear the replacements for you after function execution is terminated. Such util also handles async code, but might require you to wrap the entire test to work effectively with scheduled code paths, or event driven implementations.
import { injectable, runWithDi } from 'react-magnetic-di';
import { myApiFetcher, fetchApi } from '.';
it('should call the API', async () => {
const fetchApiDi = injectable(
fetchApi,
jest.mock().mockResolvedValue('mock')
);
const result = await runWithDi(() => myApiFetcher(), [fetchApiDi]);
expect(fetchApiDi).toHaveBeenCalled();
expect(result).toEqual('mock');
});
By default magnetic-di
does not complain if an injectable is not used or if a dependency has not being replaced. In large codebases however, that might led to issues with stale, unused injectables or with lack of knowledge in what could be replaced. To ease introspection, the library provides a stats
API that returns unused
injectables.
stats.unused()
returns an array of entries { get(), error() }
for all injectables that have not been used since stats.reset()
has been calledThis is an example of stats guard implementation using the returned error()
helper:
import { stats } from 'react-magnetic-di';
beforeEach(() => {
// it's important to reset the stats after each test
stats.reset();
});
afterEach(() => {
stats.unused().forEach((entry) => {
// throw an error pointing at the test with the unused injectable
throw entry.error();
});
});
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:
// In your .babelrc / babel.config.js
// ... other stuff like presets
plugins: [
// ... other plugins
['react-magnetic-di/babel-plugin', { forceEnable: true }],
],
In case of babel macro (eg for use with CRA), the configName
key is reactMagneticDi
.
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.
di
and manually return the array of mocked dependencies, but it is not recommended.use
and target
props (changes are ignored)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.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.
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>;
}
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.
FAQs
Context driven dependency injection
The npm package react-magnetic-di receives a total of 8,170 weekly downloads. As such, react-magnetic-di popularity was classified as popular.
We found that react-magnetic-di demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.