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.
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.
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.
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.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
