Principals of writing Meteor + React (with hooks) App

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