Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useFind and useSubscribe #332

Merged
merged 17 commits into from
Dec 3, 2021
Merged

useFind and useSubscribe #332

merged 17 commits into from
Dec 3, 2021

Conversation

CaptainN
Copy link
Contributor

@CaptainN CaptainN commented May 12, 2021

This PR adds useFind and useSubscribe as discussed and prototyped in the new-hooks branch (PR #298).

The hooks are basically done. They just need docs and tests.

TODO:

  • Write tests
  • Add documentation (readme)

@CaptainN CaptainN mentioned this pull request May 12, 2021
@CaptainN
Copy link
Contributor Author

I added some initial tests for useFind and update the README!

The test covers the basic useFind usage. It isn't super exhaustive, but it probably covers most of what people will be doing with it.

I didn't add any tests to cover useSubscribe but that's such a simple wrapper over useTracker anyway. There is a small rerender optimization having to do with the .ready()/isLoading() method it returns that should probably be tested.

The only thing I think maybe I missed is that the useFind hook requires a cursor to be returned - I think there was a case where maybe we wanted the cursor to optional, in case we wanted to check for loading state within the cursor factory - but I don't know how necessary that is. It might even be beneficial to skip that check altogether, and just declaratively set up the query, and let React do the "isLoading" work. I'm not sure we need to add that functionality.

Anyway, this is probably ready for either an alpha or beta release, IMHO.

@CaptainN CaptainN added ready We've decided how to solve or implement it and removed help wanted labels Aug 10, 2021
@yched
Copy link
Contributor

yched commented Aug 10, 2021

the "optional cursor" bit was because you cannot conditionally call a hook (while you can conditionally do a cursor.find() in a useTracker() callback). It's not necessarily tied to the isLoading state of a previous subscription, but could be just any condition (does some prop have a value ?)

I'd think we do need this ability to conditionally return a cursor

@CaptainN
Copy link
Contributor Author

I'll add support for null return values - but it might be an anti-pattern to do conditional queries. I'll expand on that another time.

@filipenevola
Copy link
Collaborator

filipenevola commented Aug 11, 2021

Hey @CaptainN maybe this is out of the scope of this PR but I believe it's worth to mention.

I'm working in a branch providing a way to use mongo Meteor package without depending on APIs that are relying on Fibers.

What does that mean?

For example, if you need to call LinksCollection.find() and we know that in the future this find is going to depend on async code we are already creating a LinksAsyncCollection.findAsync() so you need to use await already and you don't need to change your code later when we remove Fibers from the implementation. You can read more about this work in this discussion here.

This is probably going to be optional in the client as we don't have Fibers in the client as well but if you want to keep your code isomorphic you should use the new async methods in the client as well.

And why I'm bringing this here?

Including an async call inside useTracker is going to add some challenges and maybe useFind could already solve this supporting async functions also as callbacks or even only supporting async functions as callbacks.

I didn't review the code of this PR yet to say how hard that would be or if this is possible keeping the same ideas but I decided to share this here as I'm working in the mongo package and I can see this impact coming.

@CaptainN
Copy link
Contributor Author

I agree it'll be challenging to support async/await in useTracker.

Here is should be relatively easy to set it up as we need it. In useFind we really only use the normal fetch method once, to grab initial data. It creates a cursor and does the initial (non-reactive) data fetch in useMemo, and after that, it just uses the observe API on the cursor for updates.

@CaptainN
Copy link
Contributor Author

I implemented and tested null return value for useFind - this should be good to go.

@StorytellerCZ
Copy link
Collaborator

@CaptainN ready to move this to the next stage?

@CaptainN
Copy link
Contributor Author

Yep, ready for testing! Maybe a beta release?

@StorytellerCZ
Copy link
Collaborator

Let's resolve the conflicts and then we can do a beta release.

packages/react-mongo/.gitignore Outdated Show resolved Hide resolved
packages/react-meteor-data/README.md Show resolved Hide resolved
@CaptainN CaptainN marked this pull request as ready for review September 30, 2021 17:31
Copy link
Collaborator

@filipenevola filipenevola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @CaptainN 😄

I just suggested a new example in the docs.

packages/react-meteor-data/README.md Outdated Show resolved Hide resolved
Copy link
Collaborator

@StorytellerCZ StorytellerCZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just need one slight addition in the documentation and should be ready to go. I'm testing this and so far no problems.

packages/react-meteor-data/README.md Outdated Show resolved Hide resolved
@filipenevola
Copy link
Collaborator

filipenevola commented Oct 19, 2021

Hi @CaptainN sorry for the late feedback but shouldn't we have a way to skip a subscribe and even a find?

At least I have one useFind that I implemented myself and in some cases I need to skip the subscription because I don't have all the params and as we can't call hooks conditionally it's necessary to provide a way to skip inside the hook.

My example:

const { data: modules } = useFind({
    publicationName: 'modules',
    arg: { modeName },
    skip: !modeName,
    find: () => ModulesCollection.find({}, { sort: { order: 1 } }),
  });

My implementation (it's not a package, just the code in one of my apps):

export const useFind = ({
  publicationName,
  arg,
  find,
  skip,
  shouldSkip,
  dataReturnWhenLoading,
}) =>
  useTracker(() => {
    if (skip || (shouldSkip && shouldSkip())) {
      return {
        data: [],
        skipped: true,
      };
    }
    const handler = Meteor.subscribe(publicationName, arg);

    if (!handler.ready()) {
      return {
        loading: true,
        data: dataReturnWhenLoading || undefined,
      };
    }

    const result = find();

    // is findOne? so no fetch
    return { data: result.fetch ? result.fetch() : result };
  });

How could I do the same using the hooks from this package? How could I skip the subscription call when modeName is not available?

@CaptainN
Copy link
Contributor Author

You can't conditionally call a subscription - but useSubscribe is mostly a simple wrapper around useTracker, and there's no reason you can't just use useTracker for advanced cases like this.

@filipenevola
Copy link
Collaborator

You can't conditionally call a subscription - but useSubscribe is mostly a simple wrapper around useTracker, and there's no reason you can't just use useTracker for advanced cases like this.

My point is not to block this PR, it is just to understand how common this could be.

I would say this is pretty common. But if you disagree, ok :)

@CaptainN
Copy link
Contributor Author

I think it might be less common than it seems if you are organizing your code with smaller purpose built components, the way the two hooks - useSubscribe and useFind - would imply. A recommendation might be to not even include your useSubscribe and useFind hooks in the same component. These are of course patterns that we need to set and communicate.

But like I said, it's easy enough to fall back to useTracker in this case anyway.

We could refactor to allow the use of a factory, instead of or in addition to the non-factory hook signature we use now, if it's too confusing the way it is.

@filipenevola
Copy link
Collaborator

No, it is not confusing.

I was asking because it was a need that I had just a few days ago and I would like to confirm how common do we think this is.

thank you!

btw, @StorytellerCZ is probably going to publish a new version of this package soon including new hooks.

@CaptainN
Copy link
Contributor Author

I really don't know how common it would be, but I do wonder whether it's part of Meteor's philosophy to reduce friction when coding, and if so, maybe we should add the factory option to useSubscribe? I don't think it'd be that difficult.

@CaptainN
Copy link
Contributor Author

So actually, useSubscribe already supports conditional subscriptions - if the subscription name argument is falsy it will simply fail silently (and isReady() will return false). I can add documentation for this.

@StorytellerCZ
Copy link
Collaborator

I have bumped the versions for next beta release @filipenevola

@filipenevola
Copy link
Collaborator

Published [email protected].

@filipenevola
Copy link
Collaborator

I've refactored my internal useFind:

import { Meteor } from 'meteor/meteor';
import { getLanguage } from './languages';
import { useTracker } from 'meteor/react-meteor-data';

const subscribeCall = (publicationName, arg) => {
  const argWithLanguage = { ...(arg || {}), language: getLanguage() };
  return Meteor.subscribe(publicationName, argWithLanguage);
};

export const useFind = ({
  publicationName,
  arg,
  find,
  skip,
  shouldSkip,
  dataReturnWhenLoading,
}) =>
  useTracker(() => {
    if (skip || (shouldSkip && shouldSkip())) {
      return {
        data: [],
        skipped: true,
      };
    }

    if (publicationName) {
      const handler = subscribeCall(publicationName, arg);

      if (!handler.ready()) {
        return {
          loading: true,
          data: dataReturnWhenLoading || undefined,
        };
      }
    }

    const result = find();

    // is findOne? so no fetch
    return { data: result.fetch ? result.fetch() : result };
  });

to use the new hooks and they are working perfectly, now I'm calling my hook useData:

import { getLanguage } from './languages';
import { useFind, useSubscribe } from 'meteor/react-meteor-data';

export const useData = ({
  publicationName,
  arg,
  find,
  skip,
  shouldSkip,
  dataReturnWhenLoading,
  deps = [],
}) => {
  const argWithLanguage = { ...(arg || {}), language: getLanguage() };
  const isSkip = skip || (shouldSkip && shouldSkip());
  const isLoading = useSubscribe(
    isSkip ? null : publicationName,
    argWithLanguage
  );
  const result = useFind(find, deps);

  if (isLoading()) {
    return {
      loading: true,
      data: dataReturnWhenLoading || undefined,
    };
  }

  // is findOne? so no fetch
  return { data: result.fetch ? result.fetch() : result };
};

You can see it in action here.

@CaptainN
Copy link
Contributor Author

CaptainN commented Dec 2, 2021

That's a great example of functional composition. Love it.

I'm not sure findOne will work though - useFind doesn't support findOne. You never have to call fetch on the result of useFind. It always returns an array of items, even if there is only one. You'd probably want to detect whether it's findOne or find, then use useFind to get an array, and return only the first item if it's findOne, or the entire array if it's find. (Internally, findOne basically works this same way - it just wraps find and grabs the first element.)

@filipenevola
Copy link
Collaborator

I'm not sure findOne will work though - useFind doesn't support findOne. You never have to call fetch on the result of useFind. It always returns an array of items, even if there is only one. You'd probably want to detect whether it's findOne or find, then use useFind to get an array, and return only the first item if it's findOne, or the entire array if it's find. (Internally, findOne basically works this same way - it just wraps find and grabs the first element.)

It's working but I believe I just have one use case for findOne inside a React component, I will double-check it later.

Copy link
Collaborator

@filipenevola filipenevola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great.

@filipenevola filipenevola merged commit 07f607c into master Dec 3, 2021
@filipenevola
Copy link
Collaborator

v2.4.0 is published now.

@CaptainN do you want to start the blog post for this version?

@filipenevola
Copy link
Collaborator

BTW, as always, thank you for the great work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready We've decided how to solve or implement it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants