DEV Community

Cover image for GraphQL Components that Work in Every Framework
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ

Posted on • Originally published at apolloelements.dev

GraphQL Components that Work in Every Framework

You know how it goes. You hear about the hot new framework everyone's posting about. Sooner or later the shine fades on your lame old app's front-end stack. You start dreaming about the next project, how you'll do things differently. Or maybe you commit to The Big Rewriteโ„ข๏ธ, but it doesn't go as planned. Despairing, despondent, dejected, you wonder if there's any way to get off this churning treadmill.

Friend, let me tell you - there's a solution looking your right in the face. In fact that solution comes built in to the browser you're reading this post with. You can escape the hype cycle for good with web components.

Framework Interoperability

One of the main advantages to web components is that they are just HTML DOM objects. That means they interface with each other and the rest of the app primarily via standards like HTML attributes, DOM properties, and Events. So imagine you had a <spicy-input> element that fires a spicy-change event every time the user types a word that's for grown-ups only. If, down the line, you refactor <spicy-input> by writing it with a different custom-element base class (e.g. swapping LitElement for FASTElement) as long as the external API (attributes, properties, named slots, css custom properties or shadow parts, and events) stays the same, you don't have to change any other files.

If reading this gave you a sudden urge to implement that <spicy-input> element, then you're the hero we're waiting for, so get on it.

With web components, the library or framework you build them with is an implementation detail, and you can swap them out piecemeal without changing the rest of your app.

This also means that you can use web components inside your old-school framework, or with other web-component libraries.

Angular

<spicy-input
  (spicy-change)="onSpicyChange($event)"
  [value]="spicyValue"></spicy-input>
Enter fullscreen mode Exit fullscreen mode

Preact

return (
  <spicy-input
    onspicy-change={onSpicyChange}
    value={spicyValue}
  ></spicy-input>
);
Enter fullscreen mode Exit fullscreen mode

React

const spicyInputRef = createRef(null);
const onSpicyChange = e => setSpicyValue(e.target.value);
const [spicyValue, setSpicyValue] = useState('');
useEffect(() => spicyInputRef.current.addEventListener('spicy-change', onSpicyChange));
useEffect(() => (spicyInputRef.current.value = spicyValue), [spicyValue]);
return <spicy-input ref={spicyInputRef}></spicy-input>
Enter fullscreen mode Exit fullscreen mode

Svelte

<spicy-input
  on:spicy-change={onSpicyChange}
  value={spicyValue}></spicy-input>
Enter fullscreen mode Exit fullscreen mode

Vue

<spicy-input
  @spicy-change="onSpicyChange"
  :value="spicyValue"></spicy-input>
Enter fullscreen mode Exit fullscreen mode

TL;DR: web components work in all frameworks1, or none, since they're just HTML and the DOM.

GraphQL Meets Web Components

So interop takes the pressure off "The Big Rewriteโ„ข๏ธ" and helps us share our work across teams and apps, but web components have another super power which helps us get the job done: encapsulation.

A well-written web component keeps its insides in and its outsides out. By that, I mean that the internal implementation of the component is kept hidden while the external APIs remain stable and accessible.

In the world of GraphQL, that can mean associating a query with a component so that it's data and it's DOM structure are tightly linked. Apollo Elements lets us define query components that render their data to their private shadow DOM.

import { useQuery, html, component } from '@apollo-elements/haunted';
import { gql, TypedDocumentNode } from '@apollo/client/core';
import { PostsQuery } from './Posts.query.graphql';

function Posts({ limit, sort }) {
  const { data } = useQuery(PostsQuery, { variables: { limit, sort } });

  const posts = data?.posts ?? [];

  function onLike() {
    const { id } = event.target.closest('li');
    this.dispatchEvent(new CustomEvent('like-post', { detail: { id } }));
  }

  return html`
    <h2>Posts</h2>
    <ol>
      ${posts.map(({ id, coverImage, summary, title, url, liked }) => html`
      <li id="${id}">
        <a href="${url}"><img src="${coverImage}" role="presentation"/> ${title}</a>
        <p>${summary}</p>
        <button role="switch"
            aria-checked="${liked}"
            aria-label="toggle liked"
            @click="${onLike}">
          ${liked ? '๐Ÿ’”' : '๐Ÿ’“'}
        </button>
      </li>
      `)}
    </ol>
  `;
}

Posts.observedAttributes = ['limit', 'sort'];

customElements.define('posts-list', component(Posts));
Enter fullscreen mode Exit fullscreen mode

In this code snippet, the <posts-list> component renders a list of post summaries, where each list-item bears the id of it's post. Ordinarily, we'd be reticent to use element IDs so dynamically, but with Shadow DOM, the internals of <posts-list> are isolated from the rest of the document, so we as the author have complete control of the shadow root and can do what we like there without worrying about whether it might negatively affect the rest of the page.

And since that internal DOM is so strongly encapsulated, our colleagues on the Angular team can import it into their app with confidence that indeed nothing will go wrong, even if down the line we change the internal structure of the component.

Examples

Angular

<label>Posts per page
  <input type="number" step="10" [(ngModel)]="limit"/>
</label>

<label>Sort
  <select [(ngModel)]="sort">
    <option value="asc">Ascending</option>
    <option value="desc">Descending</option>
  </select>
</label>

<posts-list
    [limit]="limit"
    [sort]="sort"
    (like-post)="onLikePost($event)"
></posts-list>
Enter fullscreen mode Exit fullscreen mode

Preact

function PostContainer() {
  const [limit, setLimit] = useState(10);
  const [sort, setSort] = useState('asc');
  return (
    <label>Posts per page
      <input
          type="number"
          step="10"
          value={limit}
          onInput={e => setLimit(e.target.value)}/>
    </label>

    <label>Sort
      <select onInput={e => setSort(e.target.value)}>
        <option value="asc">Ascending</option>
        <option value="desc">Descending</option>
      </select>
    </label>

    <posts-list
        limit={limit}
        sort={sort}
        onlike-post={onLikePost}
    ></posts-list>
  );
}
Enter fullscreen mode Exit fullscreen mode

React

function PostContainer() {
  const postsListRef = createRef(null);
  const [limit, setLimit] = useState(10);
  const [sort, setSort] = useState('asc');
  useEffect(() => postsListRef.current.addEventListener('like-post', onLikePost));
  useEffect(() => postsListRef.current.limit = limit, [limit]);
  useEffect(() => postsListRef.current.sort = sort, [sort]);

  return (
    <label>Posts per page
      <input
          type="number"
          step="10"
          value={limit}
          onInput={e => setLimit(e.target.value)}/>
    </label>

    <label>Sort
      <select onInput={e => setSort(e.target.value)}>
        <option value="asc">Ascending</option>
        <option value="desc">Descending</option>
      </select>
    </label>

    <posts-list ref={postsListRef}></posts-list>
  );
}
Enter fullscreen mode Exit fullscreen mode

Svelte

<label>Posts per page
  <input type="number" step="10" bind:value={limit}/>
</label>

<label>Sort
  <select bind:value={sort}>
    <option value="asc">Ascending</option>
    <option value="desc">Descending</option>
  </select>
</label>

<posts-list limit={limit} sort={sort} on:like-post={onLikePost}></posts-list>
Enter fullscreen mode Exit fullscreen mode

Vue

<label>Posts per page
  <input type="number" step="10" v-model="limit"/>
</label>

<label>Sort
  <select v-model="sort">
    <option value="asc">Ascending</option>
    <option value="desc">Descending</option>
  </select>
</label>

<posts-list limit="limit" sort="sort" @like-post="onLikePost"></posts-list>
Enter fullscreen mode Exit fullscreen mode

You can write complex single-page apps with just Apollo Elements. Each component manages it's own internal state while using a templating system like lit-html or an old-school JavaScript framework to hook up properties and events, and the Apollo cache for managing client-side state more generally.

Learn More

Interested to learn more? Read our getting started guide to find out how your team can bring GraphQL to your web app one web component at a time. Or if you've already started, check out the API docs for the low-down on how components work using your favourite web component library.

So dive in, start building GraphQL and web components into your app today!

Footnotes

Top comments (0)