react-tracker
This package provides an integration between React and Tracker
, Meteor's reactive data system.
DISCLAIMER: This is a forked and hevily modified version of react-meteor-data
. The reason for this divergance is because react-meteor-data
requires mongo to function. While this is ok, there are many apps that don't need mongo
and just need tracker. Examples would be for utilizing Session.
Table of Contents
Install
To install the package, use meteor add
:
meteor add maka:react-tracker
Or with maka-cli
maka add maka:react-tracker
You'll also need to install react
if you have not already:
meteor npm install react
Changelog
Usage
This package provides two ways to use Tracker reactive data in your React components:
The useTracker
hook, introduced in version 2.0.0, embraces the benefits of hooks. Like all React hooks, it can only be used in function components, not in class components.
The withTracker
HOC can be used with all components, function or class based.
It is not necessary to rewrite existing applications to use the useTracker
hook instead of the existing withTracker
HOC.
useTracker(reactiveFn)
basic hook
You can use the useTracker
hook to get the value of a Tracker reactive function in your React "function components." The reactive function will get re-run whenever its reactive inputs change, and the component will re-render with the new value.
useTracker
manages its own state, and causes re-renders when necessary. There is no need to call React state setters from inside your reactiveFn
. Instead, return the values from your reactiveFn
and assign those to variables directly. When the reactiveFn
updates, the variables will be updated, and the React component will re-render.
Arguments:
reactiveFn
: A Tracker reactive function (receives the current computation).
The basic way to use useTracker
is to simply pass it a reactive function, with no further fuss. This is the preferred configuration in many cases.
useTracker(reactiveFn, deps)
hook with deps
You can pass an optional deps array as a second value. When provided, the computation will be retained, and reactive updates after the first run will run asynchronously from the react render execution frame. This array typically includes all variables from the outer scope "captured" in the closure passed as the 1st argument. For example, the value of a prop used in a subscription or a minimongo query; see example below.
This should be considered a low level optimization step for cases where your computations are somewhat long running - like a complex minimongo query. In many cases it's safe and even preferred to omit deps and allow the computation to run synchronously with render.
Arguments:
reactiveFn
deps
: An optional array of "dependencies" of the reactive function. This is very similar to how thedeps
argument for React's built-inuseEffect
,useCallback
oruseMemo
hooks work.
1import { useTracker } from 'meteor/react-meteor-data'; 2 3// React function component. 4function Foo({ listId }) { 5 // This computation uses no value from the outer scope, 6 // and thus does not needs to pass a 'deps' argument. 7 // However, we can optimize the use of the computation 8 // by providing an empty deps array. With it, the 9 // computation will be retained instead of torn down and 10 // rebuilt on every render. useTracker will produce the 11 // same results either way. 12 const currentUser = useTracker(() => Meteor.user(), []); 13 14 // The following two computations both depend on the 15 // listId prop. When deps are specified, the computation 16 // will be retained. 17 const listLoading = useTracker(() => { 18 // Note that this subscription will get cleaned up 19 // when your component is unmounted or deps change. 20 const handle = Meteor.subscribe('todoList', listId); 21 return !handle.ready(); 22 }, [listId]); 23 const tasks = useTracker(() => Tasks.find({ listId }).fetch(), [listId]); 24 25 return ( 26 <h1>Hello {currentUser.username}</h1> 27 {listLoading ? ( 28 <div>Loading</div> 29 ) : ( 30 <div> 31 Here is the Todo list {listId}: 32 <ul> 33 {tasks.map(task => ( 34 <li key={task._id}>{task.label}</li> 35 ))} 36 </ul> 37 </div> 38 )} 39 ); 40}
Note: the eslint-plugin-react-hooks package provides ESLint hints to help detect missing values in the deps
argument of React built-in hooks. It can be configured to also validate the deps
argument of the useTracker
hook or some other hooks, with the following eslintrc
config:
1"react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useTracker|useSomeOtherHook|..." }]
useTracker(reactiveFn, deps, skipUpdate)
or useTracker(reactiveFn, skipUpdate)
You may optionally pass a function as a second or third argument. The skipUpdate
function can evaluate the return value of reactiveFn
for changes, and control re-renders in sensitive cases. Note: This is not meant to be used with a deep compare (even fast-deep-equals), as in many cases that may actually lead to worse performance than allowing React to do it's thing. But as an example, you could use this to compare an updatedAt
field between updates, or a subset of specific fields, if you aren't using the entire document in a subscription. As always with any optimization, measure first, then optimize second. Make sure you really need this before implementing it.
Arguments:
reactiveFn
deps?
- optional - you may omit this, or pass a "falsy" value.skipUpdate
- A function which receives two arguments:(prev, next) => (prev === next)
.prev
andnext
will match the type or data shape as that returned byreactiveFn
. Note: A return value oftrue
means the update will be "skipped".false
means re-render will occur as normal. So the function should be looking for equivalence.
1import { useTracker } from 'meteor/react-meteor-data'; 2 3// React function component. 4function Foo({ listId }) { 5 const tasks = useTracker( 6 () => Tasks.find({ listId }).fetch(), [listId], 7 (prev, next) => { 8 // prev and next will match the type returned by the reactiveFn 9 return prev.every((doc, i) => ( 10 doc._id === next[i] && doc.updatedAt === next[i] 11 )) && prev.length === next.length; 12 } 13 ); 14 15 return ( 16 <h1>Hello {currentUser.username}</h1> 17 <div> 18 Here is the Todo list {listId}: 19 <ul> 20 {tasks.map(task => ( 21 <li key={task._id}>{task.label}</li> 22 ))} 23 </ul> 24 </div> 25 ); 26}
withTracker(reactiveFn)
higher-order component
You can use the withTracker
HOC to wrap your components and pass them additional props values from a Tracker reactive function. The reactive function will get re-run whenever its reactive inputs change, and the wrapped component will re-render with the new values for the additional props.
Arguments:
reactiveFn
: a Tracker reactive function, getting the props as a parameter, and returning an object of additional props to pass to the wrapped component.
1import { withTracker } from 'meteor/react-meteor-data'; 2 3// React component (function or class). 4function Foo({ listId, currentUser, listLoading, tasks }) { 5 return ( 6 <h1>Hello {currentUser.username}</h1> 7 {listLoading ? 8 <div>Loading</div> : 9 <div> 10 Here is the Todo list {listId}: 11 <ul>{tasks.map(task => <li key={task._id}>{task.label}</li>)}</ul> 12 </div} 13 ); 14} 15 16export default withTracker(({ listId }) => { 17 // Do all your reactive data access in this function. 18 // Note that this subscription will get cleaned up when your component is unmounted 19 const handle = Meteor.subscribe('todoList', listId); 20 21 return { 22 currentUser: Meteor.user(), 23 listLoading: !handle.ready(), 24 tasks: Tasks.find({ listId }).fetch(), 25 }; 26})(Foo);
The returned component will, when rendered, render Foo
(the "lower-order" component) with its provided props in addition to the result of the reactive function. So Foo
will receive { listId }
(provided by its parent) as well as { currentUser, listLoading, tasks }
(added by the withTracker
HOC).
For more information, see the React article in the Meteor Guide.
withTracker({ reactiveFn, pure, skipUpdate })
advanced container config
The withTracker
HOC can receive a config object instead of a simple reactive function.
getMeteorData
- ThereactiveFn
.pure
-true
by default. Causes the resulting Container to be wrapped with React'smemo()
.skipUpdate
- A function which receives two arguments:(prev, next) => (prev === next)
.prev
andnext
will match the type or data shape as that returned byreactiveFn
. Note: A return value oftrue
means the update will be "skipped".false
means re-render will occur as normal. So the function should be looking for equivalence.
1import { withTracker } from 'meteor/react-meteor-data'; 2 3// React component (function or class). 4function Foo({ listId, currentUser, listLoading, tasks }) { 5 return ( 6 <h1>Hello {currentUser.username}</h1> 7 {listLoading ? 8 <div>Loading</div> : 9 <div> 10 Here is the Todo list {listId}: 11 <ul>{tasks.map(task => <li key={task._id}>{task.label}</li>)}</ul> 12 </div} 13 ); 14} 15 16export default withTracker({ 17 getMeteorData ({ listId }) { 18 // Do all your reactive data access in this function. 19 // Note that this subscription will get cleaned up when your component is unmounted 20 const handle = Meteor.subscribe('todoList', listId); 21 22 return { 23 currentUser: Meteor.user(), 24 listLoading: !handle.ready(), 25 tasks: Tasks.find({ listId }).fetch(), 26 }; 27 }, 28 pure: true, 29 skipUpdate (prev, next) { 30 // prev and next will match the shape returned by the reactiveFn 31 return ( 32 prev.currentUser?._id === next.currentUser?._id 33 ) && ( 34 prev.listLoading === next.listLoading 35 ) && ( 36 prev.tasks.every((doc, i) => ( 37 doc._id === next[i] && doc.updatedAt === next[i] 38 )) 39 && prev.tasks.length === next.tasks.length 40 ); 41 } 42})(Foo);
Maintaining the reactive context
To maintain a reactive context using the new Meteor Async methods, we are using the new Tracker.withComputation
API to maintain the reactive context of an
async call, this is needed because otherwise it would be only called once, and the computation would never run again,
this way, every time we have a new Link being added, this useTracker is ran.
1 2// needs Tracker.withComputation because otherwise it would be only called once, and the computation would never run again 3const docs = useTracker('name', async (c) => { 4 const placeholders = await fetch('https://jsonplaceholder.typicode.com/todos').then(x => x.json()); 5 console.log(placeholders); 6 return await Tracker.withComputation(c, () => LinksCollection.find().fetchAsync()); 7}); 8
A rule of thumb is that if you are using a reactive function for example find
+ fetchAsync
, it is nice to wrap it
inside Tracker.withComputation
to make sure that the computation is kept alive, if you are just calling that function
that is not necessary, like the one bellow, will be always reactive.
1 2const docs = useTracker('name', () => LinksCollection.find().fetchAsync()); 3