Skip to content

Optimized useTracker hook #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 133 commits into from
Nov 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
f9c6ae3
Expose useTracker hook and reimplement withTracker HOC on top of it
yched Nov 4, 2018
eecab69
run the callback non-reactively in the initial render
yched Nov 5, 2018
46c243a
no need for useCallback
yched Nov 5, 2018
a2cc120
setup computation once on mount, deferring subscriptions until didMount
yched Nov 6, 2018
b61f809
stop deferred inital subscriptions on computation invalidate rather t…
yched Nov 8, 2018
2589ac7
Revert last two commits: no reactivity on mount
yched Nov 9, 2018
c8c1c5f
comments
yched Nov 11, 2018
24be369
minor: code style
yched Feb 15, 2019
e509e26
minor: coding style
yched Mar 30, 2019
5d25bf2
Handle Mongo.Cursor warnings checks inside useTracker, and user React…
yched Mar 30, 2019
ae3f323
Add a note about refs BC break and #262
yched Mar 30, 2019
3caa109
bump React minimum version to 16.8
yched Mar 30, 2019
61ef31e
minor: streamline comments
yched Mar 31, 2019
65e619d
minor: streamline useEffect
yched Mar 31, 2019
3841a4c
minor: streamline client / server versions of useTracker
yched Mar 31, 2019
ef64751
default useTracker's deps param to [] to avoid infinte rerender loop …
yched Mar 31, 2019
4486531
Docs
yched Mar 30, 2019
0041ce7
minor: linebreak fix
yched Mar 31, 2019
92f8576
docs : unify headers for useTracker / withTracker sections (include a…
yched Mar 31, 2019
d8922dd
docs : typos
yched Apr 1, 2019
c4e24f2
remove createContainer / ReactMeteorData
yched Apr 1, 2019
b7a92d6
docs : added compatibility notes
yched Apr 1, 2019
c135ef0
docs : adjust formatting / fix unfinished sentence / link to React li…
yched Apr 1, 2019
e79b596
docs : better doc for the react-hooks/exhaustive-deps ESLint config
yched Apr 3, 2019
17322a5
remove dependency on tmeasday:check-npm-versions
yched Apr 3, 2019
f0a0328
optim : only warn about Mongo.Cursor in dev environment
yched Apr 3, 2019
eb55a16
forward references to the inner component (supercedes #266)
yched Apr 3, 2019
514a87d
fix checkCursor() when reactiveFn returns null
yched Apr 23, 2019
fe4fa42
- rewrite useTracker in order to stay fully consistent with current w…
Jun 21, 2019
fbc33c6
- withTracker should always recompute on re-render so deps for useTra…
Jun 21, 2019
c8d8645
update Readme to reflect omitted deps behavior
Jun 21, 2019
44b5247
fix code comment
Jun 21, 2019
ddfb7cd
get rid of Math.random(), wasn't needed at all
Jun 21, 2019
365584f
don't handle Meteor.isServer as it's already been taken care of in ex…
Jun 21, 2019
c3803b9
replace Math.random() when enforcing an update
Jun 21, 2019
eeb115a
- align areHookInputsEqual with
Jun 22, 2019
1eb71dd
warn if dep is not an array when Meteor isDevelopment
Jun 22, 2019
6b540e6
fix prevDeps isArray check
Jun 22, 2019
8d64e41
warn if initial deps is not an array
Jun 22, 2019
b1996a2
Retain synchronous render behavior for firstRun (including after deps…
CaptainN Jul 1, 2019
af6a4a0
Fix eslint errors
CaptainN Jul 1, 2019
f31b95d
Merge branch 'hooks' into half-duplex
CaptainN Jul 1, 2019
be11569
disambiguate the disposition of previous computation - this works the…
CaptainN Jul 2, 2019
d77fab5
Merge pull request #1 from CaptainN/half-duplex
menelike Jul 4, 2019
bfbf823
Merge branch 'half-duplex' into hooks
CaptainN Jul 4, 2019
80fef10
Use 1 useRef instead of multiple
CaptainN Jul 4, 2019
8ae0db4
If reactiveFn will run synchrously next render, don't bother running …
CaptainN Jul 4, 2019
f68ae5b
Dispose of the computation early, if the deps are falsy on meteor rea…
CaptainN Jul 4, 2019
c587026
previousDeps will always equal deps at this point, so just check prev…
CaptainN Jul 5, 2019
d801348
Merge pull request #3 from CaptainN/hooks
menelike Jul 5, 2019
e63cbd0
Implement a computationHandler API, with a cleanup method
CaptainN Jul 18, 2019
c9d2e2f
use a self contained forceUpdate value
CaptainN Jul 18, 2019
1315b55
Add a check and a warning for the appropriate return type from comput…
CaptainN Jul 19, 2019
70fab72
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 19, 2019
bb29e32
Rewrite readme for brevity and to update and explain optional `deps`
CaptainN Jul 19, 2019
574416a
Add some basic tests for useTracker
CaptainN Jul 19, 2019
76e6c2c
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 19, 2019
796ce6d
add some after unmount tests
CaptainN Jul 19, 2019
f0df78a
make sure we aren't accidentally triggering reactiveDict's migration …
CaptainN Jul 19, 2019
0c1dd3c
Test changes to enclosed values when not using deps too
CaptainN Jul 19, 2019
ca47cc5
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 20, 2019
68ec9c9
Allow setting deps in withTracker options
CaptainN Jul 20, 2019
a4ce87d
add missing typeof operator
CaptainN Jul 20, 2019
f44809f
Use the full hook on the server, to allow complete computation lifecycle
CaptainN Jul 20, 2019
85fb484
Add some argument checks and move the deps check to the same block
CaptainN Jul 20, 2019
9975c73
Add tests for the computation lifecycle
CaptainN Jul 20, 2019
7de2ea6
Fix forceUpdate by using a reference to update the counter value
CaptainN Jul 20, 2019
9e59a83
add a comment explaining the use of a reference with forceUpdate and …
CaptainN Jul 20, 2019
ef320e7
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 20, 2019
bff72c7
Use a much cleaner forceUpdate method based on useReducer
CaptainN Jul 21, 2019
b9d6911
Update to avoid running side effects in render, cause double invocati…
CaptainN Jul 22, 2019
7e71790
Revert "Update to avoid running side effects in render, cause double …
CaptainN Jul 22, 2019
5d73c1a
Make "falsy" deps check in computation consistent with other checks.
CaptainN Jul 22, 2019
044e163
Port the old outdated tests to useTracker (all tests are without deps)
CaptainN Jul 23, 2019
884246c
get more ported tests working
CaptainN Jul 23, 2019
4976a3b
add a version of one of the ported tests with deps
CaptainN Jul 23, 2019
774350c
Add withTracker tests
CaptainN Jul 23, 2019
c3c73f6
Fix some previous stall points with new knowledge from old tests
CaptainN Jul 23, 2019
317e2cf
use useMemo to avoid incorporating all those deps comparison methods
CaptainN Jul 23, 2019
ee06642
fix typo in computation deps compare
CaptainN Jul 23, 2019
11f66b4
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 23, 2019
a127f3c
only check if third argument is a function if it's not falsy
CaptainN Jul 23, 2019
ce0e52b
tracked does not return a value, so don't assign it
CaptainN Jul 23, 2019
d5a2d14
add back a computation test
CaptainN Jul 23, 2019
58dfa81
using the forceUpdate counter wasn't enough for useMemo deps, use a r…
CaptainN Jul 23, 2019
8118837
use es5 functions in package.js
CaptainN Jul 23, 2019
f6d9529
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 23, 2019
aa498b5
Move argument checks to avoid using them in production
CaptainN Jul 23, 2019
2e786d2
remove remove use of deps in withTracker. It's impractical to use the…
CaptainN Jul 24, 2019
3e29ac5
Clarify some comments and fix the deps check inside the computation.
CaptainN Jul 24, 2019
8afb742
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 24, 2019
f472e22
Only warn the user if the dep value is not undefined, null or an array.
CaptainN Jul 24, 2019
070484d
Merge branch 'hooks' into hooks-cleanup-2
CaptainN Jul 24, 2019
d90ac32
better checks/optimizations
CaptainN Jul 24, 2019
cfc2a7e
pass the current computation to reactiveFn
CaptainN Jul 25, 2019
7a9db72
Update readme to include current computation being passed to reactiveFn
CaptainN Jul 25, 2019
092c592
add note about why we are checking deps for null and undefined
CaptainN Jul 25, 2019
4975a2f
Update note about Suspense support.
CaptainN Jul 25, 2019
7ac732c
Merge branch 'hooks' into hooks-useMemo
CaptainN Jul 25, 2019
d87ec12
Merge branch 'hooks-useMemo' into hooks-plus-ultra
CaptainN Jul 25, 2019
d8fba79
we don't need to process deps for useMemo, just pass them along
CaptainN Jul 25, 2019
94cbd51
Merge branch 'hooks-cleanup-2' into hooks-plus-ultra
CaptainN Jul 25, 2019
f5c424a
Attempt a solution for React Suspense.
CaptainN Jul 25, 2019
1da0e68
Mostly complete the work to suppot suspsense/concurrent mode
CaptainN Aug 5, 2019
02c406b
Only allow reactiveFn to be invoked on first run (synchronously) and …
CaptainN Aug 5, 2019
4cea85e
Only run unmounted reactiveFn one time, even without deps
CaptainN Aug 6, 2019
5c15306
Get some basic tests for suspense working.
CaptainN Aug 6, 2019
4079784
Add @testing-library/react package to package.json
CaptainN Aug 6, 2019
23c754a
Add a note about Concurrent Mode, Suspense and Error Boundaries to th…
CaptainN Aug 6, 2019
45d2b9e
Merge branch 'hooks-suspense' into hooks-plus-ultra
CaptainN Aug 6, 2019
17c87c6
Only try to force update when the component is mounted.
CaptainN Aug 6, 2019
8ab9587
Reduce allocations within the hook body. Don't allocate default value…
CaptainN Aug 6, 2019
6735d22
Use more explicit undefined check instead of falsy check
CaptainN Aug 6, 2019
0095083
Rewrite the notes on concurrent mode, suspense, and error boundaries,…
CaptainN Aug 7, 2019
adca979
Rewrite last paragraph of the section on concurrent mode for clarity
CaptainN Aug 7, 2019
b352154
Add a note about how useEffect yields for paint
CaptainN Aug 9, 2019
af3fb03
Fix up eslint errors
CaptainN Aug 9, 2019
de7041f
Make sure reactiveFn is only run once in cases where we restart the c…
CaptainN Aug 12, 2019
90b37a4
Reduce complexity and increase readability by switching to useEffectL…
CaptainN Aug 12, 2019
30f71e8
It's useLayoutEffect, not useEffectLayout...
CaptainN Aug 12, 2019
42d7d66
disentangle the server from the client impl, silences useEffectHook w…
CaptainN Aug 13, 2019
8199b3f
revert to useEffect version for compatibility with concurrent mode
CaptainN Aug 13, 2019
2cf9a62
remove server pathways from useTracker impl
CaptainN Aug 13, 2019
2a069a8
increase commit timeout to 1 second - it can take a pretty long while
CaptainN Aug 13, 2019
88b86d7
Export server version of useTracker from useTracker.js for use in wit…
CaptainN Aug 27, 2019
bcd1ef0
Since we provide a separate isServer implementation, tracked will nev…
CaptainN Aug 27, 2019
ebdf24c
Fix lost reactivity in some cases.
CaptainN Aug 27, 2019
6e20b9d
Make some comments clearer
CaptainN Aug 27, 2019
d75f6c9
Cleanup the clearTimeout methods
CaptainN Aug 27, 2019
629c87f
Comment about dispose/clear in response to reactive updates when not …
CaptainN Aug 27, 2019
718c605
Make sure refs to deps and computationHandler are always up to date
CaptainN Aug 27, 2019
048b698
Move tracked out of hook body, one less allocation on every render, a…
CaptainN Aug 27, 2019
e8784e0
Run reactiveFn on the server inside nonreactive
CaptainN Oct 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 104 additions & 19 deletions packages/react-meteor-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,127 @@ npm install --save react

### Usage

This package exports a symbol `withTracker`, which you can use to wrap your components with data returned from Tracker reactive functions.
This package provides two ways to use Tracker reactive data in your React components:
- a hook: `useTracker` (v2 only, requires React `^16.8`)
- a higher-order component (HOC): `withTracker` (v1 and v2).

The `useTracker` hook, introduced in version 2.0.0, embraces the [benefits of hooks](https://reactjs.org/docs/hooks-faq.html). 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, deps)` hook

You can use the `useTracker` hook to get the value of a Tracker reactive function in your (function) components. The reactive function will get re-run whenever its reactive inputs change, and the component will re-render with the new value.

Arguments:
- `reactiveFn`: A Tracker reactive function (receives the current computation).
- `deps`: An optional array of "dependencies" of the reactive function. This is very similar to how the `deps` argument for [React's built-in `useEffect`, `useCallback` or `useMemo` hooks](https://reactjs.org/docs/hooks-reference.html) work. If omitted, the Tracker computation will be recreated on every render (Note: `withTracker` has always done this). If provided, the computation will be retained, and reactive updates after the first run will run asynchronously from the react render cycle. 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.

```js
import { useTracker } from 'meteor/react-meteor-data';

// React function component.
function Foo({ listId }) {
// This computation uses no value from the outer scope,
// and thus does not needs to pass a 'deps' argument.
// However, we can optimize the use of the computation
// by providing an empty deps array. With it, the
// computation will be retained instead of torn down and
// rebuilt on every render. useTracker will produce the
// same results either way.
const currentUser = useTracker(() => Meteor.user(), []);

// The following two computations both depend on the
// listId prop. When deps are specified, the computation
// will be retained.
const listLoading = useTracker(() => {
// Note that this subscription will get cleaned up
// when your component is unmounted or deps change.
const handle = Meteor.subscribe('todoList', listId);
return !handle.ready();
}, [listId]);
const tasks = useTracker(() => Tasks.find({ listId }).fetch(), [listId]);

return (
<h1>Hello {currentUser.username}</h1>
{listLoading ?
<div>Loading</div> :
<div>
Here is the Todo list {listId}:
<ul>{tasks.map(task => <li key={task._id}>{task.label}</li>)}</ul>
</div}
);
}
```

**Note:** the [eslint-plugin-react-hooks](https://www.npmjs.com/package/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:

```
"react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useTracker|useSomeOtherHook|..." }]
```

#### `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.

```js
import { withTracker } from 'meteor/react-meteor-data';

// React component
function Foo({ currentUser, listLoading, tasks }) {
// ...
// React component (function or class).
function Foo({ listId, currentUser, listLoading, tasks }) {
return (
<h1>Hello {currentUser.username}</h1>
{listLoading ?
<div>Loading</div> :
<div>
Here is the Todo list {listId}:
<ul>{tasks.map(task => <li key={task._id}>{task.label}</li>)}</ul>
</div}
);
}

export default withTracker(props => {
// Do all your reactive data access in this method.
export default withTracker(({ listId }) => {
// Do all your reactive data access in this function.
// Note that this subscription will get cleaned up when your component is unmounted
const handle = Meteor.subscribe('todoList', props.id);
const handle = Meteor.subscribe('todoList', listId);

return {
currentUser: Meteor.user(),
listLoading: !handle.ready(),
tasks: Tasks.find({ listId: props.id }).fetch(),
tasks: Tasks.find({ listId }).fetch(),
};
})(Foo);
```
The first argument to `withTracker` is a reactive function that will get re-run whenever its reactive inputs change.

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 `FooContainer`'s `props` as well as `{currentUser, listLoading, tasks}`.
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](http://guide.meteor.com/react.html) in the Meteor Guide.

### Note on `withTracker` and `createContainer`
### Concurrent Mode, Suspense and Error Boundaries

The new `withTracker` function replaces `createContainer` (however it remains for backwards compatibility). For `createContainer` usage, please [see prior documentation](https://github.com/meteor/react-packages/blob/ac251a6d6c2d0ddc22daad36a7484ef04b11862e/packages/react-meteor-data/README.md). The purpose of the new function is to better allow for container composability. For example when combining Meteor data with Redux and GraphQL:
There are some additional considerations to keep in mind when using Concurrent Mode, Suspense and Error Boundaries, as each of these can cause React to cancel and discard (toss) a render, including the result of the first run of your reactive function. One of the things React developers often stress is that we should not create "side-effects" directly in the render method or in functional components. There are a number of good reasons for this, including allowing the React runtime to cancel renders. Limiting the use of side-effects allows features such as concurrent mode, suspense and error boundaries to work deterministically, without leaking memory or creating rogue processes. Care should be taken to avoid side effects in your reactive function for these reasons. (Note: this caution does not apply to Meteor specific side-effects like subscriptions, since those will be automatically cleaned up when `useTracker`'s computation is disposed.)

```js
const FooWithAllTheThings = compose(
connect(...), // some Redux
graphql(...), // some GraphQL
withTracker(...), // some Tracker data
)(Foo);
```
Ideally, side-effects such as creating a Meteor computation would be done in `useEffect`. However, this is problematic for Meteor, which mixes an initial data query with setting up the computation to watch those data sources all in one initial run. If we wait to do that in `useEffect`, we'll end up rendering a minimum of 2 times (and using hacks for the first one) for every component which uses `useTracker` or `withTracker`, or not running at all in the initial render and still requiring a minimum of 2 renders, and complicating the API.

To work around this and keep things running fast, we are creating the computation in the render method directly, and doing a number of checks later in `useEffect` to make sure we keep that computation fresh and everything up to date, while also making sure to clean things up if we detect the render has been tossed. For the most part, this should all be transparent.

The important thing to understand is that your reactive function can be initially called more than once for a single render, because sometimes the work will be tossed. Additionally, `useTracker` will not call your reactive function reactively until the render is committed (until `useEffect` runs). If you have a particularly fast changing data source, this is worth understanding. With this very short possible suspension, there are checks in place to make sure the eventual result is always up to date with the current state of the reactive function. Once the render is "committed", and the component mounted, the computation is kept running, and everything will run as expected.

### Version compatibility notes

- `react-meteor-data` v2.x :
- `useTracker` hook + `withTracker` HOC
- Requires React `^16.8`.
- Implementation is compatible with "React Suspense", concurrent mode and error boundaries.
- The `withTracker` HOC is strictly backwards-compatible with the one provided in v1.x, the major version number is only motivated by the bump of React version requirement. Provided a compatible React version, existing Meteor apps leveraging the `withTracker` HOC can freely upgrade from v1.x to v2.x, and gain compatibility with future React versions.
- The previously deprecated `createContainer` has been removed.

- `react-meteor-data` v1.x / v0.x :
- `withTracker` HOC (+ `createContainer`, kept for backwards compatibility with early v0.x releases)
- Requires React `^15.3` or `^16.0`.
- Implementation relies on React lifecycle methods (`componentWillMount` / `componentWillUpdate`) that are [marked for deprecation in future React versions](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes) ("React Suspense").
188 changes: 0 additions & 188 deletions packages/react-meteor-data/ReactMeteorData.jsx

This file was deleted.

21 changes: 0 additions & 21 deletions packages/react-meteor-data/createContainer.jsx

This file was deleted.

12 changes: 12 additions & 0 deletions packages/react-meteor-data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* global Meteor*/
import React from 'react';

if (Meteor.isDevelopment) {
const v = React.version.split('.');
if (v[0] < 16 || v[1] < 8) {
console.warn('react-meteor-data 2.x requires React version >= 16.8.');
}
}

export { default as useTracker } from './useTracker';
export { default as withTracker } from './withTracker.jsx';
Loading