Skip to main content

@modular-component/with-components

Provides a components() stage that fills the components argument with a map of React components. Useful when running tests in an environment that does not allow module mocking: sub-components can be stubbed in tests by mocking the stage to replace their implementations.

Usage

Stage function imports

import { ModularComponent, render } from '@modular-component/core'
import { components } from '@modular-component/with-components'

import { SomeComponent } from 'some-component'

const MyComponent = ModularComponent()
.with(components({ SomeComponent }))
.with(render(({ props, components }) => <components.SomeComponent />))

Stage registration

import { ModularComponent } from '@modular-component/core'
import '@modular-component/core/register'
import '@modular-component/with-components/register'

import { SomeComponent } from 'some-component'

const MyComponent = ModularComponent()
.withComponents({ SomeComponent })
.withRender(({ props, components }) => <components.SomeComponent />)

Replacing sub-components

Replacing sub-components can be done either by updating or mocking the stage. It allows creating a clone of the component with a different set of sub-components, either for tests or for content. For instance, one could imagine a Layout base component taking advantage of this functionality:

import React from 'react'

const PageLayout = ModularComponent()
.withComponents({
Title: React.Fragment,
Subtitle: React.Fragment,
Content: React.Fragment,
Footer: React.Fragment,
})
.withRender(({ components }) => {
// Build a layout using <components.Title />, <components.Subtitle />...
})

const PageOne = PageLayout.withComponents({
Title: () => <>First page</>,
Subtitle: () => <>I have a subtitle but no footer</>,
Content: () => <>First page content</>,
Footer: React.Fragment,
})

const PageTwo = PageLayout.withComponents({
Title: () => <>Second page</>,
Subtitle: React.Fragment,
Content: () => <>Second page content</>,
Footer: () => <>I have a footer but no subtitle</>,
})

Stage registration

You can either automatically register the stage on withComponents by importing @modular-component/with-components/register, or handle the registration manually thanks to the components function and WithComponents type exports.

For instance, here is how you could register it on withSubComponents instead:

import { ModularComponent, ModularContext } from '@modular-component/core'
import { components, WithComponents } from '@modular-component/with-components'

// Register the stage on the factory
ModularComponent.register({ subComponents: components })

// Extend the type definition
declare module '@modular-component/stages' {
export interface ModularComponentStages<Context extends ModularContext> {
withSubComponents: WithComponents<Context>
}
}

Implementation

components() is a simple stage adding the provided record as a components argument. It has a restriction on accepted values, to only accept a record of React components.

import { ComponentType } from 'react'
import {
addTo,
wrap,
ModularContext,
GetConstraintFor,
GetValueGetterFor,
StageParams,
StageReturn,
} from '@modular-component/core/extend'

type Constraint<Context extends ModularContext> = GetConstraintFor<
Context,
'components',
Record<string, ComponentType<any>>
>

export function components<
Context extends ModularContext,
Type extends Constraint<Context>,
>(components: GetValueGetterFor<Context, 'components', Type>) {
return addTo<Context>().on('components').provide(wrap(components))
}

export type WithComponents<
Context extends ModularContext
> = <
Type extends Constraint<Context>,
>(
...args: StageParams<typeof components<Context, Type>>
) => StageReturn<typeof components<Context, Type>>