Best Practices
If you are reading this documentation, it is probably because JSX Middlewares are a new concept for you. There may be some pitfalls that you may not be aware of. This section will help you to avoid them, and use JSX Middlewares in the best way possible.
Use responsibly
JSX Middlewares give you a lot of control over your JSX. However, this can be a double-edged sword. If you are not careful, you can end up with a lot of middlewares that are hard to understand. It is recommended to use JSX Middlewares only when you need them, and to keep them simple.
Middlewares can't do anything React itself can't do. But it can do some things the JSX syntax alone can't do.
If you see yourself writing a lot of the same code in your JSX, it may be a good idea to write a middleware for it. In this regard, JSX Middlewares are similar to Higher Order Components (HOCs). It is also similar to Angular directives.
Understand the JSX
JSX Middlewares are called for every JSX element in your application. It is important to understand how JSX works to be able to avoid pitfalls. It is especially important if you are a library developer, and you want to write middlewares that work with any JSX. You can't assume that the JSX will be written in a certain way.
Some of the things to keep in mind are:
- The
typeis the rendered component. It can be astring,functionorclass. But it can also be a React Fragment, in which case it is asymbol. - The
propsis the object which includes all the props passed to the rendered component. Remember thatkeycan't be accessed fromprops. It is a separate argument. propscan include thechildrenpassed to a component. It is important to understand that thechildrenis not always astringorReactNode. It can be any JavaScript value, includingArray,null,undefinedor even functions.- The
keyis the key passed to the rendered component. You should generally pass it to the next middleware as is.
Understand the order of middlewares
The order of middlewares is important. Middlewares are called in the reverse order they were added (LIFO). So, if you add middlewares like this:
addMiddlewares(middleware1, middleware2);
addMiddlewares(middleware3);
The order of execution will be middleware3, middleware2, middleware1 and lastly the original JSX factory.
Avoid side effects
JSX Middlewares are not meant to be used for side effects. They need to be pure functions. They are meant to transform JSX elements into other JSX elements.
If you need to do side effects, you can wrap your JSX in a component that does the side effects. Check the example in Rules of hooks section to see how to do this.
Bail out before touching props
Middlewares run for every JSX element, on every render. The middleware chain itself is very cheap, but cloning props is not free — and it is easy to clone them accidentally for every element in your app by destructuring in the function signature:
// ⚠️ This clones props for EVERY element, even ones without $ripple
function rippleMiddleware(next, type, { $ripple, ...props }, key) {
// ...
}
Instead, check whether your middleware applies before creating any new objects, and pass the arguments through untouched when it doesn't:
function rippleMiddleware(next, type, props, key) {
// Elements without the directive pass through with zero allocations
if (!('$ripple' in props) && type !== 'button') return next(type, props, key);
const { $ripple, ...rest } = props;
// ...
}
In a benchmark with three middlewares, the bail-early style reduced element creation overhead by about 60% compared to destructuring in the signature. The same applies to spreading props ({ ...props, something }) — only do it on the elements your middleware actually changes.
Avoid infinite recursion
It is possible to create infinite recursion with JSX Middlewares if you are not careful. This often happens when you unconditionally create new JSX elements in a middleware.
For example, a middleware like below will cause infinite recursion, because creating a div will call this middleware again, and so on.
function wrapMiddleware(next, type, props, key) {
return <div>{next(type, props, key)}</div>;
}
There are several ways to fix this. For example, by adding a condition to wrap in a div only if the type is not already a div.
It is also a good idea to run the logic of the middleware only if a specific prop is passed to the element. To distinguish such props, it can be a good idea to prefix them with $.
Another way to solve this is to use the original property of next to create JSX elements without middlewares.
function wrapMiddleware(next, type, props, key) {
const children = next(type, props, key);
return next.original('div', { children }, key);
}
Remember the rules of hooks
You cannot use hooks in JSX middlewares. This is because middlewares are not components, and hooks can only be used in components.
function exampleMiddleware(next, type, props, key) {
// Don't use hooks in middlewares!
const [count, setCount] = useState(0);
return next(type, props, key);
}
Instead, you can wrap your JSX in a components that uses hooks. See Button Click Counter for an example.