Eliminate observer in Formily/reactive and Keep Real‑Time Updates

An in‑depth guide shows how to remove the need for the observer wrapper in Formily/reactive by leveraging React’s internal __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, using ReactionStack, Object.defineProperty, and ahooks to automatically refresh components when reactive data changes.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Eliminate observer in Formily/reactive and Keep Real‑Time Updates

Background

Developers familiar with React have likely used state‑management tools such as Redux or MobX. Recently we have been using formily/reactive extensively, which works similarly to other state‑management libraries but requires wrapping components with observer. Forgetting this wrapper leads to stale UI, so we explore ways to drop observer while keeping automatic updates.

About State Management Tools

As the saying goes, “Know your enemy before you defeat it.” We first understand why reactive state‑management tools need an observer -like function.

formily/reactive is the reactive data framework under Alibaba’s formily solution, mainly used in form models to handle field relationships. It enables fine‑grained component refreshes, and observer acts as a watcher that registers a component as a dependent of a reactive state, triggering a refresh when the state changes.

Although observer seems reasonable, JavaScript is single‑threaded, so at any moment only one component can be rendering. If we can obtain the currently rendering component, we could achieve the same effect without an explicit observer wrapper.

Reactive Principle

When creating a reactive object via the API, formily/reactive intercepts set and get using Proxy. During a component render that accesses a reactive state, the intercepted get records the component in a “dependency basket”. Later, when the state changes, the intercepted set iterates over that basket and forces each component to re‑render.

Thus, formily/reactive already handles most of the work; we only need to manage the dependency basket.

We can store a simple refresh function in the basket, for example the function returned by ahooks ’s useUpdate.

Run Demo

We start with a minimal demo to illustrate the desired behavior.

import { model } from '@formily/reactive';

const reactiveData = model({
  data: 1,
  add() {
    this.data += 1;
  }
});

function App() {
  return (
    <div style={{ padding: 40 }}>
      <button onClick={() => reactiveData.add()}>{reactiveData.data}</button>
    </div>
  );
}

The demo does not update the displayed number because the App component never re‑renders when reactiveData.data changes.

demo1
demo1

React's Secret Internals

React does not expose a public API for the current rendering component, but it contains a hidden property __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED. This object bridges the internal implementation of hooks and maintains React’s internal state.

The secret object lives in react-reconciler and holds ReactCurrentDispatcher , which in turn stores the current hook dispatcher for the rendering phase.

Inspecting the dispatcher reveals that hook implementations differ across phases: mount, update, and after render.

hooks implementation diagram
hooks implementation diagram
hooks phase diagram
hooks phase diagram

Using ReactCurrentDispatcher and Object.defineProperty

Because the dispatcher value changes with the rendering phase, we can intercept its current property via Object.defineProperty to detect when a component starts and finishes rendering.

import * as React from 'react';
let currentDispatcher;
const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactInternals } = React;
function init() {
  Object.defineProperty(ReactInternals.ReactCurrentDispatcher, 'current', {
    get() { return currentDispatcher; },
    set(nextDispatcher) { currentDispatcher = nextDispatcher; }
  });
}
init();

By examining useCallback.toString() we can infer the current phase (invalid, mount, update) and expose a helper:

const getDispatcherType = (dispatcher) => {
  if (!dispatcher) return 'invalid';
  const impl = dispatcher.useCallback.toString();
  if (/Invalid/.test(impl)) return 'invalid';
  if (/mountCallback/.test(impl)) return 'mount';
  return 'update';
};

During the set interceptor we compare the previous and next dispatcher types to know when rendering enters or leaves a component.

// inside set interceptor
const currentDispatcherType = getDispatcherType(currentDispatcher);
const nextDispatcherType = getDispatcherType(nextDispatcher);
currentDispatcher = nextDispatcher;
if (currentDispatcherType === 'invalid' && ['mount', 'update'].includes(nextDispatcherType)) {
  // component started rendering
} else if (['mount', 'update'].includes(currentDispatcherType) && nextDispatcherType === 'invalid') {
  // component finished rendering
}

Integrating with Formily

When a component starts rendering we push a refresh function (from useUpdate) onto ReactionStack; when rendering ends we pop it.

import { ReactionStack } from '@formily/reactive/esm/environment';
function _useWatcher() {
  const trackRef = React.useRef();
  const update = useUpdate();
  if (!trackRef.current) {
    ReactionStack.push(update);
  }
}
// called at the start of rendering
_useWatcher();
// called after rendering
ReactionStack.pop();

This completes the bridge between React’s rendering lifecycle and Formily’s reactive tracking.

Conclusion

We demonstrated a proof‑of‑concept that removes the explicit observer wrapper from Formily/reactive by hijacking React’s internal dispatcher and using ReactionStack together with ahooks. The approach has many edge cases—such as race conditions in the set interceptor, server‑side rendering, and production‑mode differences—so it is not recommended for production use.

ReActhooks
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.