The case when it will be useful to have changeable Proxy.target

There are situations where we need "stable references" to objects. Example createRef โ€“ React

That function creates special Reference object that later will receive real object. The caveat is that we always need to access real value with .current property

In fact that functionality may not require anything special. That createRef can be implemented as

function createRef() {
   let p;
   function apply(that) { Proxy.changeTarget(p,that) }
   p = new Proxy(null, {apply});
   return p;
}

So we can use proxies also as object references.

function Component() {
   const input = createRef(); 
   return <main>
        <label>ref</label> 
        <input type="text" ref={input} /> 
        <button onClick={ () => input.focus() }>focus</button>
   </main>
}

here input will receive reference to real DOM element after its creation.

If a proxy could change target that would produce a value that breaks the specified invariants: ECMAScriptยฎ 2024 Language Specification

e.g in the React ref example the value goes from being a callable function to a non-callable object.

Another example would be values that go from true to false for Object.isFrozen. This would make the language harder to reason about, and potentially invalidate existing checks in code.

One way to achieve something similar in React today:

function Component() {
   const [input, setInput] = React.useState();
   return <main>
        <label>ref</label> 
        <input type="text" ref={setInput} /> 
        <button onClick={ () => input.focus() }>focus</button>
   </main>
}

Though personally I would stick with React.useRef and ref.current

One way to achieve something similar in React today

Well, in that particular case I've solved this without any state

function Component() {
   let input;
   return <main>
        <label>ref</label> 
        <input type="text" var={input} /> 
        <button onClick={ () => input.focus() }>focus</button>
   </main>
}

under the hood, in my JSX implementation that works as if the following is given:

   return <main>
        <label>ref</label> 
        <input type="text" var={ target => input = target  } /> 
        <button onClick={ () => input.focus() }>focus</button>
   </main>

And the runtime calls functions in @var's upon DOM element construction. This works without any explicit proxies and references.

You could restrict what changes are allowed to avoid that.

I'm not a fan of OP's suggestion anyways, but for different reasons (namely, performance).

I'd say that react creates an unstable reference, since it swaps out .current behind our backs :-) Or maybe the traditional use case of a react ref is that it's a stable but lazily initialised reference.

Regardless, I don't see why you would need Proxy.changeTarget for this. You can already implement this today:

function createRef() {
  let current = null;
  return new Proxy(newValue => {
    if (typeof newValue === "function") throw new TypeError("Cannot create a mutable ref to a function");
    current = newValue;
  }, {
    getOwnPropertyDescriptor(_, key) {
      return Reflect.getOwnPropertyDescriptor(current, key);
    },
    defineProperty(_, key, descriptor) {
      return Reflect.defineProperty(current, key, descriptor);
    },
    has(_, prop) {
      return Reflect.has(current, prop);
    },
    deleteProperty(_, key) {
      return Reflect.deleteProperty(current, key);
    },
    get(_, key, receiver) {
      return Reflect.get(current, key, receiver);
    },
    set(_, key, value, receiver) {
      return Reflect.set(current, key, value, receiver);
    },
    getPrototypeOf(_) {
      return Reflect.getPrototypeOf(current);
    },
    setPrototypeOf(_, prototype) {
      return Reflect.setPrototypeOf(current, prototype);
    },
    isExtensible(_) {
      return Reflect.isExtensible(current);
    },
    preventExtensions(_) {
      return Reflect.preventExtensions(current);
    },
    ownKeys(_) {
      return Reflect.ownKeys(current);
    },
  });
}

(This does probably violate a few proxy invariants, but it's the basic implementation of what you desire).

1 Like

Wouldn't be too hard to correct, though, and you have two general routes to soundness:

  1. Easy: Make preventExtensions and defineProperty always fail and have getOwnPropertyDescriptor only return (possibly failing) writable descriptors. Doing this (and never forwarding them) would ensure it can still change over time.
  2. Hard: Disallow target swapping after either preventExtensions is called or defineProperty is called with a configurable: false descriptor. This ensures descriptors always remain consistent for as long as property values can change.

2 also may have other requirements around the backing target and proxy target, but you get the gist.