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?

vovacodes commented 5 years ago

Looks like the first step to make it happen is to start using the react 16.3 context API, I opened #2540 to address this

kristianmandrup commented 5 years ago

Yes, the Mutation and Query tags should also be available as useMutation and useQuery :) Let's do it!

kristianmandrup commented 5 years ago

I started a very basic attempt at hooks here: https://github.com/kristianmandrup/react-apollo/tree/hooks

Feel free to continue this effort. So far extracted Query and Mutation as classes so they are not React Components any longer but can be used as standalone classes.

Also added dependencies to 16.7 alpha versions of react and react-dom

    "react": "^16.7.0-alpha.0",
    "react-dom": "^16.7.0-alpha.0",
lifeiscontent commented 5 years ago

I also opened up this issue here. should I close it? https://github.com/apollographql/apollo-feature-requests/issues/64

vovacodes commented 5 years ago

@lifeiscontent no idea, would love to hear from the maintainers. it makes sense to close one of the issues to keep the discussion in one place, but I don't really know which place is the right one

kristianmandrup commented 5 years ago

The main effort lies in updating the tests to test the hooks. The implementation itself is mostly just wrapping existing functionality with a few changes. I did much of the basic work, but tests remain to be refactored which I've started.

See my hooks branch if you want to give it a go.

kristianmandrup commented 5 years ago

I now have ~70% of the grunt work done here. Should be able to get it fully working and all tests passing, with ~3-4 hrs more work... Would love to see someone fork it and continue. Over and out here: midnight in London.

kristianmandrup commented 5 years ago

Added usage docs and examples for current implementation attempt: Apollo Hooks

aralroca commented 5 years ago

I like a lot the proposal 😍

parsadotsh commented 5 years ago

Integration with Suspense will also be nice.

albertorestifo commented 5 years ago

@hyperparsa Suspense is not yet ready to be used with data fetching.

trojanowski commented 5 years ago

I created sample hooks to use with Apollo Client: https://github.com/trojanowski/react-apollo-hooks.

danielkcz commented 5 years ago

Even though Suspense is not ready for data fetching, isn't it like double work to do it with hooks first and then rewrite to Suspense? I mean if you look at this video from React Conf (time included) it's kinda clear that any data fetching won't be just a simple hook. Andrew even mentions Apollo there and that it should probably wait for a proper implementation.

vovacodes commented 5 years ago

@FredyC that's a very good point, that's why it would really be great if the maintainers would share their vision before we go nuts with our implementations and PRs :) I understand the excitement everybody feels about Hooks but we need to plan better what should the ideal API look like @jbaxleyiii

prateekrastogi commented 5 years ago

getting crazy for hooks. Can we resolve this issue on priority basis?

danielkcz commented 5 years ago

@prateekrastogi If you are so crazy, just use a package from @trojanowski but be warned it will most likely become obsolete and completely different.

https://github.com/trojanowski/react-apollo-hooks.

prateekrastogi commented 5 years ago

@FredyC I did checked that package, and that was my only reservation about it. :-)

maxguzenski commented 5 years ago

I've made a very basic useQuery/useMutation that is working on my medium/large project (not in production yet)

https://gist.github.com/maxguzenski/f23f7bba4c726ea5956b17fd3917fa1c

juank11memphis commented 5 years ago

Any idea when would this be ready? Would be awesome to have an official react-apollo hooks implementation :)

aralroca commented 5 years ago

@juank11memphis I imagine after hooks will be more stable, not alpha 😑 However, I'm also looking forward for this

danielkcz commented 5 years ago

@juank11memphis @aralroca Do you realized this isn't actually about hooks? To get the most out of upcoming changes, Apollo should leverage Suspense and that works somewhat differently. Sure, you can use hooks instead of current render prop components, no harm in there and it will work (some working implementations are already done ⬆️ ). However, note that you cannot really suspend rendering with a hook. You have to handle loading states by checking loading prop.

alan345 commented 5 years ago

Great article: Writing Custom React Hooks for GraphQL https://medium.com/open-graphql/react-hooks-for-graphql-3fa8ebdd6c62

trojanowski commented 5 years ago

Even though Suspense is not ready for data fetching, isn't it like double work to do it with hooks first and then rewrite to Suspense? I mean if you look at this video from React Conf (time included) it's kinda clear that any data fetching won't be just a simple hook. Andrew even mentions Apollo there and that it should probably wait for a proper implementation.

Isn't the base of suspense stable since React 16.6? At least the Suspense component is documented in the official React docs. I understood from the linked presentation that the unstable part is the react-cache library which will allow us to easily build suspense-compatible resources. By the base of suspense, I mean a possibility of throwing promises in the render method / functional components and React waits for them to be resolved and shows the fallback UI defined with the <Suspense /> component. @acdlite @gaearon Is there a possibility it will be changed?

@juank11memphis @aralroca Do you realized this isn't actually about hooks? To get the most out of upcoming changes, Apollo should leverage Suspense and that works somewhat differently. Sure, you can use hooks instead of current render prop components, no harm in there and it will work (some working implementations are already done ⬆️ ). However, note that you cannot really suspend rendering with a hook. You have to handle loading states by checking loading prop.

@FredyC

I don't think it's correct. You can throw a promise inside of a hook. My react-apollo-hooks seem to be suspense compatible (you actually have to use it with the <Suspense /> component). It's done in that line.

hlehmann commented 5 years ago

@trojanowski for what I have seens with react-apollo-hooks, useRef() and other hooks are not saved when you throw the promise, breaking the app in some case.

trojanowski commented 5 years ago

@hlehmann Could you please create an issue at https://github.com/trojanowski/react-apollo-hooks/issues with more details?

dai-shi commented 5 years ago

This is kind of off topic, but I've been thinking how I could use hooks without waiting for official support. I created a small wrapper library that transforms render props to hooks. See the example below.

import { useRenderProps, wrap } from 'react-hooks-render-props';

const QUERY_POSTS = gql`                           
query queryPosts {                                        
  posts {                                                 
    id
    text
  }
}
`;

const useApolloQuery = (query) => {
  const [result] = useRenderProps(Query, { query });
  const fallbackResult = { loading: true }; // XXX this is a limitation.
  return result || fallbackResult;
};

// yeah, we can't avoid wrapping...
const PostList = wrap(() => {
  const { loading, error, data } = useApolloQuery(QUERY_POSTS);
  if (loading) return <span>Loading...</span>;
  if (error) return <span>Error: {error}</span>;
  if (!data) return <span>No Data</span>;
  return (
    <ul>
      {data.posts.map(item => <li key={String(item.id)}>{item.text}</li>)}
    </ul>
  );
});

const ADD_POST = gql`                              
mutation addPost($text: String!) {                        
  addPost(text: $text)                                    
}
`;

const useApolloMutation = (props) => {
  const results = useRenderProps(Mutation, props);
  return results;
};

const NewPost = wrap(() => {
  const [addPost] = useApolloMutation({ mutation: ADD_POST, refetchQueries: ['queryPosts'] });
  const add = (text) => {
    addPost({ variables: { text } });
  };
  return (
    <TextInput add={add} />
  );
});

The full example is in: https://github.com/dai-shi/react-hooks-render-props/tree/master/examples/03_apollo

Note that this is an experimental project for fun.

prateekrastogi commented 5 years ago

Since apollo-react 2.3 released yesterday, are there any plans or timeline to give this feature request serious attention for future release?

kristianmandrup commented 5 years ago

My attempt was mostly just looking into what it would take to refactor the existing code to be more flexible in order to accommodate hooks or other api "consumers". Currently the code contains a lot of anti patterns (at least in my book), being hard coded for one particular consumer.

The code could use some fresh refactoring to be much more composable and loosely coupled IMO. I'm fully aware that my "attempt" is not working. I never even tried to run it.

The problem is that the core code is fitted directly into a component, whereas a much better way would be to have it standalone and then add a thin component on top, to accommodate queries/mutations being used from the view layer as components (render props, HOC etc.) but allowing direct access for other exposers/consumers to the core logic without going through a component.

dennishansen commented 5 years ago

Any update on this?

danielkcz commented 5 years ago

@dennishansen What update are you expecting? If you are eager to play with hooks, just grab https://github.com/trojanowski/react-apollo-hooks? I think that top-level API won't be that different once implemented officially.

fbartho commented 5 years ago

@FredyC Are we waiting on some particular release of the React Hooks API before we consider accepting PRs that would add Hooks support into the react-apollos core repo?

It'd be awesome if we could consider porting @trojanowski's work into core as a starting point.

benseitz commented 5 years ago

I also believe that thinking about an official apollo hooks API is a good idea.

Once react hooks get to stable (sometimes in Q1 2019) people want to use them with apollo in production. This means the apollo hooks API shouldn't change then :)

Also: The time between now and then gives the community the possibility to give feedback on the API design.

RIP21 commented 5 years ago

I will be extremely happy with the alpha version that will use hooks, also I think it can give a nice PR effect and hype effect to react-apollo itself which is nice for an OSS project. So I think that would be win-win for everyone. Please consider it. Thanks.

hwillson commented 5 years ago

Hi all - we're definitely planning on implementing React Hooks support. We've been in discussions with the React core team over Hooks and Suspense, and while these new React features are super exciting, they're not quite ready to be implemented yet. When things stabilize a bit more, we'll be jumping on this for sure.

fbartho commented 5 years ago

I'm fantastically interested in this feature. We re-wrote our whole app to React-Native over last year, and we heavily relied on Apollo. It shipped & is stable!

I did a quick experiment in our app, and react-hooks would significantly increase the legibility of many of our data containers (Screens/Controllers) -- as well as increasing team velocity due to the lower cognitive burden & easier diffing.

We could take a hybrid approach and use hooks for everything except for the Query-components, but using @trojanowski's lib, it really flattens and simplifies a lot of what's going on.

I have team resources for the next few months that I can apply to this, so we would love to contribute as-soon-as-possible. Please let us know!

hwillson commented 5 years ago

Thanks very much for the offer to help @fbartho! We're discussing this internally at a meeting this upcoming Monday, after which I'll post back with a plan of attack.

danielkcz commented 5 years ago

I have been using @trojanowski lib for a bit as well and it's clear to me now that hooks version is not a silver bullet. There are basically two scenarios where it needs a bit different approach. Perhaps more, but I haven't found them yet.

Executing a query on request (by a user), like filter form which should run a query only after hitting the "filter" button. I basically made a custom useExecutableQuery which just uses client.query underneath, but it's not always ideal. Especially thinking in terms of Suspense, I opted to usual loading variable as I have much more control over what's displayed where. I hope this will get somehow improved in future.

Another scenario is a conditional query. The skip attribute (got merged yesterday) is one way but feels rather awkward. Rendering appropriate <Query> feels so much easier in the end.

Mutations and Subscriptions are winning with hooks for sure. Either way, it's just as a few considerations for future implementations, nothing spectacular :)

danilobuerger commented 5 years ago

@hwillson Any news? Whats the plan of attack?

hwillson commented 5 years ago

Sorry for the delay all (last weeks meeting was re-scheduled to today). We've had some preliminary discussions about React Apollo's future Hooks support, and have set aside more time to work on it. Our biggest Apollo Client / React Apollo push for the next 2 weeks is finalizing https://github.com/apollographql/apollo-client/pull/4155, and everything that's impacted by that PR (e.g. Apollo Client Devtools, Link state, docs, etc.). After AC 2.5 is published, we'll have more time to devote to Hooks work. In the meantime however, we're working on hashing out an initial design, which we'll share with everyone here for feedback. Once we're all happy with the design, then we'll go from there - if anyone here wants to help work on the code, then awesome! :bow:

conradreuter commented 5 years ago

Isn't the base of suspense stable since React 16.6? At least the Suspense component is documented in the official React docs I don't think it's correct. You can throw a promise inside of a hook.

AFAIK the whole "throw a Promise" thing is not part of React's official API. At the moment <Suspense> can be only be used in combination with lazy.

mbrowne commented 5 years ago

@conradreuter Throwing a promise already works inside <Suspense> but it might be premature to start using it, since it's not official yet...for one thing, it doesn't seem to work with <ConcurrentMode> yet.

trojanowski commented 5 years ago

@conradreuter @mbrowne I found this in Reactiflux:

screenshot 2019-02-02 at 08 01 03

and I also found this tweet: https://twitter.com/acdlite/status/1056940697775878145. So I guess the "throw a promise" part of suspense can be considered safe to use.

trojanowski commented 5 years ago

An update to my last comment - @gaearon says that using Suspense for data fetching is not recommended yet: https://github.com/trojanowski/react-apollo-hooks/issues/69#issuecomment-460833136.

Richard87 commented 5 years ago

Hi guys! I'm really looking forward to Apollo Client Hooks :D While I'm (anxiously) waiting, I have built my own for our use case (basically CRUD, create, retrieve, update and delete functionality) and some helper functions to make my life easier ;)

An extremeley naive implementation, that doesn't support SSR :(

import {useContext, useEffect, useState} from "react"; import {ApolloContext} from "../App"; import {CreateChildComponent, DeleteComponent, GetComponent, MutateComponent} from "./Site"; export const useComponent = (id) => { const [data, updateData] = useState({}) const [currentData, updateCurrentData] = useState({}) const {apolloClient: client} = useContext(ApolloContext) || {} const saveComponent = (newVariables) => { const variables = mixVariables(currentData, newVariables || currentData) client.mutate({ mutation: MutateComponent, variables: {id, ...variables}, refetchQueries: [{ query: GetComponent, variables: {id} }] }) updateCurrentData(JSON.parse(JSON.stringify(variables))) updateData(variables) } const changeComponent = (newVariables) => { if (typeof newVariables !== "object") { throw new Error("changeComponent must have a object argument!!!!!!!!!!!!") } const variables = mixVariables(currentData, newVariables) updateCurrentData(variables) } const resetComponent = () => { updateCurrentData(JSON.parse(JSON.stringify(data))) } const addChildComponent = (type, data) => { client.mutate({ mutation: CreateChildComponent, variables: {id,type, ...data}, update: (cache, {data: {createChildComponent}}) => { const { component} = cache.readQuery({ query: GetComponent, variables: {id} }); component.children.push(createChildComponent) cache.writeQuery({ query: GetComponent, data: { component}, variables: {id} }); updateCurrentData(JSON.parse(JSON.stringify(component))) updateData(component) } }) } const deleteComponent = () => { client.mutate({ mutation: DeleteComponent, variables: {id}, update: (cache, {data: {deleteComponent}}) => { const deletedComponentId = deleteComponent.id; const keys = Object.keys(cache.data.data); for (let i = 0; i < keys.length; i++) { const tmp = cache.data.data[keys[i]] if (tmp.__typename !== "Component") continue const deletedItem = tmp.children.find(item => item.id === 'Component:'+deletedComponentId) if (deletedItem) { const {component: parent} = cache.readQuery({ query: GetComponent, variables: {id: tmp.id} }); parent.children = parent.children.filter(child => child.id !== id) cache.writeQuery({ query: GetComponent, data: { component: parent}, variables: {id: parent.id} }); break; } } } }) } useEffect(() => { if (!id) { updateData({}) updateCurrentData({}) return; } client.query({ query: GetComponent, variables: {id} }).then(({data}) => { updateData(data.component) updateCurrentData(JSON.parse(JSON.stringify(data.component))) }) }, [id]) return [id ? currentData : {},changeComponent, saveComponent, resetComponent,deleteComponent, addChildComponent] } export default useComponent function mixVariables(currentData, {content, lang, order, tags}) { return { content: typeof content !== "undefined" ? content : currentData.content, lang: typeof lang !== "undefined" ? lang : currentData.lang, order: typeof order !== "undefined" ? order : currentData.order, tags: typeof tags !== "undefined" ? tags : currentData.tags, }; }

Also, can't wait for native Apollo hooks (did Isay this already?)

pleunv commented 5 years ago

Personally I'm not in a rush to replace <Query /> components, <Mutation /> components however very quickly lead to render prop "christmas trees" when you have to add a 3 or more mutations to one particular data container, and I'm guessing most people have a similar experience. Would it be an option to incrementally add hooks support by perhaps focusing on mutations first, or does that not make much of a different implementation-wise in the grand scheme of things?

seeden commented 5 years ago

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

breytex commented 5 years ago

Opposed to @pleunv, I cant wait to get Hook support for queries and mutations in apollo. The renderprop syntax is the only thing which holds be back from using apollo and graphql in all of my projects. I am rather using the plain JS graphql api for now, and load the data in componentDidMount.

Hooks gonna bring the API for frontend graphql to a new level, so please push that topic :)

n1ru4l commented 5 years ago

@breytex You could still use the graphql hoc api

Urigo commented 5 years ago

So now that Hooks are stable in React, I want to share here our full stack WhatsApp clone!

It uses React Hooks (updated to the new stable version), Suspense, Apollo, GraphQL Subscriptions, Typescript and also the whole backend (Postgres, Typescript, etc..) - mostly thanks to the great work from @trojanowski on react-apollo-hooks.

I wanted to write it here mostly because I feel there should be more comments on this thread about the amazing work @trojanowski is doing. He is doing a fantastic job on his library, which led to a lot of great design discussions happening in the open (on issues on his repo), which led to many constant improvements for the library itself. For us, even in the current state, hooks make sense in a lot of cases already, and where it still rusty, we are part of discussions on his repo and where people offer different solutions, strategies and best practices.

I would love if @trojanowski will be part of the discussions inside Apollo and their talks with the React team about the implementation. Might help make those as open as those happening on @trojanowski 's library.

@trojanowski your work is inspiring - thank you ❤️

breytex commented 5 years ago

hey @n1ru4l, thanks for the suggestion :) I was very sad when apollo killed the HOC api, which was, in my opinion, better syntax for 90% of the use cases compared to the renderprops.

So I tried to go as vanilla as possible for now, to refactor as soon as a new API is live, which is not long in the future anymore :)