react-app-modules
An easy-to-use, type-safe IoC library for React apps.
Features
- Type-Free Syntax: complete type-safety with no generics / parameterization
- Observable Architecture: providers can be made extremely flexible, modules can be swapped out live
- Custom Hooks: modules can be loaded within functional components
- Symbol Names: easy to rename modules using standard refactoring tools
- Written in TypeScript: no need to add an
@types
package ;)
Not Currently Supported
- Lazy loading
- Class components
Table of Contents
Installation
npm install react-app-modules
yarn add react-app-modules
Tutorial
This tutorial will demonstrate how to use react-app-modules
to define, provide, and use a simple ICalculator
module.
It is recommended that the reader follows this tutorial by
typing along (rather than copying / pasting) to get a sense
of the type-safety that this library provides. Feel emboldened
to try to use the api incorrectly; see for yourself what happens!
Setup
npx create-react-app tutorial --template typescript
cd tutorial
yarn add react-app-modules
Directory Structure
Here is the directory structure used for the code in this tutorial,
but feel free to use whatever structure works for you!
Defining a Module
Our ICalculator
module will implement a single method, sum
,
which returns the arithmetic sum of a list of numbers.
export interface ICalculator {
sum(numbers: number[]): number;
}
Defining the Symbols
First, we are defining a Calculator
symbol, which represents any
implementation of the ICalculator
interface. Note that we have not
coupled ourselves to an implementation. While not covered in this tutorial,
the createProvider
api be used to return a DigitalCalculator
versus
an Abacus
, for example.
Next, we extend the internal AppModules
type within react-app-modules
. This
bit of magic is what gives us complete type-safety for all the api methods.
import { ICalculator } from './definitions/calculator';
export const Calculator = Symbol('Calculator');
declare module 'react-app-modules' {
interface Types {
AppModules: {
[Calculator]: ICalculator;
};
}
}
Defining a Provider
The provider is responsible for providing an implementation of ICalculator
.
To demonstrate module loading, we will emit an implementation after 1 second.
The createProvider
api is a convenient way to define a module implementation.
Note that this architecture allows a provider to dynamically provide an
implementation for the ICalculator
interface, as well as swap out for
various ICalculator
implementations during runtime, though that is out of
scope for this tutorial. Also, the createProvider
api can be used to indicate
module dependencies, but that is not covered in this tutorial for the sake of brevity.
import { createProvider } from 'react-app-modules';
import { timer } from 'rxjs';
import { map } from 'rxjs/operators';
import { Calculator } from '../symbols';
export const provideCalculator = createProvider(Calculator, [], () =>
timer(1000).pipe(
map(() => ({
sum: (numbers) => numbers.reduce((total, n) => total + n),
})),
),
);
Defining an Example Component
Now we will define an Example
component that uses our new Calculator
module.
Note that we are using the useModule
hook provided by react-app-modules
.
import React from 'react';
import { useModule } from 'react-app-modules';
import { Calculator } from '../modules/symbols';
const Example = () => {
const calculator = useModule(Calculator);
if (calculator === null) {
return <span>Loading Calculator Module...</span>;
}
return <span>2 + 2 = {calculator.sum([2, 2])}</span>;
};
export default Example;
If you haven't been following along by typing, check this out:
Oh yeah.
Creating the Container / Context
We are almost there! First, we use the createContainer
api to compose
the set of module symbols with their respective providers. Then, we
supply the container to the AppContext.Provider
component, which the
useModule
hook depends upon. Finally, we render our Example
component
within the AppContext.Provider
.
import React from 'react';
import { AppContext, createContainer } from 'react-app-modules';
import Example from './components/example';
import { provideCalculator } from './modules/providers/provide-calculator';
import { Calculator } from './modules/symbols';
const container = createContainer({
[Calculator]: provideCalculator,
});
const App = () => (
<AppContext.Provider value={container}>
<Example />
</AppContext.Provider>
);
export default App;
Checking the Results
Time to fire that baby up.
yarn start
Observe that the module loads:
And then displays a result:
Api
TODO: More detailed overview of the Api methods.
Development
Dependencies
yarn
Build
yarn build
Test
yarn test
Publish
yarn publish
Misc
Update README Table of Contents
npx doctoc README.md