apollographql / react-apollo

:recycle: React integration for Apollo Client
https://www.apollographql.com/docs/react/
MIT License
6.85k stars 789 forks source link

React Hooks support #2539

Closed vovacodes closed 5 years ago

vovacodes commented 5 years ago

I'd like to start a discussion about the possible next-gen API based on react hooks for react-apollo. I think this API brings a perfect opportunity for react-apollo to improve already excellent DX even more.

// isn't this beatiful?
function Avatar() {
  const { loading, error, data } = useQuery(`
    {
       me {
          initial
       }
    }
  `);

  if (loading) return <div>Loading...<div>;

  if (error) return <div>Error happened</div>

  return (
    <div>{data.me.initial}<div>
  );
} 

What do you guys think?

maggo commented 5 years ago

@seeden you can always have react-apollo and react-apollo-hooks run side by side and use whatever you need from both :)

pleunv commented 5 years ago

I replaced react-apollo with react-apollo-hooks and everything works great in my project except subscriptions.

I definitely appreciate the effort put into it and I'm convinced it's necessary, but I'd rather wait on official design specs for the hooks API before depending on a fork in production. I've spent a bit too much time refactoring data containers the past year 😄

danielkcz commented 5 years ago

@seeden you can always have react-apollo and react-apollo-hooks run side by side and use whatever you need from both :)

Pretty sure that's not a good idea.

Why not? I am doing it like that in two production apps. Nothing wrong about it. On the contrary, it's great for gradual adoption without big rewrites.

maggo commented 5 years ago

From the readme of react-apollo-hooks:

Both packages can be used together, if you want to try out using hooks and retain Query, Mutation, Subscription, etc. HOCs from react-apollo without having to rewrite existing components throughout your app.

CarloPalinckx commented 5 years ago

@breytex the HOC api wasn't killed 🙂 The Renderprops are just favored in the docs, if I'm not mistaken.

MrLoh commented 5 years ago

Maybe let’s wait for updates from the Apollo team, if there is nothing new to say. We’re all eager to use hooks and I’m sure it’s a priority for the Apollo team. Let’s not turn this issue into a Chatroom. @hwillson could you maybe lock this thread for now.

trojanowski commented 5 years ago

@Urigo thank you for your kind words. I'd also like to thank @umidbekkarimov and other react-apollo-hooks contributors.

I'd be glad to help with the implementation of official hooks in react-apollo.

@seeden you can always have react-apollo and react-apollo-hooks run side by side and use whatever you need from both :)

Pretty sure that's not a good idea.

Why not? I am doing it like that in two production apps. Nothing wrong about it. On the contrary, it's great for gradual adoption without big rewrites.

@FredyC is right. It's possible to use react-apollo and react-apollo-hooks together (with the exception of server-side rendering, but I'm going to fix it).

mbrowne commented 5 years ago

I too am looking forward to using hooks with react-apollo (especially for mutations), but I'm not sure if they solve all use cases...there may still be a use case for render props. For our app using react-apollo, I wrote a component called QueryLoader that uses react-apollo's Query component to run the query and then checks for loading and error states so we don't have to repeat that boilerplate code in each of our components (of course, if there's some need for different loading or error handling behavior in a particular component, we still have the option of using the Query component directly). So the default behavior (thanks to QueryLoader) is to render <LoadingOverlay> for the loading state and <ErrorMessage> in the case of an error.

The problem I ran into when I tried to refactor this into a hook is that hooks seem best-suited for reusing logic, not rendering things. So I'm not sure if something like a useQueryLoader() hook would make sense (then again, I only did a brief investigation so maybe I missed something). I found a reddit thread talking about using hooks to return components: https://www.reddit.com/r/reactjs/comments/9yq1l8/how_do_you_feel_about_a_hook_returning_components/

Dan Abramov chimed in to say:

You can return elements directly. But don't return components.

...but for a case like our query-loader behavior, returning elements directly gets weird...consider this attempt:

export function useQueryLoader(query, options) {
    let result = {}
    let loadingPromise
    try {
        result = useQuery(query, options)
    } catch (promise) {
        loadingPromise = promise
    }
    return {
        ...result,
        renderContent: callback => {
            const children = callback(result)
            return (
                <QueryLoader {...result} loadingPromise={loadingPromise}>
                    {children}
                </QueryLoader>
            )
        }
    }
}

export const QueryLoader = (props) => {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <QueryLoaderInner {...props} />
        </Suspense>
    )
}

const QueryLoaderInner = ({ loadingPromise, error, children }) => {
    if (loadingPromise) {
        // use Suspense to wait until the data is available
        throw loadingPromise
    }
    return error ? <ErrorMessage errorObject={error} /> : children()
}

I realized I had just invented a less elegant version of render props, as is clear when you see the usage:

const MyComponent = () => {
    const { data, renderContent } = useQueryLoader(QUERY, { variables })
    // you'd better not try to access `data` here, because `MyComponent` renders twice—
    // during the loading state and again after data has loaded!
    return renderContent(() => (
        <div>
            <h2>{data.myData.title}</h2>
        </div>
    ))
}

Maybe having to write the <Suspense fallback={...} snippet and also if (error) in every component that consumes data is a small enough amount of code that most people don't care about writing it every time even if the behavior is the same in 90% of the cases. But personally I think it would be nice to reuse that code somehow and I'm not sure that hooks provide a good way to do it... Of course, an app-specific query-loading component (with a render prop API) could still use a useQuery() hook internally, and maybe that would be the way to go.

juank11memphis commented 5 years ago

Hi @mbrowne

I had a similar setup before hooks: a component that encapsulated showing a loading spinner and errors when doing graph queries.

This is what I did when I refactored my code to hooks (using react-apollo-hooks):

  const { data, error, loading } = useQuery(movieQueries.GET_PLAYING_NOW, {
    suspend: false,
  })
  return (
    <QueryResponseWrapper error={error} loading={loading}>
      <div>other components here...</div>
    </QueryResponseWrapper>
  )

And now that QueryResponseWrapper component is the one that handles the loading bar and errors.

mbrowne commented 5 years ago

@juank11memphis Thanks for the suggestion. One question: how do you access nested properties of data? It seems to me that this would cause undefined property errors unless you only need access to top-level properties of data, which is why I figured I'd need a function that returns the child components to render rather than rendering children (the "other components here" div in your example) directly.

eddiemoore commented 5 years ago

Although @trojanowski has done some amazing work with the react-apollo-hooks. It would be really good to have the useMutation hooks to return the data, loading, and error.

Perhaps something like:

// Query 
const { data, loading, error } = useQuery(GET_TODOS)
// with options
const { data, loading, error } = useQuery(GET_TODOS, { ...options })

// Mutation
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO)
// with options
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, { ...options })
danielkcz commented 5 years ago

@eddiemoore The useMutation is a tiny wrapper around client.mutate meaning it returns a Promise. You can handle loading state by waiting for a promise (it's easy to make a custom hook for that) and you can catch errors just as easily. I wouldn't complicate API more than necessary.

juank11memphis commented 5 years ago

Hi @mbrowne

This is a real life example of what I did:

This is how I use the QueryResponseWrapper component:

const PlayingNowMovies = ({ classes }) => {
  const { data, error, loading } = useQuery(movieQueries.GET_PLAYING_NOW, {
    suspend: false,
  })
  return (
    <QueryResponseWrapper error={error} loading={loading}>
      <div className={classes.blackBox}>
        <Text as="h3" align="center" color="white">
          Playing Now
        </Text>
        <Divider className={classes.centeredDivider} />
        <div className={classes.carouselContainer} {...rest}>
          <MoviesCarousel movies={data.movies || []} />
        </div>
      </div>
    </QueryResponseWrapper>
  )
}

And this is the actual QueryResponseWrapper component:

import React from 'react'
import PropTypes from 'prop-types'

import LoadingSpinner from '../loading-spinner/LoadingSpinner'
import ErrorMessage from '../messages/ErrorMessage'

const QueryResponseWrapper = ({ children, loading, error }) => {
  if (loading) {
    return <LoadingSpinner />
  }
  if (error) {
    return (
      <ErrorMessage error={error} message="An unexpected error has occurred" />
    )
  }
  return children
}

QueryResponseWrapper.defaultProps = {
  loading: false,
  error: null,
}

QueryResponseWrapper.propTypes = {
  children: PropTypes.node.isRequired,
  loading: PropTypes.bool,
  error: PropTypes.object,
}

export default React.memo(QueryResponseWrapper)

As you can see QueryResponseWrapper has nothing to do with data, it just renders children if loading is false and there are no errors.

I hope this helped :)

hrasoa commented 5 years ago

Maybe let’s wait for updates from the Apollo team, if there is nothing new to say. We’re all eager to use hooks and I’m sure it’s a priority for the Apollo team. Let’s not turn this issue into a Chatroom. @hwillson could you maybe lock this thread for now.

Yes please lock this thread and let's wait for the Apollo team to release their version before talking about "potential" issues.

buzinas commented 5 years ago

@hwillson Your last post was exactly one month ago, and since React Hooks were released as part of React v16.8 10 days ago, I'd like to ask: do you have any updates on this?

(No pressure, only wanted to know so I can plan our migration process).

n1ru4l commented 5 years ago

@buzinas Why would you put the effort into migrating everything to use the new APIs? HOCs do still work and I am pretty sure the FaaC API's will still work once the Apollo Team starts working on Hook/Suspense implementations.

buzinas commented 5 years ago

@n1ru4l I think I could have worded better. We just started using Apollo a few weeks ago, and we're still not running it in production, so for us it would be worth it to rewrite the parts we've done, because it won't be so much effort, and we'll be able to use hooks for everything new as well.

hwillson commented 5 years ago

Hi all - here's where we're at:

--

The Good

The Bad

We're still a bit concerned that working on Hooks support in React Apollo might be jumping the gun, until Suspense for data fetching is released. The final version of this will impact whatever decisions we make to adopt Hooks, and it's too early to tell how much it will impact them (but our guess is it will impact them significantly). We might be better off waiting until Suspense for data is released, as the last thing we want to do is publish a Hooks version of React Apollo that everyone starts using, only to have to change the public API again when Suspense for data hits. In the meantime, people who want Hooks support now can use react-apollo-hooks.

The Ugly

React Apollo supports HOC's via graphql, render props via Apollo Components, and soon it will support Hooks. Is this too much? Yes, there are pros/cons to using each approach, but there is a massive overlap in terms of what you can accomplish with each approach. We realize this is an issue (or maybe advantage?) across the entire React ecosystem, but one of the main things we try to do at Apollo is help give developers clear guidance on what we think is the best approach for working with GraphQL. Adding a new option (without removing an older one) might add to the confusion developers are already faced with, when it comes to picking and choosing a GraphQL data loading approach. So … if we add Hooks, should we deprecate another approach? Definitely a painful question to answer - each approach has its pros/cons, but supporting each one impacts library bundle size, maintenance overhead, documentation and knowledge sharing / training.

--

Given the above, here's our current plan:

  1. First get Apollo Client / React Apollo 2.5.0 launched. We won't be diving further into Hooks work until 2.5 is out (which is mostly dependent on docs work at this point, so hopefully it will be out next week).

  2. Once that's done, review react-apollo-hooks more extensively, planning out how it could be best integrated into react-apollo.

  3. At the same time, reach out to the React team to discuss all things Suspense for data, to get a better understanding of what the impact will be to a project like react-apollo (to see if we can glean how much of the Hooks work would have to change).

  4. Decide if we want to move forward with Hooks support, or wait for Suspense+data.

  5. If we're going ahead with Hooks support, then we'll post a design doc outlining the results of step 2 above, and if anyone in the community wants to work on it, then awesome! Otherwise we'll tackle it.

or

  1. If we're going to wait until Suspense+data is released, then we'll get react-apollo-hooks featured more prominently in the React Apollo docs, perhaps with a Hooks section and examples.
mbrowne commented 5 years ago

Regarding "The Ugly", that seems like a very surmountable problem, so hopefully the approach for that could be decided on already by the time Suspense is fully released. Obviously which APIs to encourage and support out of the box will need to be carefully discussed within the internal Apollo team, but (and I'm not trying to start a discussion on the details here) there are good options to provide core APIs in the main package (say, hooks and ApolloConsumer) that can be easily used to support the other APIs via separate packages. I'm sure the community would be happy to play whatever role it can here, even if that's just completing a survey and/or providing feedback on alpha/beta releases as usual.

MrLoh commented 5 years ago

I think dropping HOC support is totally unproblematic. Our app still uses HOCs, but because some features related to error handling are not supported with HOCs (#604) I reimplemented HOCs with render Props a long time ago, just release such a tiny implementation as a separate package. Hooks will definitely replace nearly all use cases of HOCs, recompose has declared itself unneeded with hooks on the rise, so I wouldn't worry too much about that.

danielkcz commented 5 years ago

My two cents to "The Ugly". It's definitely premature at this point, but it's good to discuss it for sure. I think that after hooks implementation is stabilized within the react-apollo V2, there could be a major V3 release that would drop the HOC. Projects using HOC would stay on V2 until they fully migrate and then they can switch to V3. This comes with the obvious downside that any bug fixes should be merged to V2 & V3 for the time being. But it feels like the most reasonable path.

Edit: Not sure if there are people who would dislike Hooks and prefer HOC forever. I guess there is always an option to fork V2 when it stops being maintained (we are talking year or more here) and take care of it on your own. It's the cost for staying too much in the past 👯

camjackson commented 5 years ago

To state the (potentially) obvious: it would probably be a bad thing if the same release that adds hooks also removed HOCs and/or render props, because that would force people to do a big bang migration rather than a gradual one. The React team have always done an awesome job of introducing new stuff and deprecating/removing old stuff over a couple of releases, so that usually if your project runs without any warnings, it's safe to upgrade even a major version.

When it comes to Apollo, I personally am a HOC lover, and only an occasional user of render props, for reasons that aren't relevant to this thread. There's been a few mentions here of "each approach has its pros and cons", but having watched Sophie, Dan and Ryan's talks on hooks, it kind of looks to me like hooks are superior in every way to both of the other approaches.

So, maybe it's just because they're new and shiny, and I haven't gone deep enough yet to see the cons of hooks, but personally I would be totally OK with Apollo (eventually) removing support for both HOCs and render props in favour of only supporting hooks out of the box. That would definitely be a win in terms of bundle size, and simplifying code, documentation, community advice to centre around one approach. If HOC and render prop components could be supported through separate packages (e.g. react-apollo-hocs, react-apollo-components), that feels like an 'everybody-wins' result.

danielkcz commented 5 years ago

I would really love to see some of those people who downvote dropping HOC to actually express some reasoning why they need it: @brainkim @rj254 @volkanunsal @remi2j. Is it purely an annoyance of refactoring old code or is there something else?

it kind of looks to me like hooks are superior in every way to both of the other approaches.

There is one weak spot - conditional rendering. I know, there is a skip option, but in some cases it's not optimal. There might be some variables which you cannot fulfill at the time (you get them from eg. another query) and you would need to add weird conditions there. The Query prop kinda wins this one.

If HOC and render prop components could be supported through separate packages (e.g. react-apollo-hocs, react-apollo-components), that feels like an 'everybody-wins' result.

It's a brave idea, but I don't think anybody would have a will power to maintain it, except perhaps those people who would need it and that's kinda the "fork idea" I was talking about before.

volkanunsal commented 5 years ago

Since you asked @FredyC. Deprecating HOC is not just an "annoyance." It's a considerable undertaking for those of us who are fully invested in the HOC pattern. It requires rewriting thousands of lines of code across several projects, and it directly affects the bottomline of our business.

Secondly, conditional rendering is an issue, like you mentioned above. We have several places where this pattern is used.

Third, there are scenarios where using HOC is more advantageous than using hooks. It is a common pattern to drill into a query result and store only a small part of it in a prop above your render component. You cannot do this if you're using hooks.

danielkcz commented 5 years ago

@volkanunsal Thank you for your bravery :)

Deprecating HOC is not just an "annoyance." It's a considerable undertaking for those of us who are fully invested in the HOC pattern. It requires rewriting thousands of lines of code across several projects, and it directly affects the bottomline of our business.

Alright, fair enough. Do you have for example some periodic library updates? I mean unless something really big happens in GraphQL world, the current version of react-apollo is just fine for you, right? Now imagine if there would be V2 which would still keep HOC + Render prop and add Hooks so you can choose any of that. For others, there would V3 which would drop HOC. Would you be forced to use V3 ever? I don't think so :)

It is a common pattern to drill into a query result and store only a small part of it in a prop above your render component. You cannot do this if you're using hooks.

That's far from the truth. On the contrary, Hooks allows much better composition than HOC could ever dream of. Hooks are not forcing you to punch everything into one big component. It's only a matter of open mind and a bit of a fantasy to see some really amazing patterns.

volkanunsal commented 5 years ago

That's far from the truth. On the contrary, Hooks allows much better composition than HOC could ever dream of.

No, it's not. How would you cache intermediate artefacts of a query result using hooks? This kind of cache is absolutely essential if you're doing complex transformations with your query results. Also –– and I just remembered this –– since the data object turns into an empty object when Apollo is loading new data (at least that is what react-apollo-hooks does with the data object), you need to store the results somewhere as Apollo fetches new data if you don't want to put your component into "Loading" state or have it completely broken. That is why I haven't been able to use Apollo hooks anywhere except in trivial components that don't seem too badly affected by the flakiness that occurs because of this.

I'd be happy to gradually adopt hooks into my codebase, but I wouldn't want to give up an old technique that is still more complete than a new one, no matter how amazing you say it is. From the looks of it, it doesn't do everything HOC can do right now.

volkanunsal commented 5 years ago

@FredyC I rewrote one of my components using Hooks and HOC to think more about this. I still think the second one is more elegant, but I admit using the state hook can address the intermediate data issue I raised above.

If the data object wasn't emptied out during data fetch, I might be able to get rid of the NonEmptyObject checks, too. Then, the hook approach would be just as elegant as the HOC approach.

Hooks

const DataList = ((props: { companyId: ID, filters: string[] }) => {
  const opts = {
    suspend: false,
    variables: {
      filters: props.filters,
      companyId: props.companyId,
    },
  };
  const { data, loading } = useQuery(FavoritesQueryDef, opts);
  const [newData, setData] = useState(data);

  if (data !== newData && NonEmptyObject.is(data)) {
    setData(data);
  }
  const hasData = NonEmptyObject.is(newData);

  // ... 

  return (
    <React.Fragment>
      {items}
      <Loading show={!hasData && loading} />
    </React.Fragment>
  )
}

HOC

const DataList = graphql(FavoritesQueryDef)(props => {
  const { data, loading } = props

  // ... 

  return (
    <React.Fragment>
      {items}
      <Loading show={loading} />
    </React.Fragment>
  )
})
danielkcz commented 5 years ago

@volkanunsal

If the data object wasn't emptied out during data fetch, I might be able to get rid of the NonEmptyObject checks, too. Then, the hook approach would be just as elegant as the HOC approach.

I have noticed this as well. There are same underlying concepts inside the HOC and Hooks, so unless HOC is doing some weird voodoo magic, I am fairly certain it's just a bug in Hooks version, but had no capacity to pay attention to it.

Also –– and I just remembered this –– since the data object turns into an empty object when Apollo is loading new data (at least that is what react-apollo-hooks does with the data object), you need to store the results somewhere

You mean when variables change and query is re-run? Haven't noticed that really, but it's definitely super easy to abstract such behavior into custom hook if you need a previous result available.


In the HOC you left out types and variables for a brevity? :) Especially those variables are very awkward with HOC since you cannot just access props within a closure (yay Hooks). And types to be defined with HOC instead of with component is also strange. Let's have a look at a real example, shall we? :)

// had to separate it, as it would be too unreadable mixed with the component
const decorate = graphql<{ companyId: ID, filters: string[] }>(
  FavoritesQueryDef, {
    options: props => ({
      variables: {
        filters: props.filters,
        companyId: props.companyId,
      }
    })
  }
)
const DataList = decorate((props => {
  const { data, loading } = props

  // ... 

  return (
    <React.Fragment>
      {items}
      <Loading show={loading} />
    </React.Fragment>
  )
})

How would you cache intermediate artefacts of a query result using hooks? This kind of cache is absolutely essential if you're doing complex transformations with your query results.

Not sure what you mean by that, like a memoization? What about useMemo? You could have seriously dope custom hooks that would accept selectors to make that happen. Or any other kind of cache you would like. While with HOC it's rather hard to make a custom one, Hooks are just functions so you can use whatever you like in them.

I'd be happy to gradually adopt hooks into my codebase, but I wouldn't want to give up an old technique that is still more complete than a new one, no matter how amazing you say it is. From the looks of it, it doesn't do everything HOC can do right now.

Sure, Hooks are still fresh out of the oven and more patterns will certainly emerge over time. And fair about of bug will be squashed as well :) HOC had more time to grow, so it feels much more polished for sure.

volkanunsal commented 5 years ago

Not sure what you mean by that, like a memoization? What about useMemo?

useMemo is something like a pure component, right? It's not exactly what I had in mind. I meant saving transformed artefacts. I think state hooks will do a good enough job for that.

I rewrote useQuery with a custom hook to eliminate the NonEmptyObject checks in the render component. We can apply the same technique for transformed artifacts, as well. It was easier than I thought it would be. Still not as elegant as HOC, but it's definitely in the same ballpark with HOC now.

function useCachedQuery(query, opts) {
  const { data, error, loading } = useQuery(query, opts);
  const [newData, setData] = useState(data);
  if (data !== newData && NonEmptyObject.is(data)) {
    setData(data);
  }
  return { data: newData, error, loading };
}

const DataList = ((props: { companyId: ID, filters: string[] }) => {
  const opts = {
    suspend: false,
    variables: props,
  };
  const { data, loading } = useCachedQuery(FavoritesQueryDef, opts);
  const hasData = NonEmptyObject.is(newData);

  // ... 

  return (
    <React.Fragment>
      {items}
      <Loading show={!hasData && loading} />
    </React.Fragment>
  )
}

I can see that hooks aren't much worse than HOCs in terms of storing intermediate query artifacts, like I thought. Thanks for the nice discussion @FredyC.

danielkcz commented 5 years ago

I hope that other people can see what you have just learned 👍 I believe this is kinda a root of the problem. People are used in some ways and they are content about them because there was nothing better at that time. Why would they bother with some Hooks, right? There is an obvious barrier which is hard to beat for sure, but it can pay back (with dividends) 💰

useMemo is something like a pure component, right? It's not exactly what I had in mind. I meant saving transformed artefacts. I think state hooks will do a good enough job for that.

No, that's React.memo you are talking about. The useMemo is really for memoization purposes.

Btw, tiny nit :) No need to repeat that hasData check in every component, right? You could even "merge" it with loading and hide that away completely. With HOC something like that is much harder to do as you need to add another HOC in middle to modify props.

 const { data, loading, hasData } = useCachedQuery(FavoritesQueryDef, opts);

Best part of about programming is the amount of code you delete!

mbrowne commented 5 years ago

As with any new, shiny technology, I think it's important to be wary of too much "silver bullet" thinking. But having said that, I think hooks are a particularly good fit for graphql/Apollo. I doubt that direct usage will be superior 100% of the time, but hooks could still be the underlying implementation even in those cases. @FredyC suggested that extracting HOCs and render props into a separate package might lead to a situation where they're not sufficiently maintained, but if you think about how hooks could be used to implement those same APIs, there's really not that much to maintain...for example, the Query component could be implemented as simply as:

import { useQuery } from 'react-apollo-hooks'

export default function Query({ query, children, ...otherProps }) {
    return children(useQuery(query, { ...otherProps, suspend: false }))
}

And you can even still use it from class components! ...if you need to maintain components you don't intend to migrate to function components (assuming you intend to migrate at all).

To be a bit more clear about where direct hook usage might not be the best, I'm still not convinced that the QueryLoader use case I mentioned above is improved in any way by the hook implementation suggested by @juank11memphis (much as I appreciate his feedback, the render prop solution still seems better to me from the usage perspective). That use case might always be handled in app code (as it is now in my app) rather than being supported by react-apollo directly, so I don't think this thread is the right place to discuss the tradeoffs (maybe we can discuss it elsewhere). But just to illustrate my above point, that use case too could be implemented by hooks behind the scenes and the external API could still use a render prop as my current QueryLoader component is doing:

function Demo() {
    return (
        <QueryLoader query={MY_QUERY} variables={...}>
            {({ data }) => (
                <div>
                    {data.foo}
                </div>
            )}
        </QueryLoader>
    )
}

I'm actually less concerned about the conditional rendering use case. I might be missing something, but I think that could be easily handled by an alternative mode for useQuery() (perhaps activated by a simple boolean parameter) that returns a function that executes the query rather than executing the query immediately—similar to how you can call react-apollo-hooks's useMutation() to return a mutation function that you call later. So something like:

const runQuery = useQuery(MY_QUERY, { curry: true })
...
    // later, possibly inside some condition...
    <Suspense fallback={<div>Loading...</div>}>
        {/* Passing a function to React.createElement() might not be the best idea here;
            just keeping the example concise */}
        {React.createElement(() => {
            const { data, error } = runQuery({ variables: ... })
            ...
        })}
    </Suspense>
Nayni commented 5 years ago

As with everyone in this thread I've been very interested to know what the future of React Apollo holds regarding hooks and also Suspense for the future.

I agree the remarks of The Ugly and would love to see Apollo come up with a go-to solution for the broader audience. It would be great that the usage of React-Apollo leads you to the most acceptable and clear solution to add data fetching in your app. I find this especially important for data fetching because it is the main portion of state of any medium to large size application so getting this right is a big deal.

With that said, and looking at how Hooks are the new solution forward I think it's more than logical to focus this solution into that direction, mainly because Hooks seem to be the way the React team wants to move forward as well.

I personally see this like how Render Props came into the eco-system, the only big different with Hooks and Render Props are that Hooks are an official feature React itself, while Render Props was a powerful pattern that emerged from the community. Even though Hooks are still young they do seem to solve the majority of problems and it also seems that the React team is pushing Hooks to be the solution to the majority of state management. I also notice that for the majority of problems you can solve with Render Props or HoC's there is a Hooks variant you can write, and most importantly if you can implement it with Hooks you can wrap it in the previous patterns (being Render Prop or HoC).

I agree with @camjackson that the migration is a point to be discussed and taken carefully but what I would love to see is for React Apollo to be pushing the standardized solution which I believe will be Hooks but then also offer me quick bindings to convert Hook implementations into HoC's or Render Props. @mbrowne shows some relevant use-cases for this which. I think here React Apollo could come with additional packages or documentation support to help users implement such use-cases (I'm thinking something like react-apollo-hoc or react-apollo-renderprop which could be tiny packages that wrap the official Hook implementation into a consumable HoC or RenderProp component.

We currently have a code base that is full of Higher order components and the re-compose pattern. We are on the fence to move to Hooks because a lot of this is decided by having it baked into the data fetching layer which at this point is to either wrap it ourselves or resort to react-apollo-hooks, but this has the risk that we'll have to re-do our work once React-Apollo ships with its own official hooks and we'd like to avoid doing a double refactor.

jamuhl commented 5 years ago

I do like to share some insights on how we handled above the good / the ugly in i18next.

We took the chance to make a full rewrite of our react integration for the i18next i18n lib. For us that had big impact in reducing module size and clean up a lot of stuff.

Why had we similar "ugly" problem? i18next supports adding "backend" plugins to load translation files from the backend server -> so our hooks might be in state "loading/notReady" or "loaded/ready" but we decided to use Suspense because Suspense itself is defined and works very well -> throw a promise which gets catched higher up the JSX tree and suspense rendering until that suspense gets resolved: https://github.com/i18next/react-i18next/blob/master/src/useTranslation.js#L76

Why do we feel save to do so:

See also the same discussion we had about using suspense and breaking the rules of hooks:

-> https://github.com/i18next/react-i18next/issues/735 -> React issue about throwing a Suspense from Hook: https://github.com/facebook/react/issues/14848

the hook enabled us to provide other usage options like HOC and render prop by reusing the hook:

This is just what we did and the case for apollo might be a lot different. So just take this as is.

sessionboy commented 5 years ago

Which version is expected to support the react hook ?

danielkcz commented 5 years ago

@sessionboy Please, read https://github.com/apollographql/react-apollo/issues/2539#issuecomment-464025309

sessionboy commented 5 years ago

@FredyC #2539. It's here. But it seems to be still under discussion.

danielkcz commented 5 years ago

@sessionboy But it kinda answers your question, right? Since no one knows, then no one can give you a better answer.

DAB0mB commented 5 years ago

I would like to share with everyone my experience with Apollo-GraphQL and React-hooks while building a WhatsApp-Clone. So far when I wanted to run GraphQL operations I had to implement dedicated React.Elements within the virtual DOM tree, which from an architectural perspective never really made sense to me. The virtual DOM's purpose is to build the view by binding data-models into a template, and so by adding data-fetching into the mix you basically abuse it. In addition, having to use more than one GraphQL-operation for a React.Component can cause these extra indentations which basically brings us back to the issue with ES5 where we had a callback hell; this is why they invented Promises and later on async/await.

Because of that, I was fairly excited when I first noticed @trojanowski's react-apollo-hooks package. Not only it solves all the issues above, but it also supports React.Suspense for data-fetching. This way you can control which nodes up the virtual DOM tree are gonna remain visible while waiting for the data to fetch, regardless of the implementation of the fetching React.Component. The clear advantage of using React.Suspense over not using it, was that I didn't have to handle multiple loading flags; It saved a lot of time and potential errors. Sure there are some limitations to that, such as concurrent data-fetching (something like Promise.all), but still, it all made a lot of sense and it just worked. I would love to see Apollo heading that direction.

import gql from 'graphql-tag';
import { useQuery } from 'react-apollo-hooks';

const GET_DOGS = gql`
  {
    dogs {
      id
      breed
    }
  }
`;

const GET_CATS = gql`
  {
    cats {
      id
      breed
    }
  }
`;

const Animals = () => {
  // No extra indentions, easy to read and maintain
  const { data: { dogs } } = useQuery(GET_DOGS);
  const { data: { cats } } = useQuery(GET_CATS);

  // return ...
};
mbrowne commented 5 years ago

Good point about the virtual DOM—that's one of the benefits the React team cites for hooks as well. The principle I have gathered from my understanding so far is, "hooks for shared logic, components for rendering". Data-fetching certainly falls in the "shared logic" category. (I'll have to think more about my QueryLoader component and whether or not it actually justifies adding another element to the virtual DOM. It does do rendering—the loader component, error message, or content as the case may be.)

Side-note about concurrent data-fetching and suspense: you've probably aware of this already, but I think ConcurrentMode could help with this: https://reactjs.org/blog/2018/11/27/react-16-roadmap.html#react-16x-q2-2019-the-one-with-concurrent-mode

oklas commented 5 years ago

I would suggest to pay attention to the format of the returned tuple apollo hooks. The object form is useful when the number and names of data elements are not obvious or unknown. For example, this is convenient for the response itself, which depends on the request.

The advantages of an object-based tuple might be convenient at the initial stage of development when it is not obvious what data is required and can always be expanded.

The disadvantage is the necessary for additional actions to match the names of fields and variables:

  const { data: dogs, error: dogsError, loading: dogsLoading } = useQuery(GET_DOGS);
  const { data: cats, error: catsError, loading: catsLoading } = useQuery(GET_CATS);

The disadvantage of an array-based tuples is having of rigidly binding parameters to their position. Deleting an item will require the loss of an entire position and problems with using api. However, today api has been used for quite some time and can be considered fairly settled.

The main advantage of array-based tuples is the ability for the user to set variable names without additional actions:

  const [ dogs, dogsError, dogsLoading ] = useQuery(GET_DOGS);
  const [ cats, catsError, catsLoading ] = useQuery(GET_CATS);
danielkcz commented 5 years ago

@oklas Why not just like that? :)

  const dogs = useQuery(GET_DOGS);
  const cats = useQuery(GET_CATS);

  dogs.data !== cats.data

Sure, it's slightly more verbose, but considering this is rare to call several queries within a single component, I wouldn't be complicating API much because of that. Besides, you can always merge those queries on document level and get that in a single package from the server.

sandiiarov commented 5 years ago

@FredyC This is cool. But, dogs.data && dogs.data.dogs && dogs.data.dogs.map(...

Another example.

If I do like that:

const { data: { dogs } , error, loading } = useQuery(GET_DOGS);

prettier will transform that code to:

const {
  data: { dogs },
  error,
  loading,
} = useQuery(GET_DOGS);

Or with array syntax:

const [{ dogs } , error, loading] = useQuery(GET_DOGS);
rovansteen commented 5 years ago

The nice thing about hooks is that you can always compose it with a custom hook to the syntax you prefer :)

danielkcz commented 5 years ago

@sandiiarov Really, why don't you just merge that query into a one? You would save yourself a lot of trouble. And if you want to stay DRY, just use fragments for common field selections.

query {
  dogs { ... }
  cats { ... }
  mices { ... }
}

Otherwise, what @rovansteen said, just make yourself a custom hook that would give you results in the tuple form. No harm in that.

sandiiarov commented 5 years ago

I can give you the same advice with a custom hook or custom HOC or even with a custom render props component. Just make yourself a custom hook 😄. Is it helpful? Do you wrap useState into a custom hook because you don't like syntax? I don't think so. Why should I do that with react-apollo? I would prefer to use the standard syntax even if I don't like it.

I am just trying to show the pros of that solution.

rovansteen commented 5 years ago

Hooks are way easier to compose than render props and HOCs. I agree with you and I personally prefer the tuple syntax. I’m just pointing out that whatever syntax will be implemented you can easily make a custom hook to fit your preference or use case because they compose so well.

danielkcz commented 5 years ago

@sandiiarov There is a major problem with a tuple in this case. It's rather opinionated. Does loading go as the second or third argument? What about refetch, fetchMore and several other helpers?

const [ dogs, dogsError, dogsLoading, { refetch: refetchDogs } ] = useQuery(GET_DOGS); 😮 

GraphQL gives you the nice ability to merge queries together. Feels like you want to shoot yourself in the foot deliberately not wanting to use that 😄

Besides, all that loading & error handling shenanigans are pretty much temporary. With Suspense we won't need loading in most cases. And for errors, they will be most likely be rethrown and caught with error boundary. You would design API here that would become obsolete eventually.

rovansteen commented 5 years ago

👆🏻 This exactly.

I always create my own components (or in the future hooks) on top of Apollo because I want them to be opinionated for my project and use cases. E.g. I always throw errors because I never want to handle them in my query components. But that doesn’t make it a good baseline API for everyone.

I like the tuple API for its simplicity but I agree with @FredyC that the order can be very confusing. So I think it makes most sense to stick with the current objects until Suspense simplifies everything massively.

sandiiarov commented 5 years ago

@FredyC this is a good point. I agree with you.

revskill10 commented 5 years ago

Reading this threads made me seriously rethink about current state of simplicity in design. As a developer, i'm also the customer of library author like yours. What a customer want ? Simplicity, right ? Just make a generic useGraphql is enough to me, with the whole parameters taken from Official Fetch API. So, i can just use useGraphql, put my graphql query there (it could be anything, query, mutation, subscription, or whatever, because it's just fetch after all. So, as a customer, what i expect from the library is the library could do some "smart" things like caching, invalidation, reconnect,... for developers.

In my work, even i use apollo, my API design is just

const [data, loading, errors] = useGraphql({uri, headers, query})

That query could be a graphql query, mutation, subscription, it doesn't matter to me, as the consumer of a library. So, just minimize API surface, remove all the redundant APIs like Query, Mutation, Subscription,... They're identical, indifferent to me under consumer point of view. No more graphql, What's graphql ? It's just a string i put in my POST request after all.

Last words: Keep API surface minimal, so that instead of the big documentation page, shape it so that it could fit a small github README instead.