sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
85 lines (72 loc) • 3.38 kB
text/typescript
/* eslint-disable @typescript-eslint/ban-types */
import {type ComponentType, createElement, Fragment, useMemo} from 'react'
import {flattenConfig} from '../../config'
import {useSource} from '../../studio'
import {type PluginOptions} from '../types'
const emptyRender = () => createElement(Fragment)
function _createMiddlewareComponent<T extends {}>(
defaultComponent: ComponentType<T>,
middlewareComponents: ComponentType<T>[],
): ComponentType<T> {
return (outerProps: T) => {
// This is the inner "layer" of the middleware chain
// Here we render the _default_ component (typically Sanity's component)
let next = (props: T) => createElement(defaultComponent, props)
for (const middleware of middlewareComponents) {
// As we progress through the chain, the meaning of "renderDefault" changes.
// At a given layer in the chain, the _next_ layer is the "default".
const renderDefault = next
// Here we replace `next` so that the _previous_ layer may use this as its _next_.
next = (props) => createElement(middleware, {...props, renderDefault})
}
return next({
...outerProps,
// NOTE: it's safe to pass the empty render function, since it'll be overwritten in the next step (above).
// NOTE: it's important that the default component does not use `renderDefault`, since it will
// get the `emptyRender` callback will be passed when the middleware stack is empty.
renderDefault: emptyRender,
})
}
}
/**
* @internal
* This hook returns a component based on the Components API middleware chain.
*
* - The `pick` function is used to select a component from the provided plugin options in the configuration.
* - The `defaultComponent` is the default component that gets rendered with `renderDefault`.
* The `renderDefault` function is added to the props of the middleware components so that they can render the default
* component and continue the middleware chain.
*
* @example
* Example usage of:
*
* ```ts
* const StudioLayout = useMiddlewareComponents({
* pick: (plugin) => plugin.studio?.components?.layout,
* defaultComponent: StudioLayout,
* })
*
* return <StudioLayout />
*```
*/
export function useMiddlewareComponents<T extends {}>(props: {
pick: (plugin: PluginOptions) => ComponentType<T>
defaultComponent: ComponentType<T>
}): ComponentType<T> {
const {options} = useSource().__internal
const {defaultComponent, pick} = props
return useMemo(() => {
// Flatten the config tree into a list of configs
const flattened = [...flattenConfig(options, [])]
// Since the middleware chain is executed backwards, we need to reverse the list of configs here.
// This is important because we want the order of the Components API to be consistent with the order of the other APIs.
flattened.reverse()
// Pick the middleware components from the configs
const pickedComponents = flattened.map(({config}) => pick(config))
// Since we try to pick the components in all configs, some results may be undefined.
// Therefore, we filter these values out before passing the result to the middleware creator.
const result = pickedComponents.filter(Boolean)
// Create the middleware component
return _createMiddlewareComponent(defaultComponent, result)
}, [defaultComponent, options, pick])
}