-
Notifications
You must be signed in to change notification settings - Fork 158
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
[WIP] New hooks #298
[WIP] New hooks #298
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not mean to start a review.
@CaptainN really enjoying the I have run into a use case though that I'm not sure how to approach. useSubscription( () => {
if ( boardId ) return Meteor.subscribe( 'board.projects', boardId )
}, [ boardId ] ) This triggers a warning because currently the return value must be a |
Hmm, that's an interesting problem with One way to handle this as is, would be to use 2 components, and make your decision decide which to use. But that's an onerous requirement. The easiest thing to do would be to support a falsy return value (such as "undefined" or "void" in your example), and simply do nothing in that case. I'll make that change. |
An other way (which I'm using to work around it now) is to do this: const noSubscription: Meteor.SubscriptionHandle = {
stop() {},
ready: () => true,
}
useSubscription( () => {
if ( boardId ) {
return Meteor.subscribe( 'board.projects', boardId )
} else {
return noSubscription
}
}, [ boardId ] ) But this is a little too weird for a relatively common use case like this, and I had to go into the hook's source to find it. So support for a falsy return value would be a great usability improvement. |
@CaptainN another thing I noticed, is when you do something like this: useSubscription( () => Meteor.subscribe( 'board.projects', boardId ), [ boardId ] ) A new subscription is correctly initialized every time the Not sure if this is a bug or intended behaviour, but running the cleanup function (in this case, Edit: it seems the subscriptions aren't even stopped upon unmounting the component with the |
Fix subscriptions not closed when stopping computation
Next interesting thing I noticed: the ready state for the new This is with And the exact same code with Note that using |
Do we need testing for these? These hooks will be pretty important, unwanted rerenders or invalid information could have big consequences in an application. I'm happy to write the tests if someone with more testing experience could help me set them up. It might make development easier as well (removing the back and forth every time the code is changed). |
We need more readmes, testing and probably some attention on the SSR side of things. |
I've been swamped for a few days. Hoping to get to this by the weekend. |
Tried out the latest version, and it seems to be working well. Already loving this API: // useSubscription( () => Meteor.subscribe( 'boards' ), [] )
useSubscription( 'boards' ) // ✨ And coincidentally I also found this makes my Suspense version a perfect drop-in replacement! Not sure if this was intended, but a nice bonus. import { useSubscription } from '/lib/react-mongo' // No suspense
// import useSubscription from 'meteor-subscribe-suspense' // Suspense
useSubscription( 'board.projects', boardId ) |
Big update:
Notes:
|
It looks like I'm running in to issues with the way subscriptions are recycled when using the cleaner I think this means a couple of things
Based on this, it might make the most sense to remove the deps version of That is, unless there is some way to properly manage this Flush lifecycle within React's lifecycle constraints. |
At the risk of suggesting something wild: could it help to adjust Tracker? No need to treat this code as sacred IMO, it was written in a different time, before React even existed. It is of course important for it to stay frontend agnostic, but an optimization for one the most used frontends should not be a crazy idea, right? |
@rijk I thought of modifying Tracker for other purposes - to create immutability for example. But it turned out to be significantly less effort to write the integration the way it is now, with much lower risk of effecting other projects which rely on Tracker. For this particular problem, I think the algorithm codified in However, I'd still like to have something a bit more optimized for these new hooks, even if that means we have the more special purpose Tracker implementation (which is not strictly compatible with Meteor.subscribe) only used internally. So we'd. go back to offering For the main hook, we'd sunset the |
I think I found a nice solution to the problems described above. Basically, I'm forcing a flush lifecycle for that initial computation in I did confirm that this problem can sometimes crop up in the current version of the hook. I think the way forward is to cherry pick the new I created PR #306 for the |
import { Tracker } from 'meteor/tracker' | ||
import { useState, useEffect } from 'react' | ||
|
||
export const useUserId = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just watched your talk in Impact Meteor 2020 and when you first introduced this guy I thought it would be global managed. I'm wondering that we are opening a computation every time we need userId in a component. I thought zustand would better fit this problem, although it might not be necessary because I don't know the impacts of openings many computations. What do you think? It could be applied in others states as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Computations are incredibly cheap in practice. In Blaze they are the backbone of the entire system. In Meteor, for years when using withTracker
it would actively create and destroy a computation on every react render (it still does, as does useTracker
when no deps are provided). It's basically adding one entry to an array internally, for the reactive source. Very cheap.
Still, to recycle some of the more expensive reactive sources (not the computation itself) like an expensive reactive subscription, you can always hoist the computation up your react tree (er, or down to root), and then use the context API to feed the result of that one computation to mulitple hooks. I showed how to do that in the hook introduction blog post (here is the gist). Be aware though, this can introduce some subtle timing issues with reactivity and the React lifecycle. It's easier (and cheap) to just use a lot of computations. ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One point I mean to stress whenever we ship these, is they are not solving a problem with the current HOC/hook - useTracker
is fast as is, and we'll still recommend its use with Collection.findOne
for example. We/I simply couldn't come up with anything better for that.
These new hooks are the result of some exploratory effort, to see if we can gain some advantage by implementing things in a more custom way. What I found was that some of my earlier assumptions (which I did express in the Impact Meteor 2020 preso) did not pan out - just exposing a cursor was actually quite a bit slower and less efficient than useTracker
...
The main advancements we did achieve here are in 2 very specific cases (so far anyway, there may be others when we/I get to them). useSubscription
is more optimized for subscriptions in particular. In practice, it probably won't amount to much in terms of gains, but it has a cleaner API than useTracker
for subscriptions in particular. The second one is the big win - useFind
is now immutable, and doesn't require the subscription to be mixed in with the cursor. This has the potential to open up new use cases for high performance reactivity in lists over what useTracker
and withTracker
could deliver. I say "has the potential" because these statements have not been well tested, and there are likely tradeoffs here (as with any performance optimizations). But really, the main advantage of these 2 hooks is mostly that they (arguably) improve developer experience, through a more tailored/integrated feeling API between Meteor and React, and I always love that type of improvement.
Along the way, I also discovered some improvements we should make to useTracker
which have been submitted as a separate PR.
Couple of things:
I'm not sure if that second bullet point works with Given this complexity, I'm leaning toward ditching |
@CaptainN has anything been done with the idea of a third argument to I needed this for myself (some re-renders are very expensive), so I modified You can use it like so: import * as fastDeepEqual from 'fast-deep-equal'
const versions = useTracker(()=> Versions.find().fetch(), [], fastDeepEqual) Using fastDeepEqual is just for example of course. In my case, I have a collection transform that makes the returned records immutable so I can do a more shallow comparison. I've been using it in production for a few months now, and it's working great for me. |
I have a branch with that on it. Actually, let me open a WIP PR (#321). I'd love to know more about how you made your records immutable (collection transform?). The |
@CaptainN nice. Actually I've only gone for document level immutability too because that's what looked to make sense performance-wise. Here's a (very) cut down piece of the approach: const Records = new Mongo.Collection("records", {
transform: RecordClass.load
})
class RecordClass {
static load(data) {
const unchanged = _dataCache[data._id] &&
fastDeepEqual(data, _dataCache[data._id])
if (unchanged) return _recordCache[data._id]
return _recordCache[data._id] = new RecordClass(data)
}
/* ... */
} A better approach might be to do the Even better, have the cursor observer just invalidate the record cache, which the first transform for that would then repopulate, something like: Records.find().observe({
changed: doc=> delete _recordCache[doc._id],
removed: doc=> delete _recordCache[doc._id]
})
const Records = new Mongo.Collection("records", {
transform: doc=> _recordCache[doc._id] ||= new RecordClass(doc)
}) The upside is that every If I did want field-level immutability, I would either use the observer hook: |
What do you guys think about this plan:
Both of these on a separate PR/branch, so make merge simpler. In use, it'd look something like: import { Meteor } from 'meteor/meteor'
import React, { memo } from 'react'
import { Messages } from '/imports/api/collections/messages'
import { useTracker, useFind } from 'meteor/react-meteor-data'
const ChatScreen = () => {
const roomId = 'Y5hoT957PKphnuqCA'
const isLoading = useTracker(
() => !Meteor.subscribe('messages', roomId), [roomId]
)
if (isLoading) return null
else return <MessagesList roomId={roomId} />
}
const MessagesList = ({ roomId }) => {
const testMessages = useFind(
() => Messages.find({ roomId }, { sort: { createdAt: 1 } }),
[roomId]
)
return (<>
<article className="messages">
{testMessages.map((message) => (
<Message
key={message._id}
message={message}
/>
))}
</article>
</>)
}
const Message = memo(({ message }) => {
return <div>{message.message}</div>
})
export default ChatScreen This structures things so that it's optimized for memoization, and for concurrent rendering. One of the things to watch out for, is a lot of examples for setting up subscriptions do the subscription and the list rendering in the same component, but this actually blunts concurrent modes ability to do its work (a little), by forcing that first concurrent render to render something like Anyway, I'd like to go ahead and get the |
@gunn I think the way As far as the comparator, in general, I don't like a deep equal check as a standard avenue (even fast-deep-equal) because it's hard to predict the performance of it. It can work, but it needs to be used with care and consideration, because with certain types of data that comparison can be more expensive than rerenders. That said, I added a comparator option to |
Couldn't we at least add a simple Just something like this: function useSubscription( name, ...args ) {
useTracker(
() => !Meteor.subscribe( name, ...args ).ready(),
args
)
} |
@rijk That sounds/looks good in principle, but how do you know whether the final argument should be passed to useTracker or Meteor.subscribe? (In your code, you pass it to both, but that's probably not what you want.) I found it a little weird that useFind needed to be passed a closure with a find call, instead of a collection and the find arguments, but the latter approach has a similar issue. Except Collection.find never takes an array argument, so we could actually tell what we should do with the last argument... Related, is there a reason (e.g. efficiency) that useFind needs to be a different hook, instead of useTracker behaving specifically when the return value is a cursor or array of cursors? The latter is what I expected, coming from Blaze. |
Yeah it's the reflex of adding deps like you have to do in In that case it could be even simpler, something like this: function useSubscription( name, ...args ) {
useTracker( () => !Meteor.subscribe( name, ...args ).ready() )
} The const loading = !useSubscription( 'messages', roomId ) Or if you don't care about loading state: useSubscription( 'messages', roomId ) I think this goes a long way not only in developer experience and less keystrokes, but also how tight the Meteor/React integration feels. const isLoading = useTracker(
() => !Meteor.subscribe('messages', roomId), [roomId]
) .. is something you'd probably have to look up, const isLoading = !useSubscription( 'messages', roomId ) you can probably remember. Edit: side note: It would also be nice if |
With A simple wrapper for const useSubscription = (name: string | null, ...args: any[]) => (
useTracker( () => Meteor.subscribe(name, ...args).ready() )
); |
Package.describe({ | ||
name: 'react-accounts', | ||
summary: 'React hook for reactively tracking Meteor data', | ||
version: '0.9.0', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably start at v1.0.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'll make that change in a new PR
}); | ||
|
||
Package.onUse(function (api) { | ||
api.versionsFrom('1.10'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's start at least at 1.12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for this is simply that I can't get the tests to run on anything newer than Meteor 1.10...
meteor/react-meteor-state | ||
========================= | ||
|
||
A state hook, with an API similar to React's useState, which provides data persistence between page reloads using hte Meteor reload package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo in hot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That whole readme needs to be written. :)
@CaptainN You should probably write up a blog post to go together with this once it gets released. |
Hey all - I've broken out the remaining work from this PR to #332 ( I'll do the same for Please review and comment on those new PRs! The |
I added #335 for |
A set of packages with new alternative, and much more streamlined hooks for integration between React and Meteor
useSubscription
anduseCursor
meteor/react-mongo
useMeteorState
meteor/react-meteor-state
useUser
anduseUserId
meteor/react-accounts
Basic implementations are in.
Todo: