Principals of writing Meteor + React (with hooks) App

Hi @florianbienefelt,

Would you have any github repo to show on how to use react-query with Meteor, or any specific recommendation ? I am really interested in this combo for a new project I am starting.

Thanks in advance.

I don’t have an example repo, but here’s a very basic example :

import { Meteor } from 'meteor/meteor';
import { queryCache, useIsFetching, useMutation, useQuery } from 'react-query';

// Write 2 simple wrappers around useQuery and useMutation
const defaultConfig = {
  retry: 2,
};

// args is an array with the meteor method name as first value, and
// method parameters as the next values
const useMeteorData = (args, config) =>
  useQuery(
    args,
    (query, ...params) =>
      new Promise((resolve, reject) => {
        Meteor.call(query, ...params, (err, result) => {
          if (err) {
            reject(err);
          }

          resolve(result);
        });
      }),
    { ...defaultConfig, ...config },
  );

const useMeteorMethod = (method, options) =>
  useMutation(
    (...params) =>
      new Promise((resolve, reject) => {
        Meteor.call(method, ...params, (err, result) => {
          if (err) {
            reject(err);
          }

          resolve(result);
        });
      }),
    options,
  );

// Declare your queries on the server, add more validation and security checks
Meteor.methods({
  todos({ status, visibility, type = 'many', fields, sort, skip, limit }) {
    check(status, String);
    check(visibility, String);
    check(type, String);
    // Automate checking of all mongo arguments here to avoid code duplication in your different collection queries

    const query = [
      { status, visibility },
      { fields, sort, skip, limit },
    ];

    if (type === 'one') {
      return Todos.findOne(...query);
    }

    return Todos.find(...query).fetch();
  },
  users() {
    // almost the same stuff
  },
});

// And finally, use it somewhere in your react components
const Todos = ({ status, userId }) => {
  const isFetching = useIsFetching();
  const { data: user, isLoading: isUserLoading } = useMeteorData([
    'users',
    { userId, type: 'one', fields: { role: 1, firstName: 1 } },
  ]);
  const { data: todos = [], isLoading: isTodosLoading } = useMeteorData(
    [
      'todos',
      {
        status,
        visibility: user?.role,
        fields: { title: 1, status: 1, description: 1 },
      },
    ],
    { enabled: !!user },
  );
  const [toggleTodo] = useMeteorMethod('toggleTodo', {
    onSuccess: () => {
      queryCache.invalidateQueries('todos');
    },
  });

  if (isUserLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{user.firstName}'s todos</h1>

      {todos.map(({ _id, title, description }) => (
        <div key={_id}>
          <b>{title}</b>
          {description}
          <button onClick={() => toggleTodo({ _id })}>Toggle completed</button>
        </div>
      ))}

      {isFetching ? <div>A query is fetching in the background...</div> : null}
    </div>
  );
};

There are lots of things you can improve, and you should probably read the docs to get to know all the subtleties :slight_smile:

usePaginatedQuery is also very useful, and you’ll have to write a wrapper for that one for it to play nicely with your methods and MongoDB’s limit and skip.

EDIT: usePaginatedQuery is disappearing in the next version of react-query, so don’t invest too much in it.

4 Likes

Thanks a lot, appreciated and great example. I still have to grab all the benefits of react-query compare to the “plain” minimongo but seems like a great fit indeed!

Well the most important one, is that you can completely stop using publish/subscribe, which is a bottleneck in most Meteor applications, while still maintaining a reactive feel to your app, since everything refetches from the server automatically, which minimongo does not do at all if you don’t use subscriptions.

Querying data through methods is also much faster than setting up subscriptions, so your app will feel snappier :slight_smile:

In the end, everything comes down to your use-case, if you’re building a live game or highly interactive app, you’re still going to need pub/sub, but you can easily mix the 2 together!

EDIT: The package has a lot more advantages compared to minimongo:

  • Caching and refetching logic, based on time, window focus, and more
  • Dependent queries (i.e. methods)
  • Easy pre-fetching of data before displaying another react component
  • Managed pagination and infinite queries without writing your own system
  • And more :slight_smile:
4 Likes

I’m also trying to figure how to reduce number of data requests during multiply components re-renders.

Bellow is what I use to store profile information from Socialize packages. This version uses suspense version of useSubscribe. For Meteor.user() I use useUser from meteor/react-meteor-accounts which have been created in the meantime since the creation of this post.

import { Meteor } from 'meteor/meteor'
import { useEffect, useState } from 'react'
import { useSubscribe } from 'meteor/react-meteor-data/suspense'
import { ProfilesCollection } from 'meteor/socialize:user-profile'
import type { Profile } from '../../users/definitions'
import { Tracker } from 'meteor/tracker'

export function useProfile(subscription?: string): {
  profile: Profile | null
} {
  useSubscribe(subscription || 'profile')
  const [profile, setProfile] = useState()
  useEffect(() => {
    const computation = Tracker.autorun(async () => {
      const profileData = await ProfilesCollection.findOneAsync(Meteor.userId())
      setProfile(profileData)
    })
    return () => {
      computation.stop()
    }
  }, [])
  return { profile: profile || null }
}

Thanks for the tip Florian.
.