Hacker News new | ask | show | jobs
by exogen 3159 days ago
One thing I keep running into is that for a few of my app ideas, I'm not really interested in just having static queries known at build time, like Relay and Apollo are designed for. I specifically want them to be dynamic and actually based on what components get rendered.

Consider this query from the article:

    query ProfilePageData {
      user(handle: "manifoldco") {
        ...HeaderData
        ...SidebarData
        ...TweetListData
      }
    }
That's great if the ProfilePage component knows that it only contains the Header, Sidebar, and TweetList components. Since they're designed for static queries, both Relay and Apollo suffer from this shortcoming: the parent query needs to know about every component that could possibly add fragments ahead of time, and pull them all in. In my opinion, this really fails at fulfilling the promise of components – the parent shouldn't need to know what all its descendants do.

What if the rendered components are more dynamic? Putting @skip directives on every field is not really an option (and then you need a matching query $variable for every directive.)

If you think of the query as more of a template, then you could have portions of the query that are like "template blocks" that other components could extend, e.g.:

    query ProfilePageData {
      user(handle: "manifoldco") {
        ${userFragments}
      }
    }
...then descendant components could have access to `userFragments` as an extension point. I'm not yet sure if it's a terrible idea, but I started a project just the other day to experiment with it: https://github.com/exogen/apollo-dynamic-queries
2 comments

It sounds like your child components should be fetching their own data via their own graphql queries rather than consuming it as props from the parent that's rendering them.

That way the parent can just render whatever components it wants without having to worry about what data they require.

I've considered that, but it's extremely annoying (and still not really possible with Relay or Apollo). Not only would I be relying on Apollo's query batching/merging in order to not make 100 different queries over the network, but each child component would need to duplicate the base query and variables, meaning they'd need the props that correspond to query variables passed all the way down the component tree to them.

Consider the Artist.Name and Artist.Disambiguation components in my GitHub example. Notice how they don't need the `mbid` prop that the ancestor component provides. I want a component like that for every single field in my Artist schema. If every one of those components duplicated the artist query, they'd all need to be passed the `mbid`, because that's how it determines what artist to retrieve. It's possible to do some tricks with `context` like I'm doing now, but I don't really trust that it's a better solution than having one query execution point with an extendable query.

You can have child components that get some data from props and some from their own query. Just inject the props into the relay renderer.
Yes, I'm saying that getting those props to those children in the first place is annoying to do and I'd still have to build a bunch of `context` helpers on top of Relay and Apollo to make doing that nice.

This is the API I want:

    <Artist mbid="abc-123">
      <Artist.Name />
      <Artist.Disambiguation />
      <SomeOtherComponentThatHasArtistFieldDescendants />
    </Artist>
This is the API you're saying I would have to use with vanilla Relay and Apollo:

    <Artist mbid="abc-123">
      <Artist.Name mbid="abc-123" />
      <Artist.Disambiguation mbid="abc-123" />
      <SomeOtherComponentThatHasArtistFieldDescendants mbid="abc-123" />
    </Artist>
Assuming I did build HOCs for doing so, the difference boils down to this: in your approach, query variable props would magically be passed down the component tree via `context` and used by descendant components in duplicated queries. The way I've built it, query fragments are magically passed up the component tree via `context` and used in a single query.

Considering neither are possible with vanilla Relay and Apollo and I need to build these helper HOCs either way, I don't really see why what you're proposing is better. I had already considered them both and deliberately did not do it that way.

> I've considered that, but it's extremely annoying (and still not really possible with Relay or Apollo)

It’s easy with Relay. With Classic you render a RelayRenderer. In Relay Modern you render a QueryRenderer. Either way, you can issue more queries than just your root query and it’s as easy as rendering a React component.

It’s not. I’ve tried with Relay Modern. See the API I want elsewhere in the thread.
Yes I think I see what you mean now.
Consider using conditional fields using the @include or @skip directives
I addressed that in the original post. That requires (1) adding potentially hundreds of boolean query variables to the query (and neither Apollo nor Relay give you any affordance for populating these from context, which you'd need for descendant components to add them) and (2) the parent query still knowing and listing out every possible field that could be added to the query, but with a @skip directive on it and (3) how do you deal with fields on nested objects? Not feasible in the slightest.
This is a classic static/dynamic problem. Luckily, in the case of React, there may be a really good solution that covers most necessary use cases. First, an analogy:

Consider Ruby's method_missing and corresponding respond_to? methods [1]. method_missing may respond to a method dynamically, but respond_to? should return true if a method will be responded to. If you constrain yourself to normal methods, or carefully defined method_missing bodies, you can predict with 100% accuracy, but what if you do something like this?

    class Whoops
      def method_missing(method)
        if method == :fancy and Random.rand > 0.5
          "it's your lucky day!"
        else
          super(method)
        end
      end

      def respond_to?(method, include_private = false)
        if method == :fancy
          true # Well, actually, "maybe?".
        else
          super
        end
      end
    end
Temporarily ignoring the problem created by randomness: Implementing respond_to? methods is annoyingly redundant. In order to fully-automate the process of implementing respond_to?, you'd need to analyze a method_missing implementation. In the common case, this is static type inference. In the limit case, this is arbitrary abstract interpretation.

However, effectful code, such as this random example, is provably impossible to analyze correctly! You need to have some conservative approximation. Returning true from respond_to? for the :fancy method means that respond_to? should actually be called might_respond_to?.

OK, now back to React and GraphQL: Luckily, over-fetching data is usually not that big a deal (as long as you're within bandwidth constraints). It's often desirable to! Since you might need that data soon when the view changes. Knowning this, it's safe to assume a method for assembling a GraphQL query from fragments can be conservative. That's essentially what a totally static, manual composition of query fragments does. If that's a tight enough approximation for you (like some other commenters have implied) then great! But what if it's not?

Luckily, virtual DOMs are dramatically simpler and less powerful than arbitrary method_missing implementations. If your render method is already pure, you can call it an extra time to use it as its own abstract interpretation. It will produce a virtual DOM, which you can then analyze yourself. For example:

    let capture = (f) => {
      let oldCreateElement = React.createElement;
      try {
        React.createElement = (component, props, ...children) => {
          return {component, ...props, children};
        };
        return f();
      } finally {
        React.createElement = oldCreateElement;
      }
    };

    class DynamicQuery extends React.Component {
      graphql() {
        let dom = capture(() => this.render());
        return gatherQuery(dom);
      }
      render() {
        ...
      }
    }
Here, the gatherQuery method will walk the objects returned by our monkey-patched createElement method, composing query fragments along the way. By following the component references, you can find static methods (easy!), or even instantiate components to do recursive renders yourself (this gets hairy, since React doesn't really make any of this public API). You can do something pretty simple with this: Just hard code a few component types to make some limited dynamic decisions, fallback to static over-query beyond that. Or you can get really fancy and do recursive rendering and analysis, if you really want to.

[1] http://blog.enriquez.me/2010/2/21/dont-forget-about-respond-...

That would be a similar approach to the `getDataFromTree` function that Apollo offers: http://dev.apollodata.com/react/server-side-rendering.html#g...

But it's way more work than just passing fragments up the tree via `context` in `componentWillMount` or `componentDidMount`, which is relatively simple by comparison. :)