meteor / react-packages

Meteor packages for a great React developer experience
http://guide.meteor.com/react.html
Other
574 stars 159 forks source link

Expose useTracker hook and reimplement withTracker HOC on top of it #262

Closed yched closed 5 years ago

yched commented 6 years ago

As initially posted on forums.meteor, this is a first attempt at providing a useTracker hook to get reactive data in functionnal React components.

Example usage :

import { useTracker } from 'Meteor/react-meteor-data';
function MyComponent({ docId }) {
  const doc = useTracker(() => Documents.findOne(docId), [docId]);
  return doc ? <div><h1>{doc.title}</h1>{doc.body}</div> : <Spinner/>
}

The PR also re-implements the existing withTracker HOC on top of it, which:

A couple questions remain open (more details in comments below, to follow soon), but this already seems to work fine in our real-world app heavily based on withTracker.

apollo-cla commented 6 years ago

@yched: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/

leoc commented 6 years ago

Hi there, just now as I submitted my version of react meteor hooks (#263) I saw yours!

I separated two hooks one for subscription and one for data. I would love to get some feedback on your and mine solution. How do you handle stopping a subscription when a component is unmounted? As far as I understand, the returned function of useEffect does the cleanup. I had to separate the hook functions to have access to the handle in the return function and to return results from find().fetch() / findOne().

Your solution for the first render looks promising. Maybe one could do the same but cache the handles in an array to stop all subscriptions in a useTracker function.

Cheers :+1:

yched commented 6 years ago

@leoc

I separated two hooks one for subscription and one for data

I think offering various focused hooks like useSubscription(name, ...args), useDocument(collection, selector, options), useReactiveVar(var)... could be worthwhile, especially as it encourages splitting into granular reactive functions that only rerun when their dep gets invalidated, rather than a single big one which reruns as a whole when any of its deps gets invalidated.

But as I outlined in the OP, I do see a lot of value in providing a useTracker hook that acts like the current withTracker HOC does, i.e. accepting a function that can mix subscriptions and data : it lets us reimplement withTracker on top of it (as the PR does), which instantly makes all existing Meteor/React apps compatible with React's soon-to-be-released "concurrent mode" (Fiber, Async, Suspense, the name has changed a few times), Otherwise, you have to rewrite your whole app to use "functional components + the new hooks" instead of "classes + withTracker" to be compatible, which would be a huge pain.

Then the more specialized ones (useSubscription, useDocument, useReactiveVar...), are just a couple one-line wrappers around the generic useTracker.

(Also, hooks can't be inside if()s, meaning a hook like useTracker(reactiveFunc) is the only way to be able to conditionally subscribe to a publication - you write the condition in the function. useSubscription(name, ...args) always subscribes)

How do you handle stopping a subscription when a component is unmounted? As far as I understand, the returned function of useEffect does the cleanup.

Yes, stopping the reactive computation on cleanup automatically stops the subscriptions that it made.

Maybe one could do the same but cache the handles in an array to stop all subscriptions in a useTracker function.

Yep, I have written just that actually, didn't find the time to push yet :-) Will try do so soon.

yched commented 6 years ago

So yes, the challenge here is :

Last commit solves that by having the Meteor.subscribe stub collect the subscriptions attempted at mount time. We then actually do those subscriptions manually in useEffect (and manually register them to stop when the Tracker.autorun computation is stopped). When the dependencies change, useEffect recreates a new Tracker.autorun computation and no special handling of subscriptions is needed there.

yched commented 6 years ago

Adjusted the "defer subscriptions from mount time to didMount time" part :

In short, I guess I need feedback from Tracker / DDP experts here :-)

yched commented 6 years ago

Well, come to think of it: the thing about "not firing Meteor.subscribe() at mount time because we wouldn't be able to stop the subscriptions if the mount was cancelled / restarted later by React concurrent mode", also applies to Tracker.autorun computations : we wouldn't be able to stop them if the mount is canceled.

A Tracker computation is a kind of subscription (the general notion of subscription, not Meteor.subscribe specifically) : something that you setup at some point and need to stop/cleanup to avoid memory leaks. React concurrent mode says "no subscription at mount time, only at didMoiunt / didUpdate with useEffect()", that applies to Tracker.autorun too.

So I guess the approach before the last two commits was the right one :

AFAICT, that's also the approach other libraries that do "return the results of a function and update them over time as they change" (like react-redux with its connect(mapStateToProps(state, props)) for instance) are taking : run once on mount, then rerun and setup change tracking on didMount. The difference is that we additionally need to silence Meteor subscriptions made in the first run.

Reverting to 46c243a, then. Simpler code, less awkward messing around with subscriptions...

jchristman commented 6 years ago

Whoever is in charge of this project - what are your thoughts on this method? I really love the syntax and am wondering if we would be better off spinning of a new package for this so that we can take advantage of it now (as opposed to just doing it as a “polyfill” duke in each project)?

Tl; dr - package maintainers should chime in so we can stop wasting time on this pull request and have someone step up to create a new package.

jchristman commented 6 years ago

To be clear, I believe this is the right spot for the code to live (with documentation on meteor guide already). Maybe we create a package that can be the standin until hooks are out of RFC stage?

yched commented 6 years ago

Side note : I'll be away for the next two weeks and won't be working on this in the meantime. As far as I can tell, the current code works fine and looks stable to me.

Still TBD : how exactly the package should deal with "stick with current implementation for React < [the 1st React version that officially ships hooks], use the hook-based implementation otherwise". Maybe just a new major release that requires a minimal React version ?

But yeah, on that question and on the code itself, feedback from the package maintainers is probably what's needed now.

yched commented 6 years ago

@jchristman : Yes, ideally this (or something like this) would simply be the next version of react-meteor-data.

As for creating an actual, temporary/experimental package with the code from this PR so that people can actually start using it : why not I guess - at the moment I have no plans of doing that myself, but I'm perfectly fine with someone else taking the code and publishing a package though ;-)

It should IMO just be very clear that it is a temporary package until react-meteor-data provides somithing similar, and that the code hasn't been vetted yet by the authors of the current React integration ?

dburles commented 6 years ago

This looks really awesome! my only worry is createContainer/withTracker backwards compatibility. Are there any strong reasons why we can't leave them as is?

yched commented 6 years ago

@dburles: see above : the big gain is instantly making all existing apps compatible with React Suspense, by moving away from componentWillMount / componentWillUpdate (fixing #256, #252, #242, #261)

FWIW, our app is pretty withTracker-intensive, and seems to work just fine with the drop-in hook-based reimplementation in this PR.

jchristman commented 6 years ago

@dburles, there’s also no reason that we couldn’t make a documentation note in README that says React <16 use react-meteor-data@0.2.16?

dburles commented 6 years ago

Okay I agree, I think it's worth it. A note in the readme is also a good idea. @hwillson any thoughts here?

hwillson commented 6 years ago

I like it! We should do some additional testing though, so when this PR is ready to be merged, we'll cut a beta for people to try out. @yched Let us know when you think this PR is ready for review. Thanks!

yched commented 6 years ago

@hwillson @dburles cool !

I think the PR is ready for review at the moment. You can read the comments above for details about another implementation I tried and abandoned (in short : React Suspense forbids Tracker.autorun / Meteor.subscribe at mount time)

Still TBD : how exactly should the package deal with "stick with current implementation for React < 16.[the 1st version that officially ships hooks], use the hook-based implementation otherwise". Maybe just a new major release that requires a minimal React version ?

mattblackdev commented 5 years ago

Merge! :)

menelike commented 5 years ago

Imho the benefits of this PR in conjunction with React 16.8 outweigh the urge to stay backward compatible. I just submitted https://github.com/meteor/react-packages/pull/266 which also relies on a newer React version (16.3). I'm happy to refactor that PR once this has been merged.

Also once generally approved https://github.com/meteor/react-packages/pull/262/files#diff-f54656ab7cd59d21708df27a3c521da5R21 should be solved ;)

yched commented 5 years ago

@dburles @hwillson : any feedback ? React 16.8 has shipped with hooks :-)

dburles commented 5 years ago

Just going to summarise what I think should be the plan:

yched commented 5 years ago

@dburles : works for me.

I'll try to do that in the next couple days

holmrenser commented 5 years ago

Any updates on this? Are there any contributions required that I could add? @yched if you need help with the documentation, I would happily contribute.

CaptainN commented 5 years ago

Would it make sense to make this a separate package - maybe called react-meteor-hooks (or meteor-react-hooks?). It's an opportunity to remove some of the cruft from that older package, including exposing the goods using the lazy module flag.

The changes could even be back ported to react-meteor-data eventually (but not necessarily right now) by updating the old package to use the new package, and replacing withTracker.

What do you think?

Also, if anyone is interested, I wrote a couple of super small hooks on top of this - I'm really digging hooks! Feels like Meteor again.

const useSubscription = (name, ...rest) => useTracker(
  () => Meteor.subscribe(name, ...rest).ready(),
  [name, ...rest]
)

/// ... used elsewhere
const isReady = useSubscription('my-pub', 'arg1', 'arg2')

And I always wanted an easy way to persist data through HCP, and hooks finally allow that!

import { Session } from 'meteor/session'

export const useSession = (name, defaultValue) => [
  useTracker(() => {
    // Session.setDefault prevents resetting the value after hot code push
    Session.setDefault(name, defaultValue)

    return Session.get(name)
  }, [name, defaultValue]),
  (val) => Session.set(name, val)
]

// ... used simply:
const [myVar, setMyVar] = useSession('mySessionVar', 'initial-value')

(I settled on using Session for for a bunch of reason I explained in the forums.)

mattblackdev commented 5 years ago

Are you related to Benjamin? Because this idea is fire. I totally think it makes sense. On Mar 21, 2019, 3:44 PM -0400, Kevin Newman notifications@github.com, wrote:

Would it make sense to make this a separate package - maybe called react-meteor-hooks. It's an opportunity to remove some of the cruft from that older package, including exposing the goods using the lazy module flag. The changes could even be back ported to react-meteor-data eventually (but not necessarily right now) by updating the old package to use the new package, and replacing withTracker. What do you think? Also, if anyone is interested, I wrote a couple of super small hooks on top of this - I'm really digging hooks! Feels like Meteor again. const useSubscription = (name, ...rest) => useTracker( () => Meteor.subscribe(name, ...rest).ready(), [name, ...rest] )

/// ... used elsewhere const isReady = useSubscription('my-pub', 'arg1', 'arg2') And I always wanted an easy way to persist data through HCP, and hooks finally allow that! import { Session } from 'meteor/session'

export const useSession = (name, defaultValue) => [ useTracker(() => { // Session.setDefault prevents resetting the value after hot code push Session.setDefault(name, defaultValue)

return Session.get(name) }, [name, defaultValue]), (val) => Session.set(name, val) ]

// ... used simply: const [myVar, setMyVar] = useSession('mySessionVar', 'initial-value') (I settled on using Session for for a bunch of reason I explained in the forums.) — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

CaptainN commented 5 years ago

@mattblackdev Ha! No, but I'll take that as a compliment!

Another note, PR #263 has thinking along the lines of what I've suggested, with a slightly different implementation. I think it makes sense to have a core useTracker as defined in this PR, with various other implementations built on top of that.

leoc commented 5 years ago

Ha! I love these helper hooks. I see why adding one basic useTracker hook makes sense.

I just wanted to release my work so far as react-meteor-hooks package but you/someone beat me to it.

https://www.npmjs.com/package/react-meteor-hooks

Looks good! :+1:

CaptainN commented 5 years ago

@leoc If you release that as an Atmosphere package we could reduce the overhead of the packager boilerplate in the modern bundle. (It'd be nice if there was a way to achieve the same from npm packages.)

dburles commented 5 years ago

I think all we're missing is the documentation. @yched have you begun work on that? If not, @holmrenser mentioned he could take it up.

yched commented 5 years ago

Sorry about that, been prettty busy for the past weeks. Will give it a try in the very next couple days.

It seems the next minor react version is going to trigger deprecation warnings about the deprecated componentWillXxx() lifecycles the withTracker HOC is currently using, so we might want to hurry a bit getting rid of those :-/

yched commented 5 years ago

It seems the next minor react version is going to trigger deprecation warnings about the deprecated componentWillXxx() lifecycles

Forgot to include the link : https://github.com/facebook/react/pull/15186

yched commented 5 years ago
yched commented 5 years ago

I think we should be ready here - proofreading is more than welcome for the docs ;-)

Also, @dburles : do you think we still need to keep the last part about createContainer ? (do you think we still need to export createContainer and the whole of ReactMeteorData, for that matter ?)

CaptainN commented 5 years ago

In another recent PR (#268), the version check was removed from this package, a decision I agree with.

yched commented 5 years ago

@dburles is that what you had in mind ?

dburles commented 5 years ago

@yched looks perfect!

jchristman commented 5 years ago

Pu-blish! Pu-blish! Pu-blish!

Edit: I’m a professional lurker and have been watching this for months, completely lacking the time to contribute. I’m not ashamed. 😁

dburles commented 5 years ago

@hwillson I think we are ready to publish a beta release. I left a comment earlier about how I think we should do so:

Prior to releasing this (for both beta and production). We should first publish the current package as version 1.0.0 and then release this as version 2.0.0, since the package currently is released as development versions 0.x.x.

I'll leave it to you to merge and publish as I don't have publish permissions on Atmosphere.

@yched Was there anything outstanding in terms of other PR's we should merge for this release? I wrote earlier that #266 would be good to include.

Edit: Sorry @yched I must have overlooked your comment on that PR, I'll have to spend some time on the details of what it entails, but I am happy to go with your best judgement on it.

yched commented 5 years ago

@dburles : In terms of other PRs :

CaptainN commented 5 years ago

To replace checkNpmVersions, we could put a simple runtime check for the react version - it'd be much smaller than 10K to do so:

if (Meteor.isDevelopment ) {
  // TODO: Here we can even kick a warning saying react is not installed, if it's not detected
  const v = React.version.split('.')
  if (v[0] < 16 || v[1] < 8) {
    console.warn('react-meteor-data 2.0 requires React version >= 16.8, etc.')
  }
}

If there is a way to only include this warning in a Development bundle in Meteor's package system, that'd be even better.

yched commented 5 years ago

@CaptainN : yup, that would work. @dburles : where do you think the removal of checkNpmVersions() should happen ? v2 or v1 ? And if v2, do you think such a change belongs to this PR or rather a separate one ?

FWIW, I've been willing to move the checks about reactive funcs returning a Mongo.Cursor to Meteor.isDevelopment as well, but likewise, that is not strictly related to this PR here ?

In short : we sort of know where we want to go, it's more a matter of orchestration now ;-) I'm a bit wary that cramming to much in a single PR might get in the way of actually merging it - but I'll go whichever way you see fit.

dburles commented 5 years ago

We could publish the removal of checkNpmVersions in v1.1.0, but I don't think it's worth the effort since we want to retire 1.0 anyway. I'm happy for these enhancements to be included in this PR, since I wouldn't consider them major features by themselves.

yched commented 5 years ago

Alright, so :

This is now an all-inclusive PR :-)

aadamsx commented 5 years ago

FYI: If you can't wait for this PR to pulled, and you need to get started with hooks, here's an alternative while we wait: https://github.com/andruschka/react-meteor-hooks

yched commented 5 years ago

Bump - any news ? :-)

macrozone commented 5 years ago

@yched i found a bug:

given: const currentUser = useTracker(() => Meteor.user())

if you logout on develop, you'll get:

TypeError: Cannot convert undefined or null to object

because of checkCursor which will break if the passed argument is null

yched commented 5 years ago

@macrozone Nice catch - typeof null === 'object' :-/ Should be fixed now.

dburles commented 5 years ago

Hey @benjamn @abernix, just drawing some attention to this PR here. It looks like it's ready to go. We just need someone with the capability of publishing to atmosphere to get this sorted.

The general plan is (as I had described earlier):

Prior to releasing this (for both beta and production). We should first publish the current package as version 1.0.0 and then release this PR as version 2.0.0, since the package currently is released as development versions 0.x.x.

We should release this as version 2.0.0 since it includes breaking changes. @hwillson also mentioned first publishing a beta version for testing, but I don't mind either way. I believe @yched has been using it in production for some time.

jchristman commented 5 years ago

I’ve also been using this in production as a local file import instead of a node_modules, through the power of copy/pasta. I’d certainly prefer to have only one package, since I till use withTracker for more complex Tracker related things...

yched commented 5 years ago

bump @dburles @benjamn @abernix ?

dburles commented 5 years ago

👋 @abernix @benjamn @hwillson just drawing attention here again as this release is at the mercy of anyone who has the rights to publish under the mdg org on atmosphere. There's been a lot of time and effort put in by the community on this feature, it would be great to get it out there.