Jak psát znovu použitelné zdravé komponenty Preact, React nebo Vue.js založené na rozhraní API pomocí vzoru Render Props

Hodně si hraji s JavaScript SDK. A většina mých projektů využívá Contentful SDK a Contentful obsahovou infrastrukturu k vtahování obsahu od editorů a netechnických lidí do mých aplikací.

Je jedno, jestli se zabývám upravovaným obsahem nebo statistikami GitHubu, téměř každý dataset je pro mě dostupný přes API endpointy. Tyto datové sady pak jdou do mých aplikací React, Preact nebo Vue.js. Až do dneška jsem nepřišel na nejlepší způsob, jak pracovat s daty API ve světě řízeném komponentami. Ale víte co – teď už to vím .

Běžné implementace volání API

Zda používáte široce podporovaný fetch nebo SDK může být používání dat API napříč mnoha komponentami složité. Je to proto, že musíte zjistit, ve které komponentě data načítáte, jak zacházíte se stavem a jak šířit data mezi komponenty.

Zvažte následující úryvek Preact:

// Preact | app.js
import { Component, render } from "preact";
import { Item } from "./item";
import { createClient } from 'contentful'

// create Contentful SDK with needed credentials
const client = createClient({
  space: '...',
  accessToken: '...'
})

export default class App extends Component {
  componentDidMount() {
    client.getEntries({ content_type: 'tilPost', limit: 5, order: '-fields.date' })
      .then(({ items }) => this.setState({
        learnings: items
      }))
      .catch(error => this.setState({
        error
      }));
  }

  render(props, { learnings = [], posts = [] }) {
    return (
      <div>
        <h1>Preact with SDK usage example</h1>

        <h2>My Recent Learnings</h2>
        { learnings.map(item => <Item item={item} />) }
      </div>
    );
  }
}

if (typeof window !== "undefined") {
  render(<App />, document.getElementById("root"));
}

V tomto kódu App komponenta načítá data metodou životního cyklu componentDidMount . Poté nastaví data odezvy na daný stav komponenty, který bude následně použit v jeho render metoda.

Ale co se stane, když musím provést dvě volání, abych načetl data?

// Preact | app.js
export default class App extends Component {
  componentDidMount() {
    client.getEntries({ content_type: 'tilPost', limit: 5, order: '-fields.date' })
      .then(({ items }) => this.setState({
        learnings: items
      }))
      .catch(error => this.setState({
        error
      }));

    client.getEntries({ content_type: '2wKn6yEnZewu2SCCkus4as', limit: 5, order: '-fields.date' })
      .then(({ items }) => this.setState({
        posts: items
      }))
      .catch(error => this.setState({
        error
      }));
  }

  render() { /* ... */ }
}

Pokud chcete, můžete si s tímto příkladem hrát na CodeSandbox.

Nyní musím provést dva hovory pomocí getEntries Contentful SDK klient – ​​oba jsou součástí componentDidMount . Ale tento kód mi připadá trochu chaotický. A bude to horší, čím více hovorů budete muset uskutečnit.

V minulosti by mě tato situace nutila restrukturalizovat nějaký kód a abstrahovat volání API – a možná použít knihovnu pro správu stavu, jako je Redux nebo Vuex, aby byly komponenty čisté.

Nevýhodou abstrahování věcí do modelů nebo utilit je to, že zvyšuje složitost komponent. V důsledku toho nemusí být zřejmé, co se děje pro vývojáře, který se připojí k projektu. Abych porozuměl funkčnosti, musím přeskakovat mezi soubory a komponenty musí zahrnovat rostoucí počet funkcí.

Na druhou stranu použití státní správy s něčím jako Redux něco stojí. Použití správy stavu by tedy mělo být velmi dobře zváženo, protože může přidat nežádoucí složitost.

Požadovaný způsob volání API

Vždy jsem snil o magické komponentě, která vše abstrahuje a poskytuje mi data v určitém „rozsahu“. Když ve Vue.js procházíte položky, jsou uvnitř direktiv magicky dostupné objekty:

<!-- template element of Vue.js typical single file components -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.title }}
    </li>
  </ul>
</template>

Jak je vidět výše, každých item je k dispozici uvnitř smyčky li živel. Lze tento přístup použít pro zpracování volání API?

<!-- template element of Vue.js typical single file components -->
<template>
  <HttpCall :query="{ q : 'foo' } as data">
    {{ data.title }}
  </HttpCall>
</template>

Plánoval jsem se ponořit do jádra Vue.js, abych zjistil, jestli je to možné, ale pak...

Seznamte se se vzorem „renderování rekvizit“

Poslouchal jsem epizodu Fullstack Radio s Kentem C. Doddsem jako hostem. Název byl "Vytváření opakovaně použitelných komponent React s Render Props." Zajímavé – poslouchám!

To mě okamžitě zaujalo. Kent hovořil o komponentě reakce podřazení, což je opakovaně použitelná komponenta automatického dokončování. Lze jej použít k vytvoření vlastních komponent automatického dokončování.

Háček je v tom, že poskytuje funkce, jako je manipulace s klávesami a výběr položek – a jako uživatel se musím zabývat pouze poskytováním rozhraní. Musím pouze definovat strukturu značek a „udělat to hezké“, abych tak řekl.

Funguje to tak, že poskytuje svou funkčnost tím, že intenzivně využívá takzvaný vzor „renderování“. Kent vysvětlil, jak to funguje – odpovídalo mým očekáváním krásné struktury opakovaně použitelných komponent, která umožňuje sdílet funkce bez zvýšené složitosti.

Myšlenka „renderovacích rekvizit“

Vzor "render props" funguje takto:

Používám poskytnutou komponentu a předávám render funkce přes rekvizity. Toto render funkce pak bude volána uvnitř render způsob poskytované součásti. Případná volání logiky a API lze provést v "komponentě obalu" a data lze předat funkci, kterou jsem předal.

Použití vzoru „render props“ v Reactu

Protože downshift je napsán v Reactu, pojďme se podívat, jak by vzor "render props" mohl fungovat pro volání API v Reactu.

Použití rekvizity s názvem render

Nejprve musím napsat komponentu "render props" pro načítání dat z koncového bodu Contentful API.

// React | contentful.js
import React from 'react'
import PropTypes from 'prop-types'
import { createClient } from 'contentful'

const client = createClient({
  space: '...',
  accessToken: '...'
})

export default class Contentful extends React.Component {
  // make the query for the SDK 
  // and the render function required
  static propTypes = {
    query: PropTypes.object.isRequired,
    render: PropTypes.func.isRequired
  }
  
  // set default state for the data to be fetched
  // and possible errors
  constructor(...args) {
    super(...args)
    this.state = {
      error: null,
      items: [],
      query: this.props.query
    }
  }

  componentDidMount() {
    // make the API call
    client.getEntries(this.state.query)
      .then(({ items }) => this.setState({
        items
      }))
      .catch(error => this.setState({
        error
      }))
  }

  render() {
    // return and render the function
    // that was passed in via `render` prop
    return this.props.render({
      items: this.state.items,
      error: this.state.error
    })
  }
}

Úryvek výše vypadá jako spousta kódu pro pouhé volání API – ale teď mám „komponentní superschopnosti“. Jak tedy mohu vyčistit toto volání API?

// React | app.js
const App = () => (
  <div>
    <Contentful query={{ content_type: 'tilPost', limit: 5, order: '-fields.date' }} render={({ items }) => (
      <ul>
        { items.map(item => <li>{item.fields.title}</li>) }
      </ul>
    )} />
  </div>
)

S tímto příkladem si můžete hrát na CodeSandbox.

Může se zdát legrační předat anonymní funkci jako rekvizitu, ale když se na to podíváte, vypadá to velmi blízko k tomu, co jsem si představoval při volání API – obal komponenty, který skryje volání a umožní mi definovat vizuální prezentaci.

Anonymní funkce se provádí s objektem obsahujícím items které jsou součástí odpovědi API. Docela sladké!

Použití children rekvizita

Chápu, že tento vzorec může být pro některé lidi nepříjemný, protože psaní JSX uvnitř rekvizity se může zdát divné. Naštěstí existuje ještě krásnější způsob, jak to udělat. Dokumenty React popisují "renderové rekvizity" jako:

Ukázalo se, že když umístíte funkci do komponenty, tato funkce je také dostupná jako props.children . Pamatujete si následující řádky?

// React | contentful.js
export default class Contentful extends React.Component {
  /* ... */
  
  render() {
    // return and render the function
    // that was passed in via `render` prop
    return this.props.render({
      items: this.state.items,
      error: this.state.error
    })
  }
}

Mohu jej upravit tak, aby používal children prop.

// React | app.js
export default class Contentful extends React.Component {
  /* ... */
  
  render() {
    // return and render the function
    // that was passed in via `children` prop
    return this.props.children({
      items: this.state.items,
      error: this.state.error
    })
  }
}

A teď to bude ještě kouzelnější! 🎉

// React | app.js
const App = () => (
  <div>
    <Contentful query={{ content_type: 'tilPost', limit: 5, order: '-fields.date' }}>
      {({ items }) => (
        <ul>
          { items.map(item => <li>{item.fields.title}</li>) }
        </ul>
      )}
    </Contentful>
  </div>
)

Pokud chcete, můžete si s tímto příkladem hrát na CodeSandbox.

Pokud vložím jednu funkci dovnitř(!) komponenty, bude dostupná přes this.props.children komponenty obalu.

Rychlá poznámka:Pokud do komponenty umístíte několik funkcí children se stane Array.

Výše uvedený kód nyní vypadá jako 95 % toho, o čem jsem snil! (To je něco, s čím dokážu žít.)

Použití render prop pattern v Preact

Tento článek jsem začal povídáním o Preactu – je tento vzor použitelný také v jiných frameworkech než React?

Ano to je! Při použití tohoto vzoru v Preactu je pouze jeden malý rozdíl. Preact neposkytuje pohodlné funkce children být funkcí, když je k dispozici pouze jedno dítě. To znamená, že props.children je vždy pole. Ale ouha... tohle je nesmysl.

// Preact | contentful.js
export default class Contentful extends Component {
  /* ... */
  
  render(props, state) {
    return props.children[0](state);
  }
};

Pokud chcete, můžete si s tímto příkladem hrát na CodeSandbox.

Zbytek zůstává stejný. Docela pěkné!

Pomocí render vzor vrtule ve Vue.js?

Nyní jsem se zabýval React a Preact. Tak co můj miláček Vue.js? Vue.js je trochu speciální. Můžete použít JSX ve Vue.js, ale jo... každý, koho znám, píše komponenty jednoho souboru a míchá template prvky s funkcemi vykreslování JSX mi nepřijdou vhodné. Darren Jennings v tomto článku jasně popisuje, jak to může fungovat smícháním těchto dvou způsobů psaní komponent Vue.js.

Rozsahové bloky ve Vue.js

Pokud píšete hodně kódu Vue.js, možná vás napadne, jestli byste nemohli přenést myšlenku předání šablony, která do ní přenese data, do komponenty pomocí slotů. A máš pravdu! Ukázalo se, že od Vue.js v2.1 existuje možnost používat vymezené sloty, které umožňují předávat data do obsahu, který chcete vložit do slotů.

Tento princip je těžké vysvětlit bez kódu, tak se na to pojďme podívat.

<!-- Contentful.vue -->
<template>
  <div>
    <!-- define a named slot `render` and pass items into it -->
    <slot name="render" :items="items"></slot>
  </div>
</template>

<script>
import { createClient } from 'contentful'

const client = createClient({
  space: '...',
  accessToken: '...'
})

export default {
  props: {
    // make the `query` object required
    // no query no call ;)
    query: {
      type: Object,
      required: true
    }
  },

  data () {
    // set default data
    return {
      items: [],
      error: null
    }
  },

  beforeMount () {
    // make the API call using the passed in query
    // and set it to the object
    // -> it will be passed to the `render` slot
    client.getEntries(this.query)
      .then(({ items }) => {
        this.items = items;
      })
      .catch(error => this.error = error)
  }
}
</script>

Tato komponenta Contentful definuje pojmenovaný slot s názvem render a následně do něj předá daná data – items v tomto případě. Ostatní funkce jsou víceméně stejné jako v příkladech Preact a React. Komponenta zahrnuje ověření rekvizit a provádí skutečné volání API.

Vzrušující část je následující:

<!-- App.vue -->
<template>
  <div>
    <Contentful :query="{ content_type: 'tilPost', limit: 5, order: '-fields.date' }">
      <!-- set this part of the template to go into the named slot `render` -->
      <!-- make the scoped data available via `slot-scope` -->
      <ul slot="render" slot-scope="{ items }">
        <li v-for="item in items" :key="item.sys.id">
          {{ item.fields.title }}
        </li>
      </ul>
    </Contentful>
  </div>
</template>

Pokud chcete, můžete si s tímto příkladem hrát na CodeSandbox.

Nyní mohu definovat dotaz na Contentful komponentu a použít pojmenované sloty k předání mé šablony do komponenty. K načtení dat API mohu použít slot-scope atribut (podporováno od Vue.js v2.1) a vytvořit items dostupné v mé předané šabloně.

Takto se s JavaScript SDK klientem vůbec nemusím zabývat! Kvůli své výřečnosti to nevypadá tak krásně jako vzor „render props“ v React/Preact, ale výsledek je stejný – opakovaně použitelná komponenta API. 🎉

Volání API by měla být komponentami

Po více než pěti letech vytváření jednostránkových aplikací musím říci, že mě tyto vzory přesvědčily. Konečně je snadné volat v jednoduchých aplikacích podle nastavení komponent. Skryjte funkcionalitu v komponentě a dovolte mi ji udělat hezkou! To se mi líbí.

Nechápejte mě prosím špatně, správné abstrakce jsou potřeba, když máte značnou složitost, a Redux a spol. jsou skvělé nástroje pro práci s velkými aplikacemi plnými funkcí. Ale když jen já načítám nějaká data v jednoduché aplikaci takhle to pro mě teď je .

Další zdroje

Pokud ještě nejste přesvědčeni, že "renderovací rekvizity" jsou správnou cestou react-router správce Michael Jackson přednesl fantastickou přednášku na toto téma a porovnával mixiny, komponenty vyššího řádu a vzor „renderování“. Určitě se podívejte na tohle!


No