Using Apollo Client

For the sake of equal comparison, we’re going to build our Apollo-based application on top of React. As we mentioned before, one of Apollo’s benefits is that it isn’t coupled to a specific UI framework (for example, we could also use Vue.js,[41] another popular option), but React is, by far, still the most popular choice for working with Apollo. Giving you the ability to build UI components with HTML, JavaScript, and even CSS in the same file, React has become one of the most widespread tools on the modern web.

We’re going to build a fresh user interface for our PlateSlate application, displaying the same list of menu items that we did before. We’ll also be adding support for live updating, showing you how you can use Apollo Client to work with Absinthe’s implementation of GraphQL subscriptions.

We’ll start by generating our application boilerplate. We’re going to use a handy tool, create-react-app, that will build in all the development server and compilation toolchain configuration that we’ll need to get down to work.

First, let’s install create-react-app:

Rather than forcing you to structure your application in a specific way, Apollo Client gives you the ability to connect your web-based user interface—regardless of what you use to build it—to a GraphQL API.

 $ ​​npm​​ ​​install​​ ​​-g​​ ​​create-react-app

After it’s installed, we’ll generate our project, giving it the name of plate-slate-apollo-ui:

 $ ​​create-react-app​​ ​​plate-slate-apollo-ui
 
 Creating a new React app in /path/to/plate-slate-apollo-ui.
 
 Installing packages. This might take a couple of minutes.
 Installing react, react-dom, and react-scripts...
 
 More output
 
 Success! Created plate-slate-apollo-ui at /path/to/plate-slate-apollo-ui
 Inside that directory, you can run several commands:
 
  yarn start
  Starts the development server.
 
  yarn build
  Bundles the app into static files for production.
 
  yarn test
  Starts the test runner.
 
  yarn eject
  Removes this tool and copies build dependencies, configuration files
  and scripts into the app directory. If you do this, you can’t go back!
 
 We suggest that you begin by typing:
 
  cd plate-slate-apollo-ui
  yarn start
 
 Happy hacking!

That’s a lot of output, but it’s useful stuff. create-react-app generated our application with a development web server, a test harness, and a number of other tools we can use as we build our application.

Let’s give the server a try, jumping into the project directory and running yarn start:

 $ ​​cd​​ ​​plate-slate-apollo-ui
 $ ​​yarn​​ ​​start

If the server starts up correctly, you should see a message showing how to connect to the application in your console—and your browser should automatically pull it up for you!

images/chp.frontend/create-react-app.png

Now that we’re sure things work, let’s edit our App.js and build out a basic menu list. If you’re unfamiliar with React, don’t worry. We’re not going to get fancy and build out a complex set of components or styles.

At the moment, this is what the file looks like:

 import​ React, { Component } ​from​ ​'react'​;
 import​ logo ​from​ ​'./logo.svg'​;
 import​ ​'./App.css'​;
 
 class​ App ​extends​ Component {
  render() {
 return​ (
  <div className=​"App"​>
  <header className=​"App-header"​>
  <img src=​{​logo​}​ className=​"App-logo"​ alt=​"logo"​ />
  <h1 className=​"App-title"​>Welcome to React</h1>
  </header>
  <p className=​"App-intro"​>
  To get started, edit <code>src/App.js</code> and save to reload.
  </p>
  </div>
  );
  }
 }
 
 export​ ​default​ App;

The most important thing for you to understand here is that it’s defining a React component, and the return value of the component’s render() function is what will be displayed to the user. This matches up with what we’re seeing in our browser.

This Is JavaScript?

images/aside-icons/tip.png

If you’re surprised by the appearance of import, class, the shorthand for function definitions, or the raw HTML just hanging out in a .js file, this might be the first time you’ve run into modern JavaScript (ES6) and JSX.[42] It’s a brave new world!

Let’s simplify things and render a simple listing of menu items. We’ll start with some stub data, then switch it out for our real menu item data when we connect it to our API:

1: import​ React, { Component } ​from​ ​'react'​;
class​ App ​extends​ Component {
5: // Retrieves current data for menu items
get​ menuItems() {
// TODO: Replace with real data!
return​ [
{id: ​"stub-1"​, name: ​"Stub Menu Item 1"​},
10:  {id: ​"stub-2"​, name: ​"Stub Menu Item 2"​},
{id: ​"stub-3"​, name: ​"Stub Menu Item 3"​},
];
}
15:  renderMenuItem(menuItem) {
return​ (
<li key=​{​menuItem.id​}​>​{​menuItem.name​}​</li>
);
}
20: 
// Build the DOM
render() {
return​ (
<ul>
25: {​​this​.menuItems.map(menuItem => ​this​.renderMenuItem(menuItem))​}
</ul>
);
}
30: }
export​ ​default​ App;

We’ve changed the render() to build an unordered list, taking the data that menuItems() provides and passing it to renderMenuItem() to build each list item.

The key attribute you see on line 17 is required because it helps React keep track of list item identity across changes to our data, so it knows what’s been added, removed, and changed. You need to make sure the values you pass to key are unique, and that’s easy to do: our menu items will all have unique id values, and we’ve made our stub data items have the same property.

Now that we’ve rebuilt our menu list—this time on top of React—let’s look at how we’re going to integrate Apollo Client and the packages that support using it with Absinthe.

Wiring in GraphQL

We’ll start by pulling in the @absinthe/socket-apollo-link package, an officially supported Absinthe JavaScript package that’s custom-built to add support for Absinthe’s use of Phoenix WebSockets and channels. The package will pull in a few dependencies it needs.

 $ ​​yarn​​ ​​add​​ ​​@absinthe/socket-apollo-link

We’re going to create a directory, src/client, in our JavaScript application. It will contain all our GraphQL client-related configuration.

The first bit of configuration will be for the Absinthe WebSocket configuration. We’ll put it in a new file, absinthe-socket-link.js:

 import​ * ​as​ AbsintheSocket ​from​ ​"@absinthe/socket"​;
 import​ { createAbsintheSocketLink } ​from​ ​"@absinthe/socket-apollo-link"​;
 import​ { Socket ​as​ PhoenixSocket } ​from​ ​"phoenix"​;
 
 export​ ​default​ createAbsintheSocketLink(AbsintheSocket.create(
 new​ PhoenixSocket(​"ws://localhost:4000/socket"​)
 ));

Most of this file is just importing the bits it needs: the base socket definition that @absinthe/socket provides, a utility function that knows how Apollo needs the socket to behave, and the underlying Phoenix socket code.

The most important thing to get right here is our socket URL. (If you remember, our PlateSlateWeb.UserSocket is available at /socket, based on the setup we did in PlateSlateWeb.Endpoint back on Setting Up Subscriptions).

Mind Your URLs

images/aside-icons/warning.png

You’ll want to make sure that the URLs here are build-specific, supporting development, production, and any other targets you plan on using. The create-react-app boilerplate, like most React-based systems, leverages Webpack[43] to build our application, and you can use a Webpack plugin to support different build configurations.

Don’t forget about the difference between ws:// and wss://. The latter indicates a secure WebSocket connection. You should probably only be using the insecure variant, ws://, in development.

Okay, so we have our WebSocket link configuration ready. Now let’s add some basic setup for Apollo Client using it, and adding Apollo’s standard in-memory caching facility. For the moment, we’ll send all our GraphQL traffic over our WebSocket connection, but we’ll build a hybrid configuration (sending query and mutation operations over HTTP) next.

First, let’s pull in two new dependencies:

 $ ​​yarn​​ ​​add​​ ​​apollo-client
 $ ​​yarn​​ ​​add​​ ​​apollo-cache-inmemory

Here’s the client configuration. It’s even more simplistic than the socket’s:

 import​ ApolloClient ​from​ ​"apollo-client"​;
 import​ { InMemoryCache } ​from​ ​"apollo-cache-inmemory"​;
 
 import​ absintheSocketLink ​from​ ​"./absinthe-socket-link"​;
 
 export​ ​default​ ​new​ ApolloClient({
  link: absintheSocketLink,
  cache: ​new​ InMemoryCache()
 });

We start the file with imports. Our two new dependencies put in an appearance first, followed directly after by our WebSocket link. We then instantiate our client, providing the link and cache options. That’s all the setup we need to do to have a working GraphQL client, sending requests and receiving responses over Phoenix’s rock-solid WebSocket implementation.

Now all we need to do is make sure our user interface can get the data. It’s time to throw away the menu item stub data.

Apollo Client doesn’t know anything about React directly. A specialized package, react-apollo, provides the necessary integration features. We’ll also pull in graphql-tag, used to define GraphQL documents in our application code.

 $ ​​yarn​​ ​​add​​ ​​react-apollo
 $ ​​yarn​​ ​​add​​ ​​graphql-tag

The application’s main index.js needs to make use of our brand-new GraphQL client and provide it to our React component. We make the necessary changes to the file:

 import​ React ​from​ ​'react'​;
 import​ ReactDOM ​from​ ​'react-dom'​;
 import​ ​'./index.css'​;
 import​ App ​from​ ​'./App'​;
 import​ registerServiceWorker ​from​ ​'./registerServiceWorker'​;
 
»// GraphQL
»import​ { ApolloProvider } ​from​ ​'react-apollo'​;
»import​ client ​from​ ​'./client'​;
 
 ReactDOM.render(
» <ApolloProvider client=​{​client​}​>
» <App />
» </ApolloProvider>,
  document.getElementById(​'root'​)
 );
 registerServiceWorker();

The real work happens inside the component file, where we define the GraphQL query for the menu items and use the react-apollo graphql() function to build it into a higher-order component[44] that wraps the App component:

 import​ React, { Component } ​from​ ​'react'​;
 
»// GraphQL
»import​ { graphql } ​from​ ​'react-apollo'​;
»import​ gql ​from​ ​'graphql-tag'​;
 
 class​ App ​extends​ Component {
 
 // Retrieves current data for menu items
 get​ menuItems() {
»const​ { data } = ​this​.props;
»if​ (data && data.menuItems) {
»return​ data.menuItems;
» } ​else​ {
»return​ [];
» }
  }
 
  renderMenuItem(menuItem) {
 return​ (
  <li key=​{​menuItem.id​}​>​{​menuItem.name​}​</li>
  );
  }
 
 // Build the DOM
  render() {
 return​ (
  <ul>
 {​​this​.menuItems.map(menuItem => ​this​.renderMenuItem(menuItem))​}
  </ul>
  );
  }
 
 }
 
»const​ query = gql​`
» { menuItems { id name } }
»`​;
»
»export​ ​default​ graphql(query)(App);

The nice thing about this approach is that you don’t need to deal with the GraphQL request, and the result from your API is provided to the App component automatically as React properties. In the menuItems() getter, you can see where we check to see if the data property is available and whether it has menuItems, returning them if so.

Let’s see if it works! Start up your JavaScript application again with yarn:

 $ ​​yarn​​ ​​start

If you don’t have the PlateSlate GraphQL API application still running somewhere, when the browser pops up (if you open the Developer Tools), you’ll see errors when the application attempts to connect via WebSocket:

images/chp.frontend/socket-failure.png

If everything is running as expected, the JavaScript application will connect and the Elixir application log will look something like this:

 [debug] INCOMING "doc" on "__absinthe__:control" to Absinthe.Phoenix.Channel
  Transport: Phoenix.Transports.WebSocket
  Parameter details
 [debug] ABSINTHE schema=PlateSlateWeb.Schema variables=%{}
 ---
 {
  menuItems {
  id
  name
  __typename
  }
 }
 ---
 [debug] QUERY OK source="items" db=3.7ms decode=0.1ms
 SELECT i0."id", i0."added_on", i0."description", i0."name", i0."price",
 i0."allergy_info", i0."category_id", i0."inserted_at", i0."updated_at"
 FROM "items" AS i0 ORDER BY i0."name" []

You can see the GraphQL query come across the Phoenix channel, and the Ecto query firing.

GraphQL Client-Side Caching

images/aside-icons/note.png

You may notice the addition of __typename to our GraphQL query. This is done automatically by Apollo to help facilitate client-side caching, which is done by type.

Look at the browser! As shown in the top figure, our menu items look real again!

images/chp.frontend/socket-success-html.png

Let’s dig in a little further on the browser side so we can check out exactly how we got the data.

If we open up our browser development tools, we can see the requests being sent across the WebSocket. We’re using Chrome, so the information can be found under the Network tab, then by clicking on a WebSocket request’s Frames tab. Here you see the result of our GraphQL query (with the request and channel join right before it):

images/chp.frontend/socket-success.png

It’s great to have GraphQL over WebSockets working, but we want to use normal HTTP requests for non--subscription-related operations. It’s a straightforward process to modify our GraphQL client configuration to make that work.

Using a Hybrid Configuration

Giving our GraphQL client the ability to talk HTTP/S requires us to pull in another dependency, apollo-link-http:

 $ ​​yarn​​ ​​add​​ ​​apollo-link-http

Now we’ll modify our client code and use a special function, ApolloLink.split(), to configure when each transport method should be used:

 import​ ApolloClient ​from​ ​"apollo-client"​;
 import​ { InMemoryCache } ​from​ ​"apollo-cache-inmemory"​;
»import​ { ApolloLink } ​from​ ​"apollo-link"​;
»import​ { createHttpLink } ​from​ ​"apollo-link-http"​;
»import​ { hasSubscription } ​from​ ​"@jumpn/utils-graphql"​;
 
 import​ absintheSocketLink ​from​ ​"./absinthe-socket-link"​;
 
»const​ link = ​new​ ApolloLink.split(
» operation => hasSubscription(operation.query),
» absintheSocketLink,
» createHttpLink({uri: ​"http://localhost:4000/api/graphql"​})
»);
»
»export​ ​default​ ​new​ ApolloClient({
» link,
» cache: ​new​ InMemoryCache()
»});

The hasSubscription() function, from one of @absinthe/socket’s dependencies, is a handy utility that lets us check our GraphQL for a subscription. In the event one is found, we use our WebSocket link. Otherwise, we send the request over HTTP to the configured URL.

Let’s see if this works.

After starting up our application again with yarn start (and making sure the API is still running in our other terminal), our page still displays our menu items, but this time the query happened over HTTP. In our Chrome Developer Tools panel, the request is accessible as a discrete item on the left-hand side (as graphql), and by clicking it we can preview the result as shown in the figure.

images/chp.frontend/hybrid-success-html.png

With all of this talk of subscriptions, it’s probably time to make one work with our client-side application.

Using Subscriptions

We’re going to add another subscription field to our GraphQL schema—this time so our user interface is notified when a new menu item is added. We’re not going to connect it to any of our mutations, since for this example we’re focused on just making sure the client integration works. (If you need a reminder of how Absinthe subscriptions are configured, see Chapter 6, Going Live with Subscriptions.)

 subscription ​do
 
 # Other fields
 
  field ​:new_menu_item​, ​:menu_item​ ​do
  config ​fn​ _args, _info ->
  {​:ok​, ​topic:​ ​"​​*"​}
 end
 end
 end

With our subscription in place, we just need to make some edits to our App component in the JavaScript application to support the client making the subscription and receiving its results.

At the bottom of App.js, we’ll define the subscription and add some configuration to the graphql() higher-order component to handle sending the subscription document and inserting any new menu items that are received:

1: const​ query = gql​`
{ menuItems { id name } }
`​;
5: const​ subscription = gql​`
subscription {
newMenuItem { id name }
}
`​;
10: 
export​ ​default​ graphql(query, {
props: props => {
return​ Object.assign(props, {
subscribeToNewMenuItems: params => {
15: return​ props.data.subscribeToMore({
document: subscription,
updateQuery: (prev, { subscriptionData }) => {
if​ (!subscriptionData.data) {
return​ prev;
20:  }
const​ newMenuItem = subscriptionData.data.newMenuItem;
return​ Object.assign({}, prev, {
menuItems: [newMenuItem, ...prev.menuItems]
});
25:  }
})
}
});
}
30: })(App);

We’re not going to go into depth about Apollo subscription configuration, but the most important pieces here are that we’re defining a function, subscribeToNewMenuItems(), on line 14, which uses subscribeToMore() to send our subscription—and update the components properties with updateQuery().

To create the subscription, we define a componentWillMount() function for our component. React will automatically call it for us, as it’s a special life-cycle function. It calls the subscribeToNewMenuItems() function we defined, which kicks off our subscription:

 componentWillMount() {
 this​.props.subscribeToNewMenuItems();
 }

If you have the PlateSlate GraphQL API application running, stop it and restart it using IEx so that you’ll have a console you can use to execute functions in the application:

 $ ​​iex​​ ​​-S​​ ​​mix​​ ​​phx.server

Make sure the JavaScript application is running (refresh the browser window), and then in your Elixir console type the following. We’re going to manually invoke our subscription publishing, passing it a new menu item:

 iex>​ Absinthe.Subscription.publish(
  PlateSlateWeb.Endpoint,
  %{id: "stub-new-1", name: "New Menu Item"},
  new_menu_item: "​*​"
  )

With everything in place, here’s what you’ll see in your browser window. Notice that the new menu item is displayed right at the top of the menu item listing, and the WebSocket frames shows the subscription information we just sent:

images/chp.frontend/subscription-success-html.png

Try it again if you like, substituting new menu item id and name values.

Over the last few pages, you’ve worked from a blank slate up to a working React application talking to Absinthe over HTTP and WebSockets using Apollo Client. Apollo Client is a great tool, but we’re going to take a look at a more opinionated alternative: Relay.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.116.51