React Hooks - state and lifecycle methods without a class (React 16.7.0-alpha)


#1

The bad news:

  • for those who are used to using classes for components, my hunch is that this will become the new norm moving forward

The good news:

  • no breaking changes
  • classes won’t disappear in the foreseeable future

#2

React keeps getting better!


#3

I’m not really seeing the benefit here, however ~80% of my company’s react components are classes (we use mobx) so that’s probably why.


#4

So react hooks are just a more disciplined Meteor tracker…


#5

Does this mean we can easily move from withTracker to Tracker when using hooks?


#6

I am not sure yet there might be an easier way to integrate Tracker, like a Tracker hook…


#7

I like being able to specify which variables to diff for the re-run.


#8

We can pass a comparison function to React.memo


#9

I tried to see what a useTracker hook might look like, and it seems the following code is pretty close (including a drop-in reimplementation of react-meteor-data’s withTracker HOC on top of it)

import React, { memo, useState, useEffect, useCallback } from 'react';
import { Tracker } from 'meteor/tracker';

export function useTracker(reactiveFn, dependencies) {
  const [trackerData, setTrackerData] = useState(null);
  const callback = useCallback(reactiveFn, dependencies);

  useEffect(() => {
    let computation;
    Tracker.nonreactive(() => {
      computation = Tracker.autorun(() => setTrackerData(callback()));
    });
    return () => computation.stop();
  }, [callback]);

  return trackerData;
}

export const withTracker = options => Component => {
  const expandedOptions = typeof options === 'function' ? { getMeteorData: options } : options;
  const { getMeteorData, pure = true } = expandedOptions;

  function WithTracker(props) {
    const data = useTracker(() => getMeteorData(props) || {}, [props]);
    return data ? <Component {...{ ...props, ...data }} /> : null;
  }

  return pure ? memo(WithTracker) : WithTracker;
};

Again, this would deserve actual real-world batte-testing, but it seems to be working fairly well – and the code is way simpler to grok than the current react-meteor-data :heart_eyes:

example usage :

function MyComponent({ docId }) {
  const doc = useTracker(() => Documents.findOne(docId), [docId]);
  return doc ? <div><h1>{doc.title}</h1>{doc.body}</div> : <Spinner/>
}

#10

(edited my previous post with a version of useTracker that’s slightly easier to use : useTracker can receive a closure + the array of dependencies, and take care of calling useCallback internally)


#11

Nice work! Glad a google search turned this up. Any plans to package this @yched?


#12

Well ideally this (or some version of it) would be in a future version of react-meteor-data ?
That is the current go-to package, referenced in docs, guides, blog posts, would be best to keep it that way IMO.

Also, react-meteor-data will have to quit relying on componentWillXxx() methods anyway to stay compatible with react’s forthcoming Suspense and concurrent mode. Moving to a hooks-based implementation implicitly solves that.

But sure, I’ll probably open a PR for react- meteor-data at some point :slight_smile:
Hooks are still alpha so we don’t need to rush it in (although I suspect once they come out of alpha, people are going to be eager to start fetching meteor data this way -so much nicer)


#13

Great points. I agree.

I gave it a quick test, and my first attempt failed. I tried to get the logged-in user like I would do it using withTracker:

function App() {
  const { user } = useTracker(() => ({ user: Meteor.user() }))
  //...
}

got null back, and couldn’t destructure, so I made the default state an object:

export function useTracker(reactiveFn, dependencies) {
  const [trackerData, setTrackerData] = useState({})
  //...
}

That’s a better default for my initial attempt. Then I realized I should’ve done it like your example:

const user = useTracker(() => Meteor.user())

Maybe there’s a way to accommodate both object and single value patterns, but I couldn’t figure it out quickly. Also not sure but should dependencies be defaulted to an empty array? I haven’t read up on useCallback yet.

Thanks for this!


#14

Do you think there a way to utilize hooks to improve the pub/sub approach with a react classes (sorry functions :sweat_smile:) as well?

useSession, useTracker, useSubscription, userReactiveVariable, useUser all sound like potential meteor specific hooks… just thinking loud here.


#15

@alawi :
Sure, once you have a generic “useTracker(reactiveFn)”, more specialized hooks like

  • useSubscription(name, …args),
  • useMongoDoc(collection, selector, options),
  • useReactiveVar(var)

are just a couple oneliners :slightly_smiling_face:

The question is whether react-meteor-data should be the one providing them (and if so which ones exactly) :

  • Such specialized versions could already have been provided as HOCs in the existing react-meteor-data, but the package maintainers chose to only provide the generic withTracker().
    Does the super-fluent syntax allowed by hooks change that perspective ? I’d tend to think so, but that’s probably something to discuss :slight_smile:
  • Where do we draw the line ? those different reactive sources (session, user, minimongo, reactiveVar, reactiveDict…) are provided by different packages, not all required, not all in Meteor “core”… Not sure how how we can provide functionnality on top of optional packages.

In short : I think providing a couple more specific hooks would be cool, that’s probably a discussion for after an initial useTracker hook lands - at worst, they’re super easy to implement yourself.


#16

@mattblackdev :
Yep, moving the first computation from componentWillMount() (in the current withTracker HOC) to componentDidMount() (with the useEffect() hook) means the result of the reactive function is not available in the very first render but only in an update triggered just after this inital render.
(Same challenge without hooks if we want to get rid of the soon-to-be-deprecated componentWillMount())

My implementation of useTracker() above choses null as the return value in the very first render.
That can be dealt with fairly simply for the withTracker HOC : since the reactive callback always returns at least an empty object, then it’s easy to say “if null, it’s the first render, just ignore it and render nothing”. That might be less cool for direct uses of the hook : as you point out, returning null prevents destructuring, returning anything else might be at odds with the kind of value you’d expect to receive from the callback…

Ideally the very first render would already setup the autorun and return the expected value, but I’m not sure if that is doable. Or the hook should be able to discard the first render somehow and just render null…

So yeah, I should probably open that PR sooner rather than later, so we can have those discussions there :smiley:


#17

Just wanted to say I love the above concept and want to stay apprised if you decide to package it. Pls post here if you see anyone wrap that up nicely, as I’ll probably go ahead and implement a non-Npm version for my own use in the meantime.


#18

PR created : https://github.com/meteor/react-packages/pull/262


#19

@mattblackdev : I pushed a hackish (?) workaround for the “initial value” issue in the PR. There might be a better way, not fully sure.


#20

@yched i saw this code from my twitter…
Can we use this to run Tracker.autorun instead of withTracker?