The extension system
ModularComponent aims to be a toolkit, and as such, it needs to be as agnostic as possible
of the application context. For this reason, the core factory only implements a single stage: with(render),
which is in fact a simple, traditional React function component.
Capabilities can then be added on a per-application basis, to construct a pipeline that makes sense for a specific application context: adding a stage for connecting to a global store, or for handling internationalisation...
Such capabilities are added through extensions. Extensions are functions returning configuration objects detailing a new stage to add to the pipeline.
Understanding stages
The .with() method accepts a standard object comprised of two fields:
field: the name of the argument that will get added to the argument mapuseStage: a hook that receives the current argument map and returns the value to set on the stage field
While it's possible to use those objects directly when calling .with(), for readability and ease of writing we
recommend creating custom stage functions that take relevant parameters and abstract away the stage logic.
Extension conventions
Extensions export custom stage functions, that can be passed to the .with() method of a ModularComponent.
They can export one or multiple stage functions, depending on the needs covered by the extension.
By convention, stage function name should start with a lowercase letter, and should not repeat the with keyword.
For instance, a localization extension should be called locale(), not Locale() or withLocale().
Setting a field and stage transform hook
A stage definition contains a field and useStage properties as described above.
Extensions are written as functions that return this definition. They should use the ModularStage helper type
to ensure both their field and useStage properties are correctly typed for inference.
For instance, the lifecycle definition looks like this:
import { ModularStage } from '@modular-component/core'
export function lifecycle<Args extends {}, Return>(
useLifecycle: (args: Args) => Return,
): ModularStage<'lifecycle', (args: Args) => Return> {
return { field: 'lifecycle', useStage: useLifecycle }
}
Your stage function can take any argument it needs. In the case of the lifecycle function, it takes a function
that is directly reused as the useStage property, but the useStage function could be any hook using the parameters
as it sees fit, along the arguments it receives when called.
You can add as many stage functions as you want. Different stage functions can also impact the same field if needed, for some advanced cases.
Parameters and field value inference
If we look at the lifecycle example, we can see that the stage function infers two generic parameters: Args representing
the arguments passed from upstream stages, and Return representing the value returned from the lifecycle hook.
You can use Args to type the first parameter passed to useStage (as is the case here) any time you want your return
type to be aware of the initial Args type. This also allows Typescript to automatically type the args parameter
when called:
ModularComponent<{ value: number }>()
.withLifecycle(({ props }) => ({ double: props.value * 2 }))
// 👆 Here, Typescript knows that the props argument
// is of type { value: number }
// It will also infer that the Return value is
// of type { double: number } thanks to that.
If we take another example (our components extension), you can see that sometimes the current arguments are not needed.
That is often the case when passing a static value to a stage:
import { ComponentType } from 'react'
import { ModularStage } from '@modular-component/core'
export function components<Components extends Record<string, ComponentType>>(
components: Components,
): ModularStage<'components', () => Components> {
return { field: 'components', useStage: () => components }
}
In this case, you can easily just omit the generic type parameter, as we don't need to type the useStage parameters.
Learn more
You can read the documentation for each official extension to learn more about writing extensions.