dojo dragon main logo

Middleware fundamentals

Dojo provides a concept of render middleware to help bridge the gap between reactive, functional widgets and their underlying imperative DOM structure.

Certain web app requirements are best implemented when widgets have access to information about the DOM. Common examples are:

  • Responsive UIs that are not tied to specific device types but instead adapt to varying element sizes given available page real estate.
  • Lazy-loading data only when needed once certain elements become visible in a user's viewport - such as infinite scroll lists.
  • Directing element focus and responding to user focus changes

Middleware does not need to be tied to the DOM however; the concept can also be used for more generic concerns around a widget's rendering lifecycle. Common examples of such requirements are:

  • Caching data between renders when data retrieval is costly
  • Pausing and resuming widget rendering depending on certain conditionals; avoiding unnecessary rendering when required information is not available
  • Marking a functional widget as invalid so that Dojo can re-render it

A single middleware component typically exposes certain functionality associated with one or more of a widget's rendered DOM elements; often, the widget's root node. The middleware system provides widgets more advanced control over their representation and interaction within a browser, and also allows widgets to make use of several emerging web standards in a consistent manner.

Sensible defaults get returned if a widget accesses certain middleware properties before the widget's underlying DOM elements exist. There is also middleware that can pause a widget's rendering until certain conditions are met. Using these middleware, widgets can avoid unnecessary rendering until required information is available, and Dojo will then automatically re-render the affected widgets with accurate middleware properties once data becomes available.

Creating middleware

Middleware is defined using the create() factory method from the @dojo/framework/core/vdom module. This process is similar to creating functional widgets, however, instead of returning VDOM nodes, middleware factories return an object with an appropriate API that allows access to the middleware's feature set. Simple middleware that only need a single function call to implement their requirements can also return a function directly, without needing to wrap the middleware in an object.

The following illustrates a middleware component with a trivial get()/set() API:

src/middleware/myMiddleware.ts

import { create } from '@dojo/framework/core/vdom';

const factory = create();

export const myMiddleware = factory(() => {
    return {
        get() {},
        set() {}
    };
});

export default myMiddleware;

Using middleware

Middleware is primarily used by functional widgets but can also get composed within other middleware to implement more complex requirements. In both cases, any required middleware gets passed as properties to the create() method, after which they are available via the middleware argument in the widget or middleware factory implementation function.

For example, the above myMiddleware can be used within a widget:

src/widgets/MiddlewareConsumerWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import myMiddleware from '../middleware/myMiddleware';

const render = create({ myMiddleware });
export const MiddlewareConsumerWidget = render(({ middleware: { myMiddleware } }) => {
    myMiddleware.set();
    return <div>{`Middleware value: ${myMiddleware.get()}`}</div>;
});

export default MiddlewareConsumerWidget;

Composing middleware

The following example shows middleware composing other middleware to implement more useful requirements:

  • Fetching a value from a local cache
  • Obtaining the value from an external location on a cache miss
  • Pausing further rendering of consuming widgets while waiting for the external value to return
  • Resuming rendering and invalidating consuming widgets so they can be re-rendered once the external value is made available through the local cache

src/middleware/ValueCachingMiddleware.ts

import { create, defer, invalidator } from '@dojo/framework/core/vdom';
import { cache } from '@dojo/framework/core/middleware/cache';

const factory = create({ defer, cache });

export const ValueCachingMiddleware = factory(({ middleware: { defer, cache, invalidator }}) => {
    get(key: string) {
        const cachedValue = cache.get(key);
        if (cachedValue) {
            return cachedValue;
        }
        // Cache miss: fetch the value somehow through a promise
        const promise = fetchExternalValue(value);
        // Pause further widget rendering
        defer.pause();
        promise.then((result) => {
            // Cache the value for subsequent renderings
            cache.set(key, result);
            // Resume widget rendering once the value is available
            defer.resume();
            // Invalidate the widget for a re-render
            invalidator();
        });
        return null;
    }
});

export default ValueCachingMiddleware;

Passing properties to middleware

As middleware gets defined via the create() utility function, a properties interface can also be given in the same way as specifying property interfaces for functional widgets. The main difference is that middleware properties are added to the properties interface of any consuming widgets. This means property values are given when instantiating the widgets, not when widgets make use of the middleware. Properties are considered read-only throughout the entire composition hierarchy, so middleware cannot alter property values.

The following is an example of middleware with a properties interface:

src/middleware/middlewareWithProperties.tsx

import { create } from '@dojo/framework/core/vdom';

const factory = create().properties<{ conditional?: boolean }>();

export const middlewareWithProperties = factory(({ properties }) => {
    return {
        getConditionalState() {
            return properties().conditional ? 'Conditional is true' : 'Conditional is false';
        }
    };
});

export default middlewareWithProperties;

This middleware and its property can get used in a widget:

src/widgets/MiddlewarePropertiesWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import middlewareWithProperties from '../middleware/middlewareWithProperties';

const render = create({ middlewareWithProperties });
export const MiddlewarePropertiesWidget = render(({ properties, middleware: { middlewareWithProperties } }) => {
    return (
        <virtual>
            <div>{`Middleware property value: ${properties().conditional}`}</div>
            <div>{`Middleware property usage: ${middlewareWithProperties.getConditionalState()}`}</div>
        </virtual>
    );
});

export default MiddlewarePropertiesWidget;

The value for the middleware conditional property is then specified when creating instances of MiddlewarePropertiesWidget, for example:

src/main.tsx

import renderer, { tsx } from '@dojo/framework/core/vdom';
import MiddlewarePropertiesWidget from './widgets/MiddlewarePropertiesWidget';

const r = renderer(() => <MiddlewarePropertiesWidget conditional={true} />);
r.mount();