With a system like Uniform Canvas that stores layout information with elements from many data sources, it's important to be able to control the flow of data from the linked data sources through the layout data and into a final object that's ready to render in the system of your choice. Enhancers are how Canvas aggregates the data from linked systems with the layout information.
For more details on the enhancer API, see the package reference.
Uniform supports a number of different types of enhancers.
Canvas components have parameters attached to them, which contain data values that can be of any type - text, numbers, CMS content IDs, JSON product query definitions, etc. A parameter enhancer is a function that's passed a parameter and can manipulate the parameter data into a new value. For example, a parameter enhancer might transform a text value of the ID of a CMS item into a JSON object that's the content of that item from the CMS's API.
Parameter enhancers operate within the scope of a single parameter, and can't change any other aspects of a component.
Sometimes you may want to simply add arbitrary data to a component that's not tied to a parameter, or crosses multiple parameters. A data enhancer is a function that's assigned a data key and then returns the value that will be the targeted component's data value for that key.
Often it's desirable to scope enhancers to run only for a subset of components. Enhancers are registered using the
EnhancerBuilder, a fluent interface that lets you bind enhancers to specific components, specific parameter types, or all of the above. Within the EnhancerBuilder, only the first matching enhancer will be run for a given parameter or data key. This lets you craft overrides to more generic enhancers by placing them before the more generic enhancers.
Note that because only the first matching enhancer is run, in the above example if the
foo parameter was of type
contentfulEnhancer would not be run because the
promoFooEnhancer matched first.
Parameter enhancers are simple functions that take the matched parameter and return a new value for it. For example, this is a valid parameter enhancer that would change the value of any matched parameter to
Now let's wire up the enhancer to an
EnhancerBuilder and actually run it:
enhance function mutates the input composition. See the Immutability section below if you don't like that.
In reality you'd want to do more than just replace any parameter with a fixed string, so here's a more complex parameter enhancer that does something useful. Note that you can return promises and gain access to the parent component, parameter name, and parameter value.
Data enhancers are simple functions that return a value for a data key on the component. Data keys are properties on the component's
data property. For example, this is a valid data enhancer that would set the value of its assigned data key to
Now wire up the enhancer to an
EnhancerBuilder and actually run it:
In reality you'd want to do more than just greet every component, so here's a more complex data enhancer that does something useful. Note that you can return promises and gain access to the parent component.
enhance function mutates the input composition. Usually this is fine, as enhancement would occur immediately on a Canvas API response that would not be used without enhancement; mutability is in this case a performance enhancement. However it's possible to simply make the enhance function behave immutably if this isn't desirable by using
Sometimes it makes sense to compose multiple enhancers together in a chain. A common reason for this would be to modify the output of some other enhancer: perhaps your CMS enhancer fetches more data than you need, and you wish to remove it to reduce data size. Since regular enhancers only execute the first enhancer matched, this pattern impossible with the
EnhancerBuilder directly; subsequent enhancers would simply be ignored. But you can use the
compose() function to do exactly this within the
Composing enhancers together with the
compose() function is different than a Canvas composition (a collection of components in a layout) despite the shared language.
Another tactic for composing enhancers is multi-phasic enhancement. With this pattern you take advantage of the fact that the
enhance() function is simply a function that mutates a composition - there is no reason you can't enhance more than once in sequence, on the same composition object. Choosing this technique is appropriate when you want to split enhancement itself into several phases, for example preprocessing, fetch data, postprocessing. Sometimes this can make composed enhancers make more sense.
Enhancer performance & batching#
Asynchronous enhancers are allowed to run in parallel where possible. Because only the first matching enhancer is executed, this enables the enhancer engine to queue all the necessary data fetches at once, without creating a waterfall effect that would impact performance. Advanced enhancers can also take advantage of batching. A batched enhancer has a different shape than a simple function, as it stores up all the things it needs to fetch during a walk of the components in the composition and then once the walk is complete the batch function is invoked to enable fetching multiple data results with one request for APIs that support that. An example of this is the Contentful enhancer; it can find all referenced Contentful entry IDs on a composition and then make a single call to Contentful requesting all the referenced IDs in one query.
Under load an enhancer can potentially make a large number of parallel requests to a remote API. Most APIs have rate limits which prevent this, but it's even better to avoid being rate limited entirely. As such, enhancers support providing a limit policy which allows control over the flow of requests to data source as well as error retrying logic. A limit policy is an asynchronous function that's passed the enhancer function to run as a parameter. Every enhance call for that enhancer is routed through the limit policy if it's defined, which enables you to use any logic you desire to control the flow of requests to the remote API. Uniform provides a default
createLimitPolicy function that uses
p-retry to handle rate limiting and retrying. Enhancers provided by Uniform will define a default limit policy internally based on the API they're calling, but also accept a custom limit policy passed into their creation function. This is useful as it allows you to both customize the limit policy (if you have a high rate limit plan) but also to tie several enhancers with different configurations to the same limit policy to prevent them from having parallel rate limits.
You can also implement a custom limit policy if you need advanced functionality. Here is a simple example which uses
p-limit, which allows only a certain number of promises in flight at any given time.
enhance function supports passing arbitrary context data to the enhancers. This can be used to provide data from the top level down to enhancers without relying on closures. The
preview property is always present on the context as most enhancers need to know if they should fetch preview data or not, but other properties can be added to the context as needed.
Many enhancers provide some sort of function to enable customizing their data fetching options. These functions are also passed the enhancer context, which enables tasks like localization of the data query based on application state.
Data enhancers can be executed anywhere you possess composition data, not just within a terminal web application. Uniform has also developed an enhancer proxy architecture that can be used if you wish to separate enhancement from your applications, for example to support identical enhancement configuration for several applications, apply custom cache routines, or separate development work between frontend and back end teams. In an enhancer proxy configuration, data flows more like this:
The enhancer proxy acts as a purpose built reverse proxy server that performs enhancement on the composition data before forwarding it on to the consumer. It can be run anywhere where Node.js can run - in a container, on a server-less function, or on bare metal.