diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000000..3031b723ab1 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,13 @@ +# semgrep defaults +# https://semgrep.dev/docs/ignoring-files-folders-code#defining-ignored-files-and-folders-in-semgrepignore +node_modules/ +dist/ +*.min.js +.npm/ +.yarn/ +.semgrep +.semgrep_logs/ + +# custom paths +__tests__/ +./docs/source/data/subscriptions.mdx diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index e6cd2fe29bc..3f3fed9fcac 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -57,6 +57,8 @@ To consume a multipart subscription over HTTP in an app using Relay or urql, Apo ##### Relay + + ```ts import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; import { Environment, Network, RecordSource, Store } from "relay-runtime"; @@ -73,8 +75,12 @@ export const RelayEnvironment = new Environment({ }); ``` + + #### urql + + ```ts import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; @@ -96,6 +102,8 @@ const client = new Client({ }); ``` + + ## Defining a subscription You define a subscription on both the server side and the client side, just like you do for queries and mutations. @@ -116,6 +124,22 @@ For more information on implementing support for subscriptions on the server sid In your application's client, you define the shape of each subscription you want Apollo Client to execute, like so: + + +```ts +const COMMENTS_SUBSCRIPTION: TypedDocumentNode< + OnCommentAddedSubscription, + OnCommentAddedSubscriptionVariables +> = gql` + subscription OnCommentAdded($postID: ID!) { + commentAdded(postID: $postID) { + id + content + } + } +`; +``` + ```js const COMMENTS_SUBSCRIPTION = gql` subscription OnCommentAdded($postID: ID!) { @@ -127,6 +151,8 @@ const COMMENTS_SUBSCRIPTION = gql` `; ``` + + When Apollo Client executes the `OnCommentAdded` subscription, it establishes a connection to your GraphQL server and listens for response data. Unlike with a query, there is no expectation that the server will immediately process and return a response. Instead, your server only pushes data to your client when a particular event occurs on your backend. Whenever your GraphQL server _does_ push data to a subscribing client, that data conforms to the structure of the executed subscription, just like it does for a query: @@ -158,7 +184,9 @@ npm install graphql-ws Import and initialize a `GraphQLWsLink` object in the same project file where you initialize `ApolloClient`: -```js title="index.js" + + +```ts title="index.ts" import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; @@ -167,6 +195,8 @@ const wsLink = new GraphQLWsLink(createClient({ })); ``` + + Replace the value of the `url` option with your GraphQL server's subscription-specific WebSocket endpoint. If you're using Apollo Server, see [Setting a subscription endpoint](/apollo-server/data/subscriptions/#enabling-subscriptions). ### 3. Split communication by operation (recommended) @@ -177,7 +207,9 @@ To support this, the `@apollo/client` library provides a `split` function that l The following example expands on the previous one by initializing both a `GraphQLWsLink` _and_ an `HttpLink`. It then uses the `split` function to combine those two `Link`s into a _single_ `Link` that uses one or the other according to the type of operation being executed. -```js title="index.js" + + +```ts title="index.ts" import { split, HttpLink } from '@apollo/client'; import { getMainDefinition } from '@apollo/client/utilities'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; @@ -209,12 +241,27 @@ const splitLink = split( ); ``` + + Using this logic, queries and mutations will use HTTP as normal, and subscriptions will use WebSocket. ### 4. Provide the link chain to Apollo Client After you define your link chain, you provide it to Apollo Client via the `link` constructor option: + + +```ts {6} title="index.ts" +import { ApolloClient, InMemoryCache } from '@apollo/client'; + +// ...code from the above example goes here... + +const client = new ApolloClient({ + link: splitLink, + cache: new InMemoryCache() +}); +``` + ```js {6} title="index.js" import { ApolloClient, InMemoryCache } from '@apollo/client'; @@ -226,12 +273,28 @@ const client = new ApolloClient({ }); ``` + + > If you provide the `link` option, it takes precedence over the `uri` option (`uri` sets up a default HTTP link chain using the provided URL). ### 5. Authenticate over WebSocket (optional) It is often necessary to authenticate a client before allowing it to receive subscription results. To do this, you can provide a `connectionParams` option to the `GraphQLWsLink` constructor, like so: + + +```ts {6-8} +import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; +import { createClient } from 'graphql-ws'; + +const wsLink = new GraphQLWsLink(createClient({ + url: 'ws://localhost:4000/subscriptions', + connectionParams: { + authToken: user.authToken, + }, +})); +``` + ```js {6-8} import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; @@ -244,6 +307,8 @@ const wsLink = new GraphQLWsLink(createClient({ })); ``` + + Your `GraphQLWsLink` passes the `connectionParams` object to your server whenever it connects. Your server receives the `connectionParams` object and can use it to perform authentication, along with any other connection-related tasks. ## Subscriptions via multipart HTTP @@ -258,6 +323,31 @@ You use Apollo Client's `useSubscription` Hook to execute a subscription from Re The following example component uses the subscription we defined earlier to render the most recent comment that's been added to a specified blog post. Whenever the GraphQL server pushes a new comment to the client, the component re-renders with the new comment. + + +```tsx +const COMMENTS_SUBSCRIPTION: TypedDocumentNode< + OnCommentAddedSubscription, + OnCommentAddedSubscriptionVariables +> = gql` + subscription OnCommentAdded($postID: ID!) { + commentAdded(postID: $postID) { + id + content + } + } +`; + +function LatestComment({ postID }: LatestCommentProps) { + const { data, loading } = useSubscription( + COMMENTS_SUBSCRIPTION, + { variables: { postID } } + ); + + return

New comment: {!loading && data.commentAdded.content}

; +} +``` + ```jsx const COMMENTS_SUBSCRIPTION = gql` subscription OnCommentAdded($postID: ID!) { @@ -273,10 +363,13 @@ function LatestComment({ postID }) { COMMENTS_SUBSCRIPTION, { variables: { postID } } ); + return

New comment: {!loading && data.commentAdded.content}

; } ``` +
+ ## Subscribing to updates for a query Whenever a query returns a result in Apollo Client, that result includes a `subscribeToMore` function. You can use this function to execute a followup subscription that pushes updates to the query's original result. @@ -285,6 +378,33 @@ Whenever a query returns a result in Apollo Client, that result includes a `subs As an example, let's start with a standard query that fetches all of the existing comments for a given blog post: + + +```tsx +const COMMENTS_QUERY: TypedDocumentNode< + CommentsForPostQuery, + CommentsForPostQueryVariables +> = gql` + query CommentsForPost($postID: ID!) { + post(postID: $postID) { + comments { + id + content + } + } + } +`; + +function CommentsPageWithData({ params }: CommentsPageWithDataProps) { + const result = useQuery( + COMMENTS_QUERY, + { variables: { postID: params.postID } } + ); + + return ; +} +``` + ```jsx const COMMENTS_QUERY = gql` query CommentsForPost($postID: ID!) { @@ -302,12 +422,31 @@ function CommentsPageWithData({ params }) { COMMENTS_QUERY, { variables: { postID: params.postID } } ); + return ; } ``` + + Let's say we want our GraphQL server to push an update to our client as soon as a _new_ comment is added to the post. First we need to define the subscription that Apollo Client will execute when the `COMMENTS_QUERY` returns: + + +```tsx +const COMMENTS_SUBSCRIPTION: TypedDocumentNode< + OnCommentAddedSubscription, + OnCommentAddedSubscriptionVariables +> = gql` + subscription OnCommentAdded($postID: ID!) { + commentAdded(postID: $postID) { + id + content + } + } +`; +``` + ```jsx const COMMENTS_SUBSCRIPTION = gql` subscription OnCommentAdded($postID: ID!) { @@ -319,8 +458,43 @@ const COMMENTS_SUBSCRIPTION = gql` `; ``` + + Next, we modify our `CommentsPageWithData` function to add a `subscribeToNewComments` property to the `CommentsPage` component it returns. This property is a function that will be responsible for calling `subscribeToMore` after the component mounts. + + +```tsx {10-25} +function CommentsPageWithData({ params }: CommentsPageWithDataProps) { + const { subscribeToMore, ...result } = useQuery( + COMMENTS_QUERY, + { variables: { postID: params.postID } } + ); + + return ( + + subscribeToMore({ + document: COMMENTS_SUBSCRIPTION, + variables: { postID: params.postID }, + updateQuery: (prev, { subscriptionData }) => { + if (!subscriptionData.data) return prev; + const newFeedItem = subscriptionData.data.commentAdded; + + return Object.assign({}, prev, { + post: { + comments: [newFeedItem, ...prev.post.comments] + } + }); + } + }) + } + /> + ); +} +``` + ```jsx {10-25} function CommentsPageWithData({ params }) { const { subscribeToMore, ...result } = useQuery( @@ -352,6 +526,8 @@ function CommentsPageWithData({ params }) { } ``` + + In the example above, we pass three options to `subscribeToMore`: * `document` indicates the subscription to execute. @@ -360,13 +536,26 @@ In the example above, we pass three options to `subscribeToMore`: Finally, in our definition of `CommentsPage`, we tell the component to `subscribeToNewComments` when it mounts: + + +```tsx +export function CommentsPage({ subscribeToNewComments }: CommentsPageProps) { + useEffect(() => subscribeToNewComments(), []); + + return <>... +} +``` + ```jsx -export function CommentsPage({subscribeToNewComments}) { +export function CommentsPage({ subscribeToNewComments }) { useEffect(() => subscribeToNewComments(), []); + return <>... } ``` + + ## `useSubscription` API reference > **Note:** If you're using React Apollo's `Subscription` render prop component, the option/result details listed below are still valid (options are component props and results are passed into the render prop function). The only difference is that a `subscription` prop (which holds a GraphQL subscription document parsed into an AST by `gql`) is also required. @@ -415,7 +604,9 @@ After you create your `wsLink`, everything else in this article still applies: ` The following is an example of a typical `WebSocketLink` initialization: -```js + + +```ts import { WebSocketLink } from "@apollo/client/link/ws"; import { SubscriptionClient } from "subscriptions-transport-ws"; @@ -428,4 +619,6 @@ const wsLink = new WebSocketLink( ); ``` + + More details on `WebSocketLink`'s API can be found in [its API docs](../api/link/apollo-link-ws).