Skip to content

Latest commit

 

History

History
1806 lines (1485 loc) · 34.5 KB

File metadata and controls

1806 lines (1485 loc) · 34.5 KB

import { Head, Notes } from 'mdx-deck'; import { Appear, Image } from '@mdx-deck/components'; import { Split } from '@mdx-deck/layouts'; import { CodeSurferLayout } from 'code-surfer';

import Iframe from './components/Iframe'; import Hero from './components/Hero'; import TeaserText from './components/TeaserText'; import TeaserImage from './components/TeaserImage';

import Switcher from './components/Switcher'; import SwitchSteps from './components/SwitchSteps'; import { SwitchRow, SettingsList } from './components/SwitchHelpers';

import Phone from './components/Phone'; import PhoneWithContent from './components/PhoneWithContent'; import TabletWithContent from './components/TabletWithContent'; import LaptopWithContent from './components/LaptopWithContent';

import IconGrid from './components/IconGrid'; import DecoupleLine from './components/DecoupleLine'; import Database from './components/Database'; import BusinessLogic from './components/BusinessLogic'; import API from './components/API'; import ABTest from './components/ABTest'; import Render from './components/Render'; import Components from './components/Components'; import DataFetching from './components/DataFetching'; import FeatureFlag from './components/FeatureFlag';

import Center from './components/layout/Center'; import Jumbo from './components/layout/Jumbo'; import SideBySide from './components/layout/SideBySide'; import TopCenter from './components/TopCenter';

import { SlideIn, BlurIn, ZoomSteps } from './components/lib'; import config from './config';

export { themes } from './theme';

import './global.css';

<title>Components as Data, by Luke Herrington</title>

Hi! 👋🏻 I'm Luke

<img style={{ borderRadius: '50%', height: '50%' }} src="https://lukeherrington.com/static/cdf656371789fe7d89f21fc88d7dffb2/fdbb0/luke-square-sm.png" />


Components as Data

A Cross Platform GraphQL Powered Component API


Hero

Image

Teaser


Components Win

<TabletWithContent size={0} style={{ position: 'absolute', top: '60%', left: '35%', transform: 'translate(-35%, -60%)' }} /> <LaptopWithContent size={0} style={{ position: 'absolute', top: '60%', left: '80%', transform: 'translate(-80%, -60%)' }} /> <PhoneWithContent size={0} style={{ position: 'absolute', top: '70%', left: '50%', transform: 'translate(-50%, -70%)' }} />


component = render(props + state);

Props

Read-only external data

State

Local data that changes over time

Render

Declarative representation of UI


<Switch />


Components Compose


<Phone screenTitle="Settings" style={{ position: 'absolute', top: '-20%', left: '50%', transform: 'translate(-50%, 20%)', }} size="2"

<SwitchRow style={{ gridColumn: '1/4', gridRow: '1/2', width: '100%' }}> <SwitchRow style={{ gridColumn: '1/4', gridRow: '2/3', width: '100%' }}> <SwitchRow style={{ gridColumn: '1/4', gridRow: '3/4', width: '100%' }}> <SwitchRow style={{ gridColumn: '1/4', gridRow: '4/5', width: '100%' }}> <SwitchRow style={{ gridColumn: '1/4', gridRow: '5/6', width: '100%' }}>


I ❤️ Components



Decoupling

Who What When Where Why How


Where? 🤔


The Decouple Line

<DecoupleLine steps={['MID_LEFT', 'LEFT', 'RIGHT', 'MID_RIGHT']} />



REST™



More than just "resources"


HATEOAS

Hypermedia as the Engine of Application State


{
  "links": {
    "self": "https://api.test.com/list?page[number]=1&page[size]=100",
    "next": "https://api.test.com/list?page[number]=2&page[size]=100",
    "prev": "https://api.test.com/list?page[number]=0&page[size]=100",
    "last": "https://api.test.com/list?page[number]=33&page[size]=100",
    "first": "https://api.test.com/list?page[number]=0&page[size]=100"
  }
}


Microservices:

Now you have two problems



Wait.. What?

🧐


/get-off-soapbox

Components as Data

<API style={{ height: '40%'}} />


{'{...}'}

{'{...}'}

{'{...}'}


{
  "component": "Hero",
  "data": {
    "title": "Welcome!",
    "backgroundImage": "https://via.placeholder.com/700x400",
    "subtitle": "Make yourself at home"
  }
}
{
  "component": "Hero",
  "data": {
    "title": "Welcome!",
    "backgroundImage": "https://via.placeholder.com/700x400",
    "subtitle": "Make yourself at home"
  }
}
{
  "component": "Hero",
  "data": {
    "title": "Welcome!",
    "backgroundImage": "https://via.placeholder.com/700x400",
    "subtitle": "Make yourself at home"
  }
}

<Iframe fallback={

} src="https://nbc.com" title="NBC.com" ></Iframe>

Slideshow

Slide

Shelf

SeriesTile

VideoTile


Why GraphQL

Types

Queries


interface Component {
  component: String!
  treatment: String
}

type VideoTile implements Component {
  component: String!
  treatment: String
  data: VideoData
}

type VideoData {
  image: String!
  title: String!
  secondaryTitle: String!
  percentViewed: Float
}

type SeriesTile implements Component {
  component: String!
  treatment: String
  data: SeriesData
}

type SeriesData {
  image: String!
  title: String!
  secondaryTitle: String!
  favoriteId: String
}

union Tile = VideoTile | SeriesTile

type ShelfData {
  title: String
  items: [Tile]
}

interface Section {
  component: String!
  treatment: String
}

type Shelf implements Section & Component {
  component: String!
  treatment: String
  data: ShelfData
}

interface Featured {
  component: String!
  treatment: String
}

type SlideshowData {
  items: [Slide]
}

type Slideshow implements Featured & Component {
  component: String!
  treatment: String
  data: SlideshowData
}

type SlideData {
  image: String!
  title: String!
  secondaryTitle: String
}

type Slide {
  component: String!
  treatment: String
  data: SlideData
}

type Page implements Component {
  id: ID!
  treatment: String
  component: String!
  name: String!
  featured: Featured
  sections: [Section]!
}

enum SupportedPlatforms {
  iOS
  tvOS
  roku
  rokuStick
  samsungTv
  android
  androidTv
  xboxOne
  web
  mobileWeb
  fireTablet
  fireTv
  fireTvStick
  vizio
}

type Query {
  page(
    id: ID
    name: String!
    userId: String!
    platform: SupportedPlatforms!
  ): Page
}
interface Component {
  component: String!
  treatment: String
}

type VideoTile implements Component {
  component: String!
  treatment: String
  data: VideoData
}

type VideoData {
  image: String!
  title: String!
  secondaryTitle: String!
  percentViewed: Float
}

type SeriesTile implements Component {
  component: String!
  treatment: String
  data: SeriesData
}

type SeriesData {
  image: String!
  title: String!
  secondaryTitle: String!
  favoriteId: String
}

union Tile = VideoTile | SeriesTile

type ShelfData {
  title: String
  items: [Tile]
}

interface Section {
  component: String!
  treatment: String
}

type Shelf implements Section & Component {
  component: String!
  treatment: String
  data: ShelfData
}

interface Featured {
  component: String!
  treatment: String
}

type SlideshowData {
  items: [Slide]
}

type Slideshow implements Featured & Component {
  component: String!
  treatment: String
  data: SlideshowData
}

type SlideData {
  image: String!
  title: String!
  secondaryTitle: String
}

type Slide {
  component: String!
  treatment: String
  data: SlideData
}

type Page implements Component {
  id: ID!
  treatment: String
  component: String!
  name: String!
  featured: Featured
  sections: [Section]!
}

enum SupportedPlatforms {
  iOS
  tvOS
  roku
  rokuStick
  samsungTv
  android
  androidTv
  xboxOne
  web
  mobileWeb
  fireTablet
  fireTv
  fireTvStick
  vizio
}

type Query {
  page(
    id: ID
    name: String!
    userId: String!
    platform: SupportedPlatforms!
  ): Page
}
interface Component {
  component: String!
  treatment: String
}

type VideoTile implements Component {
  component: String!
  treatment: String
  data: VideoData
}

type VideoData {
  image: String!
  title: String!
  secondaryTitle: String!
  percentViewed: Float
}

type SeriesTile implements Component {
  component: String!
  treatment: String
  data: SeriesData
}

type SeriesData {
  image: String!
  title: String!
  secondaryTitle: String!
  favoriteId: String
}

union Tile = VideoTile | SeriesTile

type ShelfData {
  title: String
  items: [Tile]
}

interface Section {
  component: String!
  treatment: String
}

type Shelf implements Section & Component {
  component: String!
  treatment: String
  data: ShelfData
}

interface Featured {
  component: String!
  treatment: String
}

type SlideshowData {
  items: [Slide]
}

type Slideshow implements Featured & Component {
  component: String!
  treatment: String
  data: SlideshowData
}

type SlideData {
  image: String!
  title: String!
  secondaryTitle: String
}

type Slide {
  component: String!
  treatment: String
  data: SlideData
}

type Page implements Component {
  id: ID!
  treatment: String
  component: String!
  name: String!
  featured: Featured
  sections: [Section]!
}

enum SupportedPlatforms {
  iOS
  tvOS
  roku
  rokuStick
  samsungTv
  android
  androidTv
  xboxOne
  web
  mobileWeb
  fireTablet
  fireTv
  fireTvStick
  vizio
}

type Query {
  page(
    id: ID
    name: String!
    userId: String!
    platform: SupportedPlatforms!
  ): Page
}
interface Component {
  component: String!
  treatment: String
}

type VideoTile implements Component {
  component: String!
  treatment: String
  data: VideoData
}

type VideoData {
  image: String!
  title: String!
  secondaryTitle: String!
  percentViewed: Float
}

type SeriesTile implements Component {
  component: String!
  treatment: String
  data: SeriesData
}

type SeriesData {
  image: String!
  title: String!
  secondaryTitle: String!
  favoriteId: String
}

union Tile = VideoTile | SeriesTile

type ShelfData {
  title: String
  items: [Tile]
}

interface Section {
  component: String!
  treatment: String
}

type Shelf implements Section & Component {
  component: String!
  treatment: String
  data: ShelfData
}

interface Featured {
  component: String!
  treatment: String
}

type SlideshowData {
  items: [Slide]
}

type Slideshow implements Featured & Component {
  component: String!
  treatment: String
  data: SlideshowData
}

type SlideData {
  image: String!
  title: String!
  secondaryTitle: String
}

type Slide {
  component: String!
  treatment: String
  data: SlideData
}

type Page implements Component {
  id: ID!
  treatment: String
  component: String!
  name: String!
  featured: Featured
  sections: [Section]!
}

enum SupportedPlatforms {
  iOS
  tvOS
  roku
  rokuStick
  samsungTv
  android
  androidTv
  xboxOne
  web
  mobileWeb
  fireTablet
  fireTv
  fireTvStick
  vizio
}

type Query {
  page(
    id: ID
    name: String!
    userId: String!
    platform: SupportedPlatforms!
  ): Page
}
interface Component {
  component: String!
  treatment: String
}

type VideoTile implements Component {
  component: String!
  treatment: String
  data: VideoData
}

type VideoData {
  image: String!
  title: String!
  secondaryTitle: String!
  percentViewed: Float
}

type SeriesTile implements Component {
  component: String!
  treatment: String
  data: SeriesData
}

type SeriesData {
  image: String!
  title: String!
  secondaryTitle: String!
  favoriteId: String
}

union Tile = VideoTile | SeriesTile

type ShelfData {
  title: String
  items: [Tile]
}

interface Section {
  component: String!
  treatment: String
}

type Shelf implements Section & Component {
  component: String!
  treatment: String
  data: ShelfData
}

interface Featured {
  component: String!
  treatment: String
}

type SlideshowData {
  items: [Slide]
}

type Slideshow implements Featured & Component {
  component: String!
  treatment: String
  data: SlideshowData
}

type SlideData {
  image: String!
  title: String!
  secondaryTitle: String
}

type Slide {
  component: String!
  treatment: String
  data: SlideData
}

type Page implements Component {
  id: ID!
  treatment: String
  component: String!
  name: String!
  featured: Featured
  sections: [Section]!
}

type Query {
  page(
    id: ID
    name: String!
    userId: String!
    platform: SupportedPlatforms!
  ): Page
}

<Iframe fallback={

} src={`${config.baseURL}/.netlify/functions/graphql`} title="GraphQL Playground" ></Iframe>

Design Schema


<Iframe fallback={

} src={`${config.baseURL}/.netlify/functions/voyager`} title="GraphQL Voyager" ></Iframe>

Sample Implementation


import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...tile, slideNumber })
      )}
    </div>
  );
}

function SeriesTile(props) {
  return (
    <div className="seriesTile">
      <Link to={`/shows/${props.urlAlias}`}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}
import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...tile, slideNumber })
      )}
    </div>
  );
}

function SeriesTile(props) {
  return (
    <div className="seriesTile">
      <Link to={`/shows/${props.urlAlias}`}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}
import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...tile, slideNumber })
      )}
    </div>
  );
}

function SeriesTile(props) {
  const a = `/shows/${props.urlAlias}`;
  return (
    <div className="seriesTile">
      <Link to={a}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}
import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, tile)
      )}
    </div>
  );
}

function SeriesTile(props) {
  const a = `/shows/${props.urlAlias}`;
  return (
    <div className="seriesTile">
      <Link to={a}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}
import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, tile)
      )}
    </div>
  );
}

function SeriesTile(props) {
  const a = `/shows/${props.urlAlias}`;
  return (
    <div className="seriesTile">
      <Link to={a}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}
import React from 'react';

function Slideshow(props) {
  // Slideshow manages its own slide number state
  const slideNumber = useTimedCounter(props.items.length);
  return (
    <div className="slideshow">
      {props.items.map(slide =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, { ...slide, slideNumber })
      )}
    </div>
  );
}

function Slide(props) {
  return (
    <div className="slide">
      <img alt={props.title} src={props.image} />
      <h1>{props.title}</h1>
    </div>
  );
}

function Shelf(props) {
  return (
    <div className="shelf">
      {props.items.map(tile =>
        // Render children with the current slideNumber and the API data
        renderComponent(COMPONENTS_DICTIONARY, tile)
      )}
    </div>
  );
}

function SeriesTile(props) {
  const a = `/shows/${props.urlAlias}`;
  return (
    <div className="seriesTile">
      <Link to={a}>
        <img alt={props.title} src={props.image} />
      </Link>
      <p>{props.title}</p>
    </div>
  );
}

/**
 * These dictionaries match our GraphQL interfaces
 */

const FEATURED_DICTIONARY = {
  Slideshow,
};

const COMPONENTS_DICTIONARY = {
  Slide,
  SeriesTile,
};

const SECTIONS_DICTIONARY = {
  Shelf,
};

export function renderComponent(DICTIONARY, { component, data }) {
  const Component = DICTIONARY[component];
  if (!Component) {
    return null;
  }
  return <Component {...data} />;
}

export default function App() {
  const [loading, data] = useComponentAPIFetch();
  if (loading) {
    return 'loading...';
  }
  return (
    <div>
      <section className="featured">
        {renderComponent(FEATURED_DICTIONARY, data.featured)}
      </section>
      <section className="sections">
        {data.sections.map(section =>
          renderComponent(SECTIONS_DICTIONARY, section)
        )}
      </section>
    </div>
  );
}

The Hard Parts

Fragmented Business logic

Variance across platforms

Pagination/Lazy Loading


The Good Parts

Simpler, presentational frontends

Shared business logic

Shared A/B Testing and Feature Flagging

Design consistency via a “Design Schema”

Query optimization


Think outside the box


Thanks! 👋🏻

GitHub: infiniteluke


Resources


Resources Continued