Skip to content

Middleware

Middleware are plain objects that modify the positioning coordinates in some fashion, or provide useful data for rendering, as calculated by the positioning cycle.

This allows you to customize the behavior of the positioning and be as granular as you want, adding your own custom logic.

computePosition()computePosition() starts with initial positioning via placementplacement — then middleware are executed as an in-between “middle” step of the initial placement computation and eventual return of data for rendering once the promise has resolved.

Each middleware is executed in order, like a stack:

computePosition(referenceEl, floatingEl, {
  placement: 'right',
  middleware: [],
});
computePosition(referenceEl, floatingEl, {
  placement: 'right',
  middleware: [],
});

Example

const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn({x, y}) {
    return {
      x: x + 1,
      y: y + 1,
    };
  },
};
const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn({x, y}) {
    return {
      x: x + 1,
      y: y + 1,
    };
  },
};

This (not particularly useful) middleware adds 11 pixel to the coordinates. To use this middleware, add it to your middlewaremiddleware array:

computePosition(referenceEl, floatingEl, {
  placement: 'right',
  middleware: [shiftByOnePixel],
});
computePosition(referenceEl, floatingEl, {
  placement: 'right',
  middleware: [shiftByOnePixel],
});

Here, computePosition()computePosition() will compute coordinates that will place the floating element to the right center of the reference element, lying flush with it. Middleware are then executed, resulting in these coordinates getting shifted by one pixel. Then that data is returned to the caller.

Shape

A middleware is an object which has a namename property and a fnfn property. The fnfn property provides the logic of the middleware, which returns new positioning coordinates or useful data.

Data

Any data can be passed via an optional datadata property of the object that is returned from fnfn. This will be accessible to the consumer via the middlewareDatamiddlewareData property:

const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn({x, y}) {
    return {
      x: x + 1,
      y: y + 1,
      data: {
        amount: 1,
      },
    };
  },
};
const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn({x, y}) {
    return {
      x: x + 1,
      y: y + 1,
      data: {
        amount: 1,
      },
    };
  },
};
computePosition(referenceEl, floatingEl, {
  middleware: [shiftByOnePixel],
}).then(({middlewareData}) => {
  console.log(middlewareData.shiftByOnePixel);
});
computePosition(referenceEl, floatingEl, {
  middleware: [shiftByOnePixel],
}).then(({middlewareData}) => {
  console.log(middlewareData.shiftByOnePixel);
});

Function

You may notice that Anchors’ core middleware are actually functions. This is so you can pass options in:

const shiftByAmount = (amount = 0) => ({
  name: 'shiftByAmount',
  options: amount,
  fn: ({x, y}) => ({
    x: x + amount,
    y: y + amount,
  }),
});
const shiftByAmount = (amount = 0) => ({
  name: 'shiftByAmount',
  options: amount,
  fn: ({x, y}) => ({
    x: x + amount,
    y: y + amount,
  }),
});

It returns an object and uses a closure to pass the configured behavior:

const middleware = [shiftByAmount(10)];
const middleware = [shiftByAmount(10)];

The optionsoptions key on a middleware object allows libraries like React to compare and update middleware on component re-renders.

Always return an object

Inside fnfn make sure to return an object. It doesn’t need to contain properties, but to remind you that it should be pure, you must return an object. Never mutate any values that get passed in from fnfn.

MiddlewareState

An object is passed to fnfn containing useful data about the middleware lifecycle being executed.

In the previous examples, we destructured xx and yy out of the fnfn parameter object. These are only two properties that get passed into middleware, but there are many more.

The properties passed are below:

type MiddlewareState = {
  x: number;
  y: number;
  initialPlacement: Placement;
  placement: Placement;
  strategy: Strategy;
  middlewareData: MiddlewareData;
  elements: Elements;
  rects: ElementRects;
  platform: Platform;
};
type MiddlewareState = {
  x: number;
  y: number;
  initialPlacement: Placement;
  placement: Placement;
  strategy: Strategy;
  middlewareData: MiddlewareData;
  elements: Elements;
  rects: ElementRects;
  platform: Platform;
};

x

This is the x-axis coordinate to position the floating element to.

y

This is the y-axis coordinate to position the floating element to.

elements

This is an object containing the reference and floating elements.

rects

This is an object containing the RectRects of the reference and floating elements, an object of shape {width, height, x, y}.

middlewareData

This is an object containing all the data of any middleware at the current step in the lifecycle. The lifecycle loops over the middlewaremiddleware array, so later middleware have access to data from any middleware run prior.

strategy

The positioning strategy.

initialPlacement

The initial (or preferred) placement passed in to computePosition()computePosition().

placement

The stateful resultant placement. Middleware like flipflip change initialPlacementinitialPlacement to a new one.

platform

An object containing methods to make Anchors work on the current platform, e.g. DOM or React Native.

Ordering

The order in which middleware are placed in the array matters, as middleware use the coordinates that were returned from previous ones. This means they perform their work based on the current positioning state.

Three shiftByOnePixelshiftByOnePixel in the middleware array means the coordinates get shifted by 3 pixels in total:

const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn: ({x, y}) => ({x: x + 1, y: y + 1}),
};
const middleware = [
  shiftByOnePixel,
  shiftByOnePixel,
  shiftByOnePixel,
];
const shiftByOnePixel = {
  name: 'shiftByOnePixel',
  fn: ({x, y}) => ({x: x + 1, y: y + 1}),
};
const middleware = [
  shiftByOnePixel,
  shiftByOnePixel,
  shiftByOnePixel,
];

If the later shiftByOnePixel implementations had a condition based on the current value of xx and yy, the condition can change based on their placement in the array.

Understanding this can help in knowing which order to place middleware in, as placing a middleware before or after another can produce a different result.

In general, offset()offset() should always go at the beginning of the middleware array, while arrow()arrow() and hide()hide() at the end. The other core middleware can be shifted around depending on the desired behavior.

const middleware = [
  offset(),
  // ...
  arrow({element: arrowElement}),
  hide(),
];
const middleware = [
  offset(),
  // ...
  arrow({element: arrowElement}),
  hide(),
];

Resetting the lifecycle

There are use cases for needing to reset the middleware lifecycle so that other middleware perform fresh logic.

  • When flip()flip() and autoPlacement()autoPlacement() change the placement, they reset the lifecycle so that other middleware that modify the coordinates based on the current placementplacement do not perform stale logic.
  • size()size() resets the lifecycle with the newly applied dimensions, as many middleware read the dimensions to perform their logic.
  • inline()inline() resets the lifecycle when it changes the reference rect to a custom implementation, similar to a Virtual Element.

In order to do this, add a resetreset property to the returned object from fnfn.

type Reset =
  | true
  | {
      placement?: Placement;
      // `true` will compute the new `rects` if the
      // dimensions were mutated. Otherwise, you can
      // return your own new rects.
      rects?: true | ElementRects;
    };
type Reset =
  | true
  | {
      placement?: Placement;
      // `true` will compute the new `rects` if the
      // dimensions were mutated. Otherwise, you can
      // return your own new rects.
      rects?: true | ElementRects;
    };
const middleware = {
  name: 'middleware',
  fn() {
    if (someCondition) {
      return {
        reset: {
          placement: nextPlacement,
        },
      };
    }
 
    return {};
  },
};
const middleware = {
  name: 'middleware',
  fn() {
    if (someCondition) {
      return {
        reset: {
          placement: nextPlacement,
        },
      };
    }
 
    return {};
  },
};

Data supplied to middlewareDatamiddlewareData is preserved by doing this, so you can read it at any point after you’ve reset the lifecycle.