I didn’t see that aspect highlighted yet. Sounds powerful now.
@a.com something like:
function MyComp(props) {
const doc = useTracker(props => {
Meteor.subscribe('my_subscription', props.var);
return MyCollection.findOne(props.id);
}, [props.var, props.id]);
return <div>{doc ? doc.title : 'No document found'}</div>;
}
You can also return an object with several values (like you currently do with the withTracker HOC), for instance if you also want the ‘ready’ status of the subscription:
function MyComp(props) {
const { ready, doc } = useTracker(props => {
const sub = Meteor.subscribe('my_subscription', props.var);
return {
ready: sub.ready(),
doc: MyCollection.findOne(props.id)
};
}, [props.var, props.id]);
return ready ? <div>{doc ? doc.title : 'No document found'}</div> : <Spinner />;
}
I’d tend to think a better practice would be to split independant results each into their own useTracker calls, since each reactive func then gets recomputed on its own only when necessary:
function MyComp(props) {
const ready = useTracker(props => {
const sub = Meteor.subscribe('my_subscription', props.var);
return sub.ready();
}, [props.var]);
const doc = useTracker(props => MyCollection.findOne(props.id), [props.id]);
return ready ? <div>{doc ? doc.title : 'No document found'}</div> : <Spinner />;
}
I have just added this to local package directory in a medium sized app using withTracker
extensively as a package override for react-meteor-data
, and in my not too extensive testing I can confirm, it works pretty well! I’m going to actually deploy this to our testing server and see how it goes.
Is there a Pull Request for this yet? (Er, nm, I found it.)
I’m kind of digging hooks. I made this quick useSubscription
hook on top of this:
// bad code, doesnt work
// export const useSubscription = (name, ...rest) => {
// const [isReady, setIsReady] = useState(false)
// useTracker(() => {
// const handler = Meteor.subscribe(name, ...rest)
// setIsReady(handler.ready())
// return () => { handler.stop() }
// }, [name, ...rest])
// return isReady
// }
/// elsewhere
// const isReady = useSubscription('my-pub', 'arg1', 'arg2')
Pretty spiffy. This is actually untested, but I’ve found it fairly easy to express various hooks, and to do so with much less code than using the old container method. Now I just have to figure out how to make a useMeteorState
wrapped around ReactiveVar, so that we can have persistent local state, which survives hot code push!
UPDATE: This code was incorrect. I mixed up the syntax (useTracker’s API is a mix of useSession
and useEffect
), and made it work like it would if useTracker was like useEffect - but it’s not. Here is a (simpler) corrected version:
export const useSubscription = (name, ...rest) => useTracker(
() => Meteor.subscribe(name, ...rest).ready(),
[name, ...rest]
)
/// elsewhere
const isReady = useSubscription('my-pub', 'arg1', 'arg2')
looking forward to it
So it looks like ReactiveVar does not persist through hot code pushes. ReactiveDict does, and Session is a single instance of ReactiveDict in a package. Cheeky. Also, ReactiveDict needs a globally unique ID in order to function correctly, which means we can’t have the API compatible version of useState
which survives hot code push (at leas not uing using ReactiveDict) that I was after.
Some other observations:
- There doesn’t seem to be an instance specific, guid inside React to use for that uniquie identifier, that I can access anywhere. I think there used to be (maybe still is) one, but it is not public, and I don’t think it’s guaranteed to the be the same value between tree builds (page refresh). If this is incorrect, and there is some reliable instance specific ID in React - please let me know!
- For a direct ReactiveDict hook, we’d need to enforce a guid at the API level. This makes the API challenging for consumers, and I still think we need a better option for the thing to be useful. (If a user were to use the component name, every component of that type would use the same value. They could use the component name + some unique id tied to some data the component is dealing with, but that could mean every instance with both of those would use the same data, and we wouldn’t want that, etc.)
- Since instance based IDs are impractical for us, we should probably focus on a
useSession
hook. The implication that every key must be unique is already set for Session, so we can, er, offload that concern to the user. This is actually probably the easiest to implement - we can just use withTracker, and access the Session API directly. - Another option would be to use our own custom hook entirely - don’t even bother with ReactiveDict, and produce a custom persistence layer using Meteor’s
Reload
package. - Of course another option would be to leverage one of React’s popular state managers, like Redux and maybe write a custom middleware built around Meteor’s Reload package - or just use one of the various existing offline middlewares.
That’s a lot to think about I suppose! I’m going to use Session in the short term. A solution for something like temporary form data would look something like this:
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)
]
// elsewhere
const [myVar, setMyVar] = useSession('mySessionVar', 'initial-value')
A couple of notes:
- I had originally tried cleaning up the Session value with
useEffect(() => () => Session.delete(name))
but it didn’t work. I’m not sure I understand how to use that well enough yet. - Funky things happen if you change
name
ordefaultValue
. The setter you get back (setMyVar in the example) stays locked with the previous value. There is some info in the hooks manual about this - and it is expected behavior - it’s just funky. I did some testing using unHooked setTimeouts, and ran into these issues - started to feel a LOT like Meteor coding when not properly using tracked functions (there are some useTimeout hook examples out there). A proper implementation of this should warn the user if they try to change either of those values.
Some opining:
I wonder if it would make sense to wrap some of these hooks into a new package (react-meteor-hooks
), rather than trying to replace the old stuff in react-meteor-data
. There’s a ton of old code in that old package, and I would hesitate changing any of that. It worked well enough in my limited testing to replace withTracker with this hooks based one (well, from the updated one in the PR), but it did seem to make my app behave a little differently - like it painted differently (this is completely subjective, and maybe not even accurate). Additionally, there’s a bunch of stuff in there we don’t need, so why not take the opportunity to do a clean break? The old package is also eagerly loaded, and it may make a mess to try to get folks to update to newer react version, etc. Just some thoughts.
If the goal is to have a global reactive state which survives hot code push, we can combine Session
with reactn. reactn will provide hooks and helpers to access/set the state. All we have to do is initiate the reactn state from Session
and update the Session
whenever the reactn state changes:
import { addCallback, setGlobal } from 'reactn'
import { Session } from 'meteor/session'
const initState = {
foo: 'bar',
}
// this will execute whenever the reactn global state changes
// update the Session as well.
addCallback(state => {
Session.set('globalState', state)
})
// set the default Session
Session.setDefault('globalState', initState)
// set reactn's initial global state from Session
// which survived hot code push
setGlobal(Session.get('globalState'))
Then inside a react functional component, we can simply do:
const [foo, setFoo] = useGlobal('foo')
Also, I’m not sure I fully understand your comment about the funky thing, but have you read this post about Dan’s useInterval
hook?
That looks similar to what I mentioned above about creating a custom middleware for redux - similar idea, except reactn gives a hook api more similar to the session API I set up. I hadn’t heard of reactn before (there are so many of these!) This implementation looks similar to what I did, with a little more redux style indirection with setting up the default value.
The funky thing is that the setVar callback you get from the hook is bound to the specific state of a specific run of render (or in this case, since it’s memoized, the state of those two variables). It’s mentioned in the react hooks docs, and it’s not unexpected, it’s just a little bit funky. I don’t know if I saw this post, but I did read another that had a hooks based setTimeout implementation (and also explained the funkiness better than I did).
reactn
feels very “natural” alongside useState
, and if you need more complexity, you can use reducer functions. I don’t think global state management can be simpler
If we are going to do “global” state management, and it looks like we need to for persistence, then you might be right. Maybe it makes sense to be strict about defining the shape of your global store outside of the flow of the program (and I love that reactn avoids “reducers” which feels like kabuki theater more often than it doesn’t - I’ll be looking into this package).
What I really wanted was a way to avoid all that, so that the programmer just has to deal with the code right in front of them - a useState that would survive HCP without globals.
I’m fine with the trade offs we’ve defined here though.
I’m on Meteor 1.8 and Blaze. I’m in the process of converting my Blaze codebase over to React 16.8 (using only the functional approach with Hooks).
The follow is an example of how I’m using Tracker (using https://github.com/meteor/react-packages/tree/devel/packages/react-meteor-data). My question is, instead of using the the HOC approach here, is there a way to use a Hooks approach?
Also, what are the current patterns for using React with Meteor + client state that DO NOT involve using yet another 3rd party library (I don’t want to take on new dependencies is I can avoid it).
<template name="BlazeComponentExample">
<nav>
{{> React component=ReactComponentExample }}
</nav>
</template>
import ReactComponentExample from './react-component-example';
Template.BlazeComponentExample.helpers({
ReactComponentExample() {
return ReactComponentExample || null;
}
});
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import SidebarNavigation from '../sidebar/sidebar-navigation';
import { statusState, iconState } from '../navbar-helpers';
const ReactComponentExample = ({ mode }) => {
...
return (
<ul>
<li>
<SidebarNavigation mode={mode} />
</li>
</ul>
);
}
export default withTracker(() => {
const handle = Meteor.subscribe('userAccount', Meteor.userId());
const { accountMode } = accounts.findOne({ userId: Meteor.userId() }) || '';
return {
loading: !handle.ready(),
mode: accountMode ? accountMode : ''
}
})(ReactComponentExample);
@aadams According to the package maintainers, the PR at https://github.com/meteor/react-packages/pull/262 is on its way to be merged as a version 2.0 of react-meteor-data, so if you’re starting from scratch and plan on favoring function componenents, I’d definitely suggest going with the useTracker
hook provided there (usage sample in my comments above, and in the README.md file in the PR - the exising withTracker
HOC is still available, just reimplemented on top of the hook).
A 2.0-beta release should be available soon(ish? - not in my control), but in the meantime you can use it in your app by cloning the git@github.com:yched/react-packages.git
repo, checking out the hooks
branch and copying the packages/react-meteor-data
folder into your app’s packages
folder - so that you end up with a folder structure like:
.meteor/
packages/
react-meteor-data/
package.js
(...rest of the package files)
(...rest of you app's source code)
Thanks for the advice @yched, and thanks for the PR that will hopefully get pulled soon! In the mean-time I cloned: https://github.com/yched/react-packagesp and copied your version into a new /packages folder I created for this purpose.
I uninstalled react-meteor-data that I had used before. Now my imports can’t find the location. How do I import from the packages directory I created? I tried ‘packages/react-meteor-data’ but then the transpiler is stating I need to do a meteor npm install --save packages, which I know is not correct.
Simply run meteor add react-meteor-data
to install the copied package as local packages take precedence over online packages. You should now see react-meteor-data
in your .meteor/packages
file.
Ah, so since I have now have a /packages/react-meteor-data
directory with the contents of yched/react-meteor-data:hooks
branch, the lib installed from meteor add react-meteor-data
will get overwritten?
Yes! it should overwrite it once that branch is officially merged to v2.0.0 react-meteor-data after you must have run meteor add react-meteor-data
. Another option is to use the npm package react-meteor-hooks - super easy once you get it.
Thanks for this @martineboh!
Wow https://github.com/andruschka/react-meteor-hooks looks great! And it uses hooks for subscriptions instead of doing a Meteor.subscribe inside a Tracker hooks as is the case with react-meteor-data.
I think I’ll just use react-meteor-hooks for now since it looks to have everything I need and for some unknown reason react-meteor-data seems to be stalled
“it uses hooks for subscriptions instead of doing a Meteor.subscribe inside a Tracker hooks”
I’m not sure those are such a great idea, actually, and I intentionally left them out of the initial PR
For example, a subscription made with react-meteor-hooks’s useSubscription() will always rerender your component whenever the ready() status of the subscription changes, even if you don’t actually need or use the value.
Some other, more specialized helpers, like useCurrentUser or useMongoDoc, could be useful helpers, less sure about useSubscription.
More importantly though, some implementations in react-meteor-hooks seems flawed to me :
- its useTracker() will behave incorrectly if passed a reactiveFn that does a subscription (the subs made on first render will never be unsubscribed)
- its useSubscription() uses local variables in a way that seems broken (for which useRef or useCallback would typically be used) - did not fully check so maybe that’s FUD, but the current code doesn’t look like it would behave correctly.