Principals of writing Meteor + React (with hooks) App

or easy-peasy, which works on top of redux: https://github.com/ctrlplusb/easy-peasy

but i advice strongly against mirroring stuff like Meteor.user, that already lives in a minimongo collection into redux. use redux only for local state

1 Like

Redux was something I considered from the beginning, but with latest React additions and growing use-cases of using Hooks made me decide to stick with pure React. There are many articles which backing the idea of using pure React with Hooks for small project as oppose to React+Redux

Thanks for the links I’ll have a look anyway.

One way another, with Redux or not, should I really store Meteor.userId, Meteor.status in the Store/state, or use it directly anywhere I need, what are the pros/cons of each approach?

But there are always extended user properties, like Name, Email, etc which I cannot use directly with Meteor. method and have to use subscription, these I assume, make sense to store in Redux or React store, right? Thanks.

I have to really disagree with this:

Where there’s a React, there’s a Redux.

And it’s not only me, the founder of Redux and the lead of React also says that a lot of the time you probably don’t need Redux: https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367

This is especially true in the context of Meteor, which provide a lot of the client side global helpers that diminish the need for a state container.

IMHO, redux is extremely bloated as well.

3 Likes

Really good topic to bring up @dens14345 thanks! I’m also curious how others resolve these…

I’ve previously used Tracker for all pages but then I realise most pages don’t need reactivity so I started turning them into a functional components that by useEffect, get page data on component mount and that’s it. I have made this a pattern and only use the reactivity with Tracker when/if needed. I think this saves a lot of CPU as well as it makes the code cleaner and simpler.

Then for the userInfo, I use Context API that subscribes to the Provider rather than always calling Meteor.user() (which is also just fine I believe)…

Thanks,

I agree about redux, it may become useful as project grows, but at the moment React offers enough functionality.

So far I created a central store (with reducer help), and trying to figure what is to store there and what is not, it makes sense, to me, to store reusable data, like Meteor.user(), etc.

I’m also trying to figure how to reduce number of data requests during multiply components re-renders.
A lot of data I use is received with Methods, these I don’t want to re-request, and thinking of storing them in store. Its really messy now in my head and I’m trying to organise and make sense of it.

I’m also closely following this topic: A crazy idea for Meteor React hooks - and Suspense which is also very important for me understand.

With more and more people using Meteor + React stack I’m expecting a lot more questions like that in the future.

I tend to use @macrozone’s suggestion, and keep my reusable hooks (which is really all of them) in /imports/api/connectors/. For expensive queries and computations, you can create a Provider, and have your “connector” hooks use the context API to pull from a single Provider. Your Provider would use useTracker and your consumable hook would just access the context API. One of the real nice things about defining reusable hooks this way is that you are making very clear contracts for the consumer of the hook, and they really don’t have to worry about the implementation details.

The linked thread is talking about using a Provider for a different purpose, which should cut down on additional computation runs in concurrency mode, but probably won’t cut down on rerendering much.

I agree with what others have said here - Meteor.user() etc. are already global state storage/managed, so there’s no need to put that in something like redux. For a while it was trendy to stuff nearly everything in redux, but it turns out to be a nightmare in practice. YAGNI. But it’s still a great tool for certain types of app state.

I will say though that useTracker is missing a feature that could help cut down on the re-renders. One idea we had when we implemented useTracker was to create a way to specify a shouldUpdate method, so that you could intentionally cut down on rerenders. This becomes useful in cases where you are querying for like 2 properties and only need to update your views when one of those two properties change. Right now useTracker will update the view whenever ANY property on the document changes, even if you aren’t selecting those (that’s just how Mongo/MiniMongo works).

I’ll probably try to get that in with the Suspense work.

2 Likes

Thanks @captainn for your answer.

To summarise:

  1. Use Meteor.user() as is, without storing it in a global state.

  2. Any other user properties (like First Name, Last Name, avatar, etc) not available with Meteor.user() store in Global State, by means outlined in #3

  3. Implement reusable hooks with Provider and Context API for expensive computations

I’m still struggling with 2 things, let’s assume I have 2 components with react router:

/comp1 and /comp2

My understating, when the app is loaded, all the components loaded also: all subscriptions initialized, methods called, so we have all the data in mini-mongo

Immediately after loading, the app is at /comp1. When I click on the link to route to /comp2, it does following:

  • initialising all the subscriptions again

  • calling all the methods again

  • rendering and re-rendering component as many times as it sees fit

So it’s kind of a loop: UI state changed (user click), this triggers component re-render (fair enough), component re-render triggers useTracker subscriptions and methods called. I call methods in useEffect hook, and have correctly assigned dependencies.

I assume there are a lot of unnecessary re-renders, computations in my code. Coming from the PHP world I’m still thinking in the ‘server side’ way and struggling to understand how the things work behind the scene, which is important to build an efficient, fast and reliable app.

Everything you wrote looks fine. Rerenders are designed to be cheap, and even disposable in React (in fact, concurrent rendering mode depends on the disposability of “stateless” functional components). You should only worry about it if it slows everything down.

One thing you might need to wrap your head around with useTracker, is that it will run and rerun for it’s entire lifecycle - which is probably a newer concept coming from PHP. It does things when the component is mounted, like starting a subscription, which will update the computation whenever anything it subscribed to changes, including loading and individual document change events, and that will cause rerender. That’s probably what you want.

A react rerender will not rerun everything in the computation from scratch every time (it might rerun the Mongo query though, if you don’t use deps), so you don’t have to worry about it restarting the subscription on every re-render. Subscriptions are only started on mount - at the beginning of the lifecycle. When the component is unmounted (you navigate to another route, with different components, etc.), then the computation is destroyed, and subscriptions stopped, etc.

One thing that wasn’t clear in your writeup though - you should NOT call methods from useTracker. That would cause the method to be invoked every time the computation is run, which can happen for a whole bunch of reasons. useEffect with deps is the right place to call a computation. One interesting pattern you can use, is you can use useEffect to call a method, then update a local memory only collection (or use ground:db), and in the same component, if you have useTracker set up to query that collection, it’ll update the react component like magic! (Or you could just use local react state, the meteor way would get you a global data store that can be used in multiple component instances.)

1 Like

Hi,

I’m not calling Method from within useTracker, I have implemented something similar to https://github.com/andruschka/react-meteor-hooks/blob/master/hooks/useMethod.js
and executing useMethod from component root, something like:

const users = useMethod('users.findByUserId');

then inside useEffect:

  useEffect(() => {
    if (users.data) {
      setUsersData(users.data);
    }
    return () => {
    };
  }, [users.data])

I’m using state to hold users data, which I’m not sure right, as it is already in: users.data

And as per my previous post, I created a React store similar way explained in https://blog.logrocket.com/use-hooks-and-context-not-react-and-redux/

I’m storing, so far, extra user data, which is required in most of the components down the tree, but I’m not storing data from Methods to global store as it makes no sense, they only required in a particular component.

This is so far best I can come up with, by using many articles, GitHub examples, etc. There is no single pleace which explains how to build Meteor + React Hooks app according to best practices, that is why I started this topic in hope to find or make one.

1 Like

Another fantastic library I’d consider is react-query: https://github.com/tannerlinsley/react-query. It solves server data querying, by properly separating server-data (your DB) from frontend-data (routing, temporary component state, etc.), which should never have been mixed together (redux introduced that, sadly).

Here’s the video that sold me on it, well worth the watch: https://www.youtube.com/watch?v=seU46c6Jz7E

We’ve essentially been building the same library in-house until we found that this library solved the same problems in a better way.

You can almost completely stop using meteor subscriptions, and create all of your static meteor method queries that refetch after certain meteor method calls.

And for the few places where you need true reactive data, create a few hooks that use useTracker as suggested above.

5 Likes

I use it directly anywhere I need, but enclosing them in useTracker for reactivity; after reading tons of articles, I find it best suits our needs.

Using React hooks in Meteor is still work in progress I must say and it improved every week with new Meteor-hooks released every now and then. I bet you, we will see very soon: useMethod, useSubscription, useUser, useUserId, etc hooks, it just makes sense. With React Suspense already available and Concurrent mode on horizon Meteor should adapt.

Thanks for the tip Florian, this is looking pretty good indeed.

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.
.