Last active
February 16, 2021 01:08
-
-
Save gunn/90bf091fa6c714683abb6ad73448a53f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare var Package: any | |
import { Meteor } from 'meteor/meteor'; | |
import { Tracker } from 'meteor/tracker'; | |
import { useReducer, useState, useEffect, useRef, useMemo, DependencyList } from 'react'; | |
// Warns if data is a Mongo.Cursor or a POJO containing a Mongo.Cursor. | |
function checkCursor (data: any): void { | |
let shouldWarn = false; | |
if (Package.mongo && Package.mongo.Mongo && data && typeof data === 'object') { | |
if (data instanceof Package.mongo.Mongo.Cursor) { | |
shouldWarn = true; | |
} else if (Object.getPrototypeOf(data) === Object.prototype) { | |
Object.keys(data).forEach((key) => { | |
if (data[key] instanceof Package.mongo.Mongo.Cursor) { | |
shouldWarn = true; | |
} | |
}); | |
} | |
} | |
if (shouldWarn) { | |
console.warn( | |
'Warning: your reactive function is returning a Mongo cursor. ' | |
+ 'This value will not be reactive. You probably want to call ' | |
+ '`.fetch()` on the cursor before returning it.' | |
); | |
} | |
} | |
// Used to create a forceUpdate from useReducer. Forces update by | |
// incrementing a number whenever the dispatch method is invoked. | |
const fur = (x: number): number => x + 1; | |
const useForceUpdate = () => useReducer(fur, 0)[1]; | |
interface IReactiveFn<T> { | |
<T>(c?: Tracker.Computation): T | |
} | |
type TrackerRefs = { | |
computation?: Tracker.Computation; | |
isMounted: boolean; | |
trackerData: any; | |
} | |
const useTrackerNoDeps = <T = any>(reactiveFn: IReactiveFn<T>) => { | |
const { current: refs } = useRef<TrackerRefs>({ | |
isMounted: false, | |
trackerData: null | |
}); | |
const forceUpdate = useForceUpdate(); | |
// Without deps, always dispose and recreate the computation with every render. | |
if (refs.computation) { | |
refs.computation.stop(); | |
// @ts-ignore This makes TS think ref.computation is "never" set | |
delete refs.computation; | |
} | |
// 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) => { | |
refs.computation = c; | |
if (c.firstRun) { | |
// Always run the reactiveFn on firstRun | |
const data = reactiveFn(c); | |
if (Meteor.isDevelopment) { | |
checkCursor(data); | |
} | |
refs.trackerData = data; | |
} else { | |
// For any reactive change, forceUpdate and let the next render rebuild the computation. | |
forceUpdate(); | |
} | |
})); | |
// To avoid creating side effects in render with Tracker when not using deps | |
// create the computation, run the user's reactive function in a computation synchronously, | |
// then immediately dispose of it. It'll be recreated again after the render is committed. | |
if (!refs.isMounted) { | |
// We want to forceUpdate in useEffect to support StrictMode. | |
// See: https://github.com/meteor/react-packages/issues/278 | |
if (refs.computation) { | |
(refs.computation as Tracker.Computation).stop(); | |
delete refs.computation; | |
} | |
} | |
useEffect(() => { | |
// Let subsequent renders know we are mounted (render is comitted). | |
refs.isMounted = true; | |
// Render is committed. Since useTracker without deps always runs synchronously, | |
// forceUpdate and let the next render recreate the computation. | |
forceUpdate(); | |
// stop the computation on unmount | |
return () =>{ | |
refs.computation?.stop(); | |
} | |
}, []); | |
return refs.trackerData; | |
} | |
const useTrackerWithDeps = <T = any>(reactiveFn: IReactiveFn<T>, deps: DependencyList, shouldNotUpdate?: CompareFn<T|undefined>): T => { | |
const forceUpdate = useForceUpdate() | |
const data = useRef<T>() | |
const setData = (newData: T)=> { | |
data.current = newData | |
forceUpdate() | |
} | |
useMemo(() => { | |
// To jive with the lifecycle interplay between Tracker/Subscribe, run the | |
// reactive function in a computation, then stop it, to force flush cycle. | |
const comp = Tracker.nonreactive( | |
() => Tracker.autorun((c: Tracker.Computation) => { | |
if (c.firstRun) data.current = reactiveFn(); | |
}) | |
); | |
// To avoid creating side effects in render, stop the computation immediately | |
Meteor.defer(() => { comp.stop() }); | |
if (Meteor.isDevelopment) { | |
checkCursor(data.current); | |
} | |
}, deps); | |
useEffect(() => { | |
const computation = Tracker.autorun((c) => { | |
const newData = reactiveFn(c) as T; | |
const shouldUpdate = !shouldNotUpdate?.(newData, data.current); | |
if (shouldUpdate) { | |
setData(newData); | |
} | |
}); | |
return () => { | |
computation.stop(); | |
} | |
}, deps); | |
return data.current as T; | |
} | |
type CompareFn<X> = (newData: X, oldData: X)=> boolean | |
const useTrackerClient = <T = any>(reactiveFn: IReactiveFn<T>, deps: DependencyList|null = null, shouldNotUpdate?: CompareFn<T>): T => | |
(deps === null || deps === undefined || !Array.isArray(deps)) | |
? useTrackerNoDeps(reactiveFn) | |
: useTrackerWithDeps(reactiveFn, deps, shouldNotUpdate); | |
const useTrackerServer = <T = any>(reactiveFn: IReactiveFn<T>, deps: DependencyList, shouldNotUpdate?: CompareFn<T>): T => | |
Tracker.nonreactive(reactiveFn) as T; | |
// When rendering on the server, we don't want to use the Tracker. | |
// We only do the first rendering on the server so we can get the data right away | |
const useTracker = Meteor.isServer | |
? useTrackerServer | |
: useTrackerClient; | |
const useTrackerDev = <T = any>(reactiveFn: IReactiveFn<T>, deps: DependencyList, shouldNotUpdate?: CompareFn<T>): T => { | |
if (typeof reactiveFn !== 'function') { | |
console.warn( | |
'Warning: useTracker expected a function in it\'s first argument ' | |
+ `(reactiveFn), but got type of ${typeof reactiveFn}.` | |
); | |
} | |
if (!Array.isArray(deps)) { | |
console.warn( | |
'Warning: useTracker expected an array in it\'s second argument ' | |
+ `(dependency), but got type of ${typeof deps}.` | |
); | |
} | |
return useTracker(reactiveFn, deps, shouldNotUpdate); | |
} | |
export default Meteor.isDevelopment | |
? useTrackerDev | |
: useTracker; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment