JavaScript proxies as a refactoring tool

Tim Petricola
Alan Product and Technical Blog
3 min readOct 9, 2023
Source: Unsplash

JavaScript has a powerful meta-programming API: Proxy. It can have many usages, one being to know when an object’s property is being accessed. Let’s see how we can rely on it to gain confidence while refactoring code.

Some context

Some time ago, we updated some legacy parts of our application from react-router from v3 to v5. One of the main changes between the 2 versions is the removal of Higher-Order-Components in favor of hooks:

// react-router v3
const Component = withRouter(({ params }) => {
const id = params.id
...
})

// react-router v5
const Component = () => {
const params = useParams();
const id = params.id
...
}

The change seems straightforward enough. But some of our legacy code doesn’t have strict typing. In such a situation, it’s easy to miss some usages of the injected params prop, especially when it’s passed to child components through prop-drilling.

Statically catching all cases is tricky, so it can be interesting to get some feedback at runtime.

A Proxy primer

Proxies are a potent mechanism. Among other things, it allows setting spies on object getter/setter methods. For instance, we can log everything a method is called, without interfering with the original object behavior:

const params = {
secretValue: 42
};

const proxy = new Proxy(params, {
get(target, property, receiver) {
console.log(`Accessing property: ${property}`);
return Reflect.get(target, prop, receiver);
}
});
const secretValue = proxy.secretValue
// logs: "Accessing property: secretValue"

Using Reflect.get allows retrieving the value in the target object (42 in our example).

Spying on Higher-Order-Components props

Proxies can help get proper data from userland. Just wrap the props in a Proxy, and when a property is accessed, log it to your favorite place (we use Datadog at Alan). Any metadata can be added for easier investigation. (e.g. component name, URL where it was used, …).

We first kept the withRouter Higher-Order-Component for our migration but replaced it with a custom withMigrationRouter.

This router wraps all props in a proxy, before passing them to the child component. The proxy spies on property access and logs it:

import { withRouter } from "react-router";

const DEPRECATED_PROPS = ["params", "location"];
const proxyDeprecatedProps = (props) => {
const newProps = { ...props };
for (const deprecatedProp of DEPRECATED_PROPS) {
newProps[deprecatedProp] = new Proxy(newProps[deprecatedProp], {
get(target, prop, receiver) {
logger.warn(
"[react-router-migration] Component used a deprecated v3 property",
{
prop: `${deprecatedProp}.${String(prop)}`,
pathname: props.location?.pathname,
}
);
return Reflect.get(target, prop, receiver);
},
});
}
return newProps;
};
export const withMigrationRouter = (Component) =>
withRouter((props) => <Component {...proxyDeprecatedProps(props)} />);

Having this in place, we can now look at logs, to make sure we’ve removed all usage of the injected props before completely removing the Higher Order Component.

And here we are! Thanks to this logging mechanism, we’ve been able to seamlessly migrate from one version to another, without introducing breaking changes. We have several usages of proxies around our codebase, but that’s another story!

--

--