Incompatible with React Forget compiler
Intended outcome: success
Actual outcome: Found the following incompatible libraries: mobx
How to reproduce the issue:
git clone https://github.com/rikisamurai/compiler-test.git
npm i
npx react-compiler-healthcheck
Versions 6.12.3
At the moment React Forget assumes that all object reads are from immutable objects. E.g. something like <div>{store.todos[0].author.name}</div> is rewritten by Forget to a pointer check on just store before re-rendering the div. So if store is unchanged it assumes statically that todos, author and name also can't have changed. So this interferes with the runtime checking and possibility to mutate objects that MobX offers.
So we're depending on some future option to opt-out form this at object or component level. Until then the architectures are fundamentally incompatible, as far as I can see atm (without severely regressing the DX of MobX).
On the upside, many of the optimisations that are offered now by Forget, were already provided by MobX.
I've been playing with the compiler playground and the only issues I could find are with a store accessed either from a global directly or through a global getter function (it seems to cache those), but if they come either via props or via a hook (including context) it seems ok, so maybe the solution is to just replace globals with a silly hook?
For example, if we have something like like: const rootStore = getRootStore()
the user could turn that into
const useRootStore = () => getRootStore() const rootStore = useRootStore()
and I think he should be ok
https://playground.react.dev/#N4Igzg9grgTgxgUxALhASwLYAcIwC4AEwBUYCAsghhADQlkDKeAhnggQL4EBmMEGBADogYCZnDzDBAOxlwI0sIQD6S3AgBifDAHEANhABGzPQQC8RDgG4Z3KNIloFBAOYI8TdVv76jJgBQAlEQEoniw0gSqeF7avsZ6Vpy29o7OpAgM-JkxokEhYRFRaqLeugYJyQ4KSgSeogDCCmwAHoQWcKKsCE3SrXj+0bE+FSaBMjIILTj4BAAmCNzMUHqEdg54TpHkAJ4AglhY+cAyBATyioQl7BYZWRg56kE2spGh7kUAPPU9zVN4ADoAAp8ABuaAWMAIoJMUAQZmA1w4AD5TmcCJ8mtgCNcyiCIFgwAikQB6VFvM6Y-hYABMONymm0+MJxIZXDJaM+JJ+vX6wLBEIQMGRLw4E1eJJJBAA7mg8AALAjMGAwZg7MApDZbAhYo7EXFMviEzjBE5vC61A38AASEAgAGtzPRMtkfs80RargyyvETE6hqU4qNEh6al7huU-Ho6RY3B5vUGo+7zWH6RHef8nRkM21-Dy-m1Ai8zoUYJFPuT0Ri5mhQcjEQn+MywACYnMIGAANoABgAugDlgrcADpMwHhwuTW62jKVP61aMLaHa2IO2u32B1AhzAR2OEBOSXOZ9Xa-PG5GEiu1z3+4P5cPR+PJ6fj585w2I77o1eOzfN9vdyfQ8XwpE86w-QN+BzQE7wfPcDyPN4uXJMUJSlWUhy3JUVTVDVpHWNJIl1Gl-H1c9mxNIhQ0uNNIMXO1HVuRhXQZZMzk9WjGRGKN-QXL9i3OVM+ODGNXHcH4fWDNjBJohdoKzMhoLzBloKLNFS3LSt0TfU8IK4jBm3-e8d0ffdn2nUCdPAhcl3tIy4KAxCqyss9P2DeyTPg8ytNnXThKjGkPMAszgIs5z3zkgsYK3YzgoQkDKQ5aRUJADggA
Expand the source map section in the right, it is easier to follow that way.
This looks indeed very promising! @xaviergonz
As a user of the library: I agree that some of the optimizations that forget provides are already done by mobx. However, there are quite a few cases we've had in our (rather complex) mobx codebase where I suspect react forget would have helped.
Specifically, it is still very easy to create accidental render cascades with functions (if you don't use useCallback). In addition, I'd classify mobx's strength at avoiding unnecessary render calls with memoization.
Forget is different - it can only run parts of a function, and it can then avoid render cascades to not-impacted parts.
So, overall, we suspect that react-forget would create additional performance improvements for us. That said, I can't measure the impact for various reasons (mainly time constraints). On the other hand, I do know that I've optimized 20+ hot paths with react-forget style optimizations, and they've helped quite a bit, especially with stuff that needed to run at 60+ FPS.
Hey folks, if you're using MobX-State-Tree, I have some early progress on React Compiler compatibility. A custom hook like this will work for properties (I haven't expanded it to views yet, but it should be possible) using MST snapshots:
import { useEffect, useState } from "react";
import { IStateTreeNode, getSnapshot, onSnapshot } from "mobx-state-tree";
export function useObservableProperty<
T extends IStateTreeNode,
K extends keyof ReturnType<typeof getSnapshot<T>>
>(model: T, property: K) {
const [value, setValue] = useState(() => {
const snapshot = getSnapshot(model);
return snapshot[property];
});
useEffect(() => {
if (!model) return;
const disposer = onSnapshot(model, (snapshot) => {
if (snapshot[property] !== value) {
setValue(snapshot[property]);
}
});
return disposer;
}, [model, property, value]);
return value;
}
export function useObservable<T extends IStateTreeNode>(model: T) {
return new Proxy({} as ReturnType<typeof getSnapshot<T>>, {
get: (target, property) => {
if (typeof property === "string") {
return useObservableProperty(
model,
property as keyof ReturnType<typeof getSnapshot<T>>
);
}
return undefined;
},
});
}
I know there have been some issues with hooks and MobX before, but I think we need to figure out some path forward to be React Compiler ready.
I looked at using useSyncExternalStore for this, but it seems like it causes re-renders even when changing properties that weren't observed, so I think writing custom hooks for MobX, MobX-State-Tree, MobX-Keystone, and other libraries might give us the ability to express the observable API in a way that React wants to consume (using a combination of plain values and built-in React hooks).
I hope folks find this helpful. I'm reaching out to MST contributors for feedback, and I'll be experimenting some more over the coming weeks. Happy to adjust my approach if there's a more MobX-focused path forward. I'd be happy to compare notes!
Any updates from mobx maintainers on this now that react compiler is in beta?
From what I have tested, now if you tag a component with observer the react compiler will just not optimize that component at all, which I guess is a sort of solution?
We're still working on it for MobX-State-Tree, but closing in on a workable solution.
The MST solution reproduces the observable HOC as a React hook and then allows for full compilation. Still has a few bugs, but I believe an approach like that could work for MobX as well.
The MST solution uses some MST APIs for convenience, so it won't work for plain MobX quite yet.
https://github.com/coolsoftwaretyler/react-compiler-demo-with-mobx-state-tree
From what I have tested, now if you tag a component with observer the react compiler will just not optimize that component at all, which I guess is a sort of solution?
But since all components in a MobX codebase should be tagged with observer ...
But since all components in a MobX codebase should be tagged with observer ...
Right, even if the compiler won't error out, a typical MobX codebase just won't benefit from the compiler because most of your components will be skipped.
MobX itself brings a lot of the same benefits as the compiler, but not all. We need to collaborate on solutions or risk MobX being left behind by the React ecosystem, and existing codebase put in a poor spot where they can't benefit from modern React tools.