This package provides a framework for writing unopinionated functional components that can easily be extended to inject styling, and composed together in an efficient manner.
This provides a foundation for writing both simple and higher order components with the following characteristics:
- Unopinionated - Components can be written such that they encapsulate the core functionality of a component, without having opinions on what styling system to use. There are myriad styling systems and all involve tradeoffs between flexibility, simplicity, and performance. Writing components which allow injection of a styling system allows consumers to optimize for their scenarios.
- Reusable - Modern practice in react encourages making functionality reusable via hooks. This framework provides a standardized pattern for hooks and how their results are communicated to the actual rendering. This allows replacing or augmenting either the hook or rendering portions without the need of writing everything from scratch.
- Customizable - Typical customization patterns involve either passing customizations via props, or customizing via wrapping. Passing customizations via props explode the complexity of the props and cause issues with cacheability and performance. Wrapping components adds additional layers to the react hierarchy and still precludes access to internals of the component.
- Composable - This framework provides a repeatable pattern which allows for a wrapped component, or a part of a higher order component, to be executed functionally without adding extra layers to the react hierarchy.
- Scalable - Composable components can be used for both simple and higher order components. The framework uses the concept of slots to make targeting and customizing sub-components easier.
- Flexible - This pattern is suitable for use with both react and react-native. None of the concepts are platform specific.
- Getting Started: Simple Components - a walkthrough of creating a simple composable component. Start here to understand the base concepts.
- Getting Started: Higher Order Components - a walkthrough of creating a complex composable component. Read this next to better understand how the pieces fit together and can be recombined.
The primary entry point into the module is via the composable
function. This creates a functional component, for use with react or react-native, which can be efficiently layered and recomposed. With all types expanded out the definition will be as follows:
export function composable<
TProps extends object,
TSlotProps extends ISlotProps = ISlotProps<TProps>,
TState = object
>(
options: {
useStyling?: (
props: TProps
) => TSlotProps,
usePrepareProps?: (
props: TProps,
useStyling: (props: TProps) => TSlotProps
) => IRenderData<TSlotProps, TState>,
render?: (
Slots: ISlots<TSlotProps>,
renderData: IRenderData<TSlotProps, TState>,
...children: React.ReactNode[]
) => JSX.Element | null,
slots: {
[K in keyof TSlotProps]: {
slotType?: React.ElementType<> | string,
filter?: (propName: string) => boolean
}
}
}
): React.FunctionComponent<TProps> & { __composable: IComposableOptions };
The composable
function takes up to three generic template arguments.
TProps extends object
- This determines the type of the functional component. At its heart the component will beReact.FunctionalComponent<TProps>
.TSlotProps extends ISlotProps = ISlotProps<TProps>
- This is the collection of props corresponding to the parts of the component. If the type is omitted then this will be defined as an object in the form of{ root: TProps }
. See the foundation-settings documentation for more on slot props.TState = object
- State is an optional type, used to pass data fromusePrepareProps
torender
.
The object is configured via the options
parameter. This has four main parts
The slots
option is how a user configures the base component types for the parts of this component.
slots: {
[K in keyof TSlotProps]: {
slotType?: React.ElementType<> | string,
filter?: (propName: string) => boolean
}
}
The most important part is setting the React.ElementType
of the component into slotType
. This determines the type of react element that will be created in render. The optional filter
can be set to automatically trim the prop set down during render.
The required slots will be determined by the values in TSlotProps.
The useStyling
function allows a user of composable to provide style injection. This is a function that takes props as input and should return ISlotProps that include references to the styling and defaults to apply.
useStyling?: (props: TProps) => TSlotProps
If not specified, an empty implementation will be set provided which will return an empty object.
The usePrepareProps
function provides a standardized pattern for preparing props for render.
usePrepareProps?: (
props: TProps,
useStyling: (props: TProps) => TSlotProps
) => IRenderData<TSlotProps, TState>,
The role of this function is to do the following:
- Call any hooks functions that need to be called
- Obtain any styling slot props from the provided useStyling implementation
- Perform any other property transformations needed by the component.
- Return the prepared props, split into the slotProps as needed.
- Optionally return a custom state
The return result is an IRenderData
which is defined as:
export interface IRenderData<TSlotProps extends ISlotProps, TState = any> {
slotProps?: TSlotProps;
state?: TState;
}
The render data is simply a custom package to be passed to the render function. Anything returned in here will be passed on as-is. Note that state
can be anything, while it may contain state data, it can be used to pass any data to render that doesn't map to props.
If usePrepareProps
is not provided a simple implementation that calls useStyling
, merges the result with the props, then passes the merged result into the root slot props. Note this is only valid in the case where there is a single slot.
The render
function is the routine that returns the JSX element tree to react. The signature is as follows:
render?: (
Slots: ISlots<TSlotProps>,
renderData: IRenderData<TSlotProps, TState>,
...children: React.ReactNode[]
) => JSX.Element | null,
The three parameters are:
Slots
- an object containing the react elements to render. These elements have a built in reference to the slotProps returned byusePrepareProps
and the filters passed in viaslots
.renderData
- is the object returned byusePrepareProps
, this generally containsslotProps
andstate
....children
- this is the children property from initial input props for the component. It can be handled as is standard for a react component.
The Slots
parameter allows plugging different react types into a complex component. These can be output by either using renderSlot
or by using the withSlots
jsx helper. Usage would be something like:
/** @jsxRuntime classic */
/* @jsx withSlots */
import { withSlots } from '@uifabricshared/foundation-composable';
render: (Slots: ISlots<IMySlotProps>, renderData, ...children) => {
// do some work
return (
<Slots.root>
<Slots.slot1 additionalProp="foo">{...children}</Slots.slot1>
<Slots.slot2 />
</Slots.root>
);
};
In the case above Slots.root
will automatically fill in the props from renderData.slotProps.root
. Slots.slot1
will do the same with the equivalent data for its slot name. Additional props can be added inline in the JSX if so desired. All of these will be merged and filtered as appropriate.
Render is optional in the case where there is only one sub-component, in this case the root slot will be rendered in the default manner.
composable
will create a function component and will also append its options to the function as __composable
. As an example if there is a FancyThing
component, its options will be accessible via FancyThing.__composable
while it can still be used in a JSX tree via <FancyThing>
or React.createElement(FancyThing, props, children)
.
The overall flow of a component can be seen in the following diagram. At its core, a functional react component takes in a set of props, and outputs a JSX.Element tree.
Using composable, the normal flow is broken into two primary parts: usePrepareProps
and render
. The optional useStyling
function allows for style injection, while the slots
define the actual JSX.Elements that will be output.
The Slots
parameter passed to render
is an object containing functions which mimic the shape of React.createElement
. When used with the @jsx helper withSlots
, These functions:
- Are created once. This is essential because creating a new closure on every render pass will cause the tree to remount. They are stored in a state hook but then never updated over the lifetime of the component.
- Include references to the
slotProps
returned as part ofusePrepareProps
. This allows for filters to act upon the props and avoids having to manually write<Slot.root {...slotProps.root}>
every time. It also allows additional props to be mixed in directly to the JSX tree with the merge happening implicitly before render. - Calls
composable.render
directly without going throughReact.createElement
. This means that it will not create an unnecessary entry in the element tree. Thecomposable.usePrepareProps
will have been called as part of the parent component's prop preparation pass. - Can be used conditionally. Because hooks will be done as part of the
usePrepareProps
pass, the actual render itself will be safe to be used conditionally, even when being called as a function.
Because of the slot behavior a sub-component may or may not have children set during usePrepareProps
. This is because children can be filled in via props, or by construction of a JSX tree within a higher order component. Because the construction of the tree happens during render
, children are not reliably available until render
.