SoFunction
Updated on 2025-04-07

Sample code for implementing React two-way binding hooks 30 lines of code

We are deeply impressed by the responsive data in Vue and MobX. In the React function component, we can also rely on hooks to implement a simple and easy-to-use useReactive.

Take a look at our goals

const CountDemo = () => {
  const reactive = useReactive({
    count: 0,
  });
  return (
    <div
      onClick={() => {
        ++;
      }}
    >
      {}
    </div>
  );
};

Simply put, we no longer need to manually trigger the setState handler. If we modify the data, the data in the component will be updated directly.

In Vue, we need to implement data responsiveness in a summary:

1. Analyze template collection dependencies

2. Publish subscription to implement updates

The process of React function components will be simpler by relying on the characteristics of functions, because every time the function component renders itRe-execute, we only need to change the data and then trigger component rendering to achieve our goal.

Therefore, the core of implementing this custom hook is:

1. Maintain the same data

2. Hijacking operations on data

3. Trigger component updates during hijacking operations(setState)

Using Proxy Proxy Data

This proxy pattern is the core of implementing responsive data. Vue2.0 uses defineProperty for data hijacking, but now it is replaced by the Proxy mode. In a word, the difference between defineProperty and proxy is that the former hijacks the property accessor, while the latter can proxy the entire object (Vue3.0, MobX).

Proxy has up to 13 types of interceptors, and what we used this time areget, set, delete

const observer = (initialState, cb) =&gt; {
  const proxy = new Proxy(initialState, {
    get(target, key, receiver) {
      const val = (target, key, receiver);
      return typeof val === "object" &amp;&amp; val !== null ? observer(val, cb) : val; // Recursively process object types    },
    set(target, key, val) {
      const ret = (target, key, val);
      cb();
      return ret;
    },
    deleteProperty(target, key) {
      const ret = (target, key);
      cb();
      return ret;
    },
  });
  return proxy;
};

The above observer completes the basic operation agent for data.

Here is a knowledge point:Why do Proxy proxy objects often access with Reflect instead of operators?

ReflectMore comprehensive and more powerful:

  • As long as the proxy method that the Proxy object has, the Reflect object has all, and exists in the form of a static method. These methods can perform default behavior,regardless Proxy How to modify the default behavior can always be passed Reflect The corresponding method gets the default behavior

For example, in line 4 above, here(target,key,receiver)At first glance, it seems to be possible withtarget[key]Equivalent, but in fact, it is not the following example. It is precisely because the third parameter of Reflect's static method receiver can be used to specify this when it is called, so use(target,key,receiver)Only by returning the correct result as we expected.

let o = {
  getb() {
    return ;
  },
};
let o1 = (
  newProxy(o, {
    get(target, key, receiver) {
      return (target, key, receiver);
    },
  })
);
 = 42;
; // 42

let o2 = (
  newProxy(o, {
    get(target, key) {
      return target[key];
    },
  })
);

 = 42;
; // undefined
  • Modify the return result of some Object methods to make it more reasonable. For example, if (obj, name, desc) cannot define a property, an error will be thrown, while (obj, name, desc) will return false.
  • Let Object operations become function behavior. Some Object operations are imperative, such as name in obj and delete obj[name], and (obj, name) and (obj, name) make them function behavior.
const useReactive = (initState) => {
  return observer(initState);
};

Our basic structure is roughly shown in the above code snippet, but here are two problems:

1. We want the function component to beimplementIt's alwaysRefer to the same proxy object

2. Observer only needs to be proxyed once during the life cycle of the component

Create the same data reference using useRef

When we see maintaining the same data, our first reaction may be to use closures to create references, but in this way we also need to manually maintain the component creation and uninstallation and the relationship between this data, and React is born with it.refWith such an API, we do not need to manage data uninstallation and binding from ourselves. We can achieve our goal by directly using useRef in the function component.

const useReactive = (initState) => {
  const ref = useRef(initState);
  return observer();
};

In this way, we use useRef and Proxy to implement the proxy for initialState

Add update handler

We found that there is also a missing handler, that is, the component update is triggered after data changes. In fact, it is relatively simple to just setState after operating the value of ref.

Because it is inside the function component, we can directly borrow useState to introduce a "update trigger" and pass this trigger into the observer proxy method.

function useReactive&lt;S extends object&gt;(initialState: S): S {
    const [, setFlag] = useState({});
    const ref = useRef &lt; S &gt; (initialState)
    return observer(, () =&gt; {
        setFlag({}); // {} !== {} Therefore, component update will be triggered    });
}

Remove Proxy multiple times

After completing the above steps, we can basically achieve the effect in the beginning demo, but there is another problem:

Since it is the function component after state updateuseReactiveIt will also be executed, soobserverIt will be executed multiple times, and as we expect, this proxy behavior should only be executed once at the beginning of component creation. Therefore, we also need to make some transformations here, and the method still depends onRef returns the same data when the function component executes multiple timesThis feature:

function useReactive(initialState) {
  const refState = useRef(initialState);
  const [, setUpdate] = useState({});
  const refProxy = useRef({
    data: null,
    initialized: false,
  });
  // When creating the ref of proxy, we add an initialized flag bit, so that when the component state update is executed  // UseReactive executes again and can determine whether to directly return the data value on current or re-execute proxy based on this flag bit  if ( === false) {
     = observer(, () =&gt; {
      setUpdate({});
    });
     = true;
    return ;
  }
  return ;
}

Add cache to improve code

The above solves the problem of repeated execution caused by the update method of function components. Here we also need to solve the duplicate proxy caused by external operations. That is, if an initialState has been proxyed, then we do not want it to be proxyed by a secondary (the user may have used useReactive twice to proxy the same object), we can useWeakMapTo cache records

const proxyMap = new WeakMap();
const observer = (initialState, cb) =&gt; {
  const existing = (initialState);
  // Add cache to prevent rebuilding proxy  if (existing) {
    return existing;
  }

  const proxy = new Proxy(initialState, {
    get(target, key, receiver) {
      const val = (target, key, receiver);
      return typeof val === "object" &amp;&amp; val !== null ? observer(val, cb) : val; // Recursively process object types    },
    set(target, key, val) {
      const ret = (target, key, val);
      cb();
      return ret;
    },
    deleteProperty(target, key) {
      const ret = (target, key);
      cb();
      return ret;
    },
  });
  (initialState, proxy);
  return proxy;
};

Summarize

So far oursuseReactiveIt's basically available, review the entire code:

const proxyMap = new WeakMap();

const observer = (initialState, cb) =&gt; {
  const existing = (initialState);
  if (existing) return existing;
  const proxy = new Proxy(initialState, {
    get(target, key, receiver) {
      const val = (target, key, receiver);
      return typeof val === "object" &amp;&amp; val !== null ? observer(val, cb) : val; // Recursively process object types    },
    set(target, key, val) {
      const ret = (target, key, val);
      cb()
      return ret;
    },
    deleteProperty(target, key) {
      const ret = (target, key);
      cb();
      return ret;
    },
  });
  return (initialState, proxy) &amp;&amp; proxy;
};

function useReactive(initialState) {
  const refState = useRef(initialState);
  const [, setUpdate] = useState({});
  const refProxy = useRef({
    data: null,
    initialized: false,
  });
  if ( === false) {
     = observer(, () =&gt; {
      setUpdate({});
    });
     = true;
    return ;
  }
  return ;
}

Sandbox Example

/s/silly-haze-xfuxoy?file=/src/

Although there are few codes, it has all the internal organs. The above useReactive implementation method is almost the same as useReactive in ahooks. This package also contains many other simple and useful hooks collections. Interested friends can learn about the implementation of other hooks, which can help you develop your business and help you deepen your understanding of the working principle of React.

This is the article about the example code of 30 lines of code to implement React two-way binding hooks. For more related React two-way binding hook content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!