Arthur Andersen

Meteor and React Hooks

til

Since react hooks have been introduced I tried to rephrase my components with `react-hooks` and I came to the conclusion that they help me to write less code that is much more readable.

Currently I am working again with Meteor and to my joy Meteor 1.8 was just released, providing react templates. As of now it is suggested to use the `withTracker` HOC from meteor/react-meteor-data to subscribe to and fetch data.

Although that can be a quite clean solution I wanted to give `react-hooks` a try.

Via `meteor create –react <<projectname>>` this component is generated:

import React, { Component } from 'react';

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

import Links from '../api/links';

class Info extends Component {
  render() {
    const links = this.props.links.map(
      link => this.makeLink(link)
    );

    return (
      <div>
        <h2>Learn Meteor!</h2>
        <ul>{ links }</ul>
      </div>
    );
  }

  makeLink(link) {
    return (
      <li key={link._id}>
        <a href={link.url} target="_blank">{link.title}</a>
      </li>
    );
  }
}

export default InfoContainer = withTracker(() => {
  return {
    links: Links.find().fetch(),
  };
})(Info);

My proposal for an API that uses `react-hooks` looks like that:

import { useMeteorSubscription, useMeteorData } from 'meteor/react-meteor-hooks';

import Links from '../api/links';

export const Info = function (props) {
  const loading = useMeteorSubscription('links');
  const links = useMeteorData(() => Links.find().fetch());

  if(loading) return (<div>Loading links ...</div>);

  return (
    <ul>
      {links.map((link) => (
        <li key={link._id}>
          <a href={link.url} target="_blank">{link.title}</a>
        </li>
      ))}
    </ul>
  );
}

I created a pull request and a sample project.

useMeteorSubscription

import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { useEffect, useState } from 'react';

let useMeteorSubscription;

if (Meteor.isServer) {
  // When rendering on the server, we don't want to use the Tracker.
  // The subscription is always ready on the server.
  useMeteorSubscription = () => true;
} else {
  useMeteorSubscription = (publication, ...parameters) => {
    const [loading, setLoading] = useState(true);
    let handle, computation;

    const cleanUp = () => {
      handle && handle.stop();
      handle = null;
      computation && computation.stop();
      computation = null;
    }

    useEffect(() => {
      if(computation) cleanUp();

      Tracker.autorun((currentComputation) => {
        computation = currentComputation;

        handle = Meteor.subscribe(publication, ...parameters);
        setLoading(!handle.ready());
      });

      return cleanUp;
    }, [publication, ...parameters]);

    return loading;
  }
}

export default useMeteorSubscription;

useMeteorData

import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { useEffect, useState } from 'react';

let useMeteorData;

if (Meteor.isServer) {
  // When rendering on the server, we don't want to use the Tracker.
  // We only do the first rendering on the server so we can get the data right away
  useMeteorData = getMeteorData => getMeteorData();
} else {
  useMeteorData = (getMeteorData, inputs = []) => {
    const [meteorData, setMeteorData] = useState(getMeteorData());
    let computation;

    const cleanUp = () => {
      computation.stop();
      computation = null;
    }

    useEffect(() => {
      if(computation) cleanUp();

      Tracker.autorun((currentComputation) => {
        computation = currentComputation;
        setMeteorData(getMeteorData());
      });

      return cleanUp;
    }, inputs);

    return meteorData;
  }
}
export default useMeteorData;