I came up with this simple useTracker
alternative because I was frustrated by how many stuck reactives I had.
CAVEAT: Does not work with subscribe
, or any reactive which needs some initial async setup to return useful data. A simple variant for those is to place the computation only in the useEffect and use deps since publications are JSONable. ~However I do not see how to implement it so that isLoading
immediately switches to true
when the deps change.~ That can be done with a hook that detects deps changes.
const useTrackerClientImpl = <T = any>(
reactiveFn: IReactiveFn<T>,
deps?: DependencyList,
skipUpdate?: ISkipUpdate<T>,
): T => {
const [iteration, forceUpdate] = useUniqueObject();
const data = useMemo(() => {
let data: T = shouldNeverBeReturned as T;
// Use Tracker.nonreactive in case we are inside a Tracker Computation.
// This can happen if someone calls `ReactDOM.render` inside a Computation.
// In that case, we want to opt out of the normal behavior of nested
// Computations, where if the outer one is invalidated or stopped,
// it stops the inner one.
Tracker.nonreactive(() =>
Tracker.autorun((c: Tracker.Computation) => {
assert(c.firstRun);
data = reactiveFn(c);
}),
).stop();
return data;
}, deps && [iteration, ...deps]);
useEffect(() => {
let prevData = data;
const computation = Tracker.nonreactive(() =>
Tracker.autorun((c: Tracker.Computation) => {
const nextData = reactiveFn(c);
if (
(!c.firstRun || !areDeepEqual(prevData, nextData)) &&
!skipUpdate?.(prevData, nextData)
) {
prevData = nextData;
forceUpdate();
}
}),
);
// Stop the computation on deps changes or unmount.
return () => {
computation.stop();
};
}, deps);
assert(data !== shouldNeverBeReturned);
return data;
};
The trick is to import areDeepEqual from 'react-fast-compare';
to skip the first potentially duplicated run of the useEffect
computation. This is particularly problematic when not using a dependency array.