Устали от ошибок типа отладки в вашем состоянии? Хотите актуальную документацию для ваших приложений React? Читать дальше!

Когда я впервые столкнулся с TypeScript, я почувствовал приличное отчаяние: почему я должен был писать то, что казалось более шаблонным кодом? Почему при использовании его с React мне приходилось определять тип каждой отдельной опоры React, а также объекты запроса и ответа для асинхронных вызовов? И что, черт возьми, было за типы пересечений и союзов?

Однако, потратив время на работу с TypeScript, я быстро влюбился в него. Это избавляет меня от тратить время на глупые ошибки типа, обеспечивает динамическую самодокументацию и упрощает моим коллегам понимание ожиданий, установленных для кода с первого взгляда.

Мой интерес к TypeScript вырос по мере того, как я изучал различные подходы к управлению состоянием в приложениях React. Мне особенно нравится новый Context API React, который, как я считаю, может быть очень мощным, особенно в сочетании с GraphQL и TypeScript.

В октябре мое волнение переросло в доклад на митапе Boston TypeScript (для которого я теперь также являюсь соорганизатором), где я рассказал о своем подходе к управлению состоянием в приложениях React с помощью TypeScript.

Но что насчет Redux?

Но прежде чем мы перейдем ко всему этому: нам нужно вкратце поговорить о Redux. Это хорошо зарекомендовавший себя шаблон управления состоянием по умолчанию для React. Так почему бы нам просто не использовать его?

  • В зависимости от потребностей вашего приложения Redux может быть слишком сложным / тяжелым.
  • Возможно, вы не захотите использовать всю экосистему Redux, включая создателей действий, редукторы и т. Д.
  • Вы получаете тонны шаблонного кода

Что такое Context API и почему меня это волнует?

В этом году я начал использовать Context API в некоторых приложениях поверх Redux и нашел его элегантным и быстрым в реализации. Контекстный API - это обновленная версия старой концепции контекста в React, которая позволяет компонентам [s] обмениваться данными вне родительско-дочерних отношений, - метко пишет Ракшит Сорал в статье Все, что вам нужно знать о контекстном API React. . »

Мое собственное краткое определение?

Контекстный API - это способ поделиться состоянием и избежать бурения опор (начиная с React 16.3). Все это сводится к чистому и красивому способу обмена информацией в вашем приложении, не отправляя ее через компоненты, которым она не важна.

Моя диаграмма Context API может быть чрезмерно упрощена, хотя вы увидите, что на самом деле это не так уж много, кроме этого. Вы используете Provider, который является корневым источником информации для всех компонентов в дереве, и вы также используете Consumer, в обязанности которого входит получение данных или функций из Provider и передача их непосредственно компонентам, которым требуется эта информация.

Во-первых, у вас есть React.createContext, который инициализирует и передает контексту начальное значение. В этом примере из Документации по API контекста React.createContext возвращает объект с Провайдером и Потребителем.

const {Provider, Consumer} = React.createContext(defaultValue);

Provider в приведенном ниже коде - также из документации - принимает свойство value, которое представляет информацию, данные, функции и т. Д., Которые передаются через контекст.

<MyContext.Provider value={/* some value */}>

Consumer в следующем примере - опять же, из документации - обертывает функцию, которая принимает значение от поставщика и возвращает JSX в форме компонентов, которые имеют доступ к информации Provider.

<MyContext.Consumer>
   {value => /* render something based on the context value */}
</MyContext.Consumer>

Что такое GraphQL и почему меня это волнует?

GraphQL, как и React, был создан Facebook. В отличие от REST, GraphQL использует только одну единственную конечную точку, которая позволяет получать данные с помощью нескольких запросов одновременно. Это позволяет вам запрашивать только те данные, которые вам нужны, и именно тогда, когда вы этого хотите.

Как вы можете видеть выше, GraphQL также имеет встроенную систему типов, которая помогает обеспечивать самодокументацию динамического API по мере роста и развития API. Более того, вы можете генерировать статические типы для своих запросов как часть системы инструментов Apollo.

Добавление GraphQL в ваше приложение

$ npm install --save apollo-boost react-apollo graphql

Apollo Boost дает вам сразу несколько пакетов прямо из коробки.

  • apollo-client - это кэширующий клиент GraphQL, который можно использовать с React (а также с Angular и другими фреймворками).
  • apollo-cache-inmemory - это стандартный кэш в памяти, рекомендуемый для использования с apollo-client.
  • apollo-link-http просто извлекает результаты GraphQL из конечной точки GraphQL через HTTP-соединение.
  • graphql-tag экспортирует gql функцию, которая позволяет вам писать легко анализируемые строки для наших запросов и изменений.

react-apollo содержит привязки для использования apollo-client с React, а graphql - это всего лишь эталонная реализация GraphQL в Facebook.

Настройка клиента Apollo

Вот пример из Документов React Apollo:

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
   // By default, this client will send queries to the `/graphql` endpoint on the same host
   // Pass the configuration option { uri: YOUR_GRAPHQL_API_URL } to the `HttpLink` to connect to a different host
   link: new HttpLink(),
   cache: new InMemoryCache(),
});

Здесь вы импортируете ApolloClient, HttpLink и InMemoryCache. Если вы предпочитаете использовать конечную точку GraphQL, отличную от конечной точки по умолчанию, которая находится на том же хосте, что и клиент, HttpLink принимает объект конфигурации.

Это означает, например, что если вы используете микросервис, который находится на другом хосте, вы передадите пользовательский объект конфигурации для своей конечной точки GraphQL.

Затем вы помещаете корневой компонент в ApolloProvider, импортированный из react-apollo. Это дает каждому компоненту вашего приложения доступ к GraphQL через Apollo. Пример ниже также взят из документации React Apollo:

import { ApolloProvider } from 'react-apollo';
ReactDOM.render(
   <ApolloProvider client={client}>
      <MyRootComponent />
   </ApolloProvider>,
   document.getElementById('root'),
);

Создание типов с помощью Apollo

$ npm i --save apollo-codegen

Сценарии package.json, которые я предпочитаю использовать, приведены ниже:

"introspect": "apollo-codegen introspect-schema GRAPHQL_ENDPOINT --output PATH_TO_SCHEMA_FILE",
// this fetches the schema and saves it in our project
"generate": "apollo-codegen generate GLOB_PATH_TO_QUERY_FILES --schema PATH_TO_SCHEMA_FILE --target typescript --output PATH_TO_GENERATED_TYPES_FILE --add-typename --tag-name gql",
// this generates type interfaces from our schema
"typegen": "npm run introspect && npm run generate"

В моем introspect скрипте я вызываю apollo codegen introspect-schema со своей конечной точки и запрашиваю GraphQL для вывода моих файлов схемы в указанный файл.

Мой generate скрипт просматривает мой автоматически сгенерированный файл схемы, мои запросы и мутации и генерирует типы для моих запросов и мутаций.

И, наконец, мой typegen сценарий объединяет эти два вышеупомянутых сценария.

Я использую npm run typegen, и могу использовать свои типы GraphQL!

Обратите внимание, еще раз: это мой предпочтительный подход. Конечно, каждый должен свободно настраивать свои package.json сценарии так, как он считает нужным!

Демонстрационное время

На днях я выпил слишком много кофе и решил, что хочу перестроить и переименовать Amazon.

К счастью, я решил начать с малого.

Мой партнер только что переехал в Филадельфию, и у людей там есть свой жаргон для разных вещей. Как этот:

Jawn: noun, chiefly in eastern Pennsylvania, used to refer to a thing, place, person, or event that one need not or cannot give a specific name to.

My Jawn Store MVP должен в конечном итоге отобразить список продуктов с их ценами и дать мне возможность добавлять вещи в корзину. Я также смогу удалять товары из корзины и мгновенно видеть обновленную общую сумму.

Пока я объясняю, как настроить React Context с GraphQL и TypeScript в оставшейся части этой статьи, вы также можете найти полный исходный код здесь.

В качестве прототипа я использую Faker.js, отличную библиотеку для создания поддельных данных. Faker.js размещает конечную точку FakerQL, что позволяет мне получать мои поддельные данные с конечной точки GraphQL. Он предлагает мне следующие типы запросов:

  • Почта
  • Продукт
  • Пользователь
  • Сделать

Для моих целей, поскольку у меня есть магазин, я буду получать данные через FakerQL для продуктов для продажи.

В моем приложении также используются следующие технологии:

  • TypeScript с Parcel.js, сборщиком, который поддерживает TS прямо из коробки.
  • Контекстный API React

Настройка моего клиента GraphQL

В моем приложении уже установлены все необходимые зависимости Apollo, и эти сценарии включены в мой package.json:

"scripts": {
   "test": "npm run test",
   "dev": "parcel ./index.html",
   "introspect": "apollo-codegen introspect-schema https://fakerql.com/graphql --output ./data/models/index.json",
   "generate": "apollo-codegen generate ./data/**/*.ts --schema ./data/models/index.json --target typescript --output ./data/models/index.ts --add-typename --tag-name gql",
   "typegen": "npm run introspect && npm run generate",
   "build": "tsc"
}

Вы заметите использование конечной точки FakerQL и путь к папке data, в которой я автоматически создаю модели схем и настраиваю типы запросов.

А вот фактическая структура моей папки data:

- data
   - formatters
   - models
   - queries

Мои formatters - это функции для расчета цен в разных странах (уже реализованы). Когда я запускаю свой introspect скрипт, Apollo выводит схему в index.json файл в моей models папке. Все файлы в папке models будут автоматически созданы.

Когда я запускаю свой generate скрипт, Apollo просматривает мои запросы вместе со схемой конечной точки и выводит типы в файл index.ts в моей папке models.

Затем мне нужно создать экземпляр ApolloClient, чтобы я мог использовать его возможности.

// ./index.tsx
import React from "react";
import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
const client = new ApolloClient({
   link: new HttpLink({
      uri: "https://fakerql.com/graphql",
      // Remember, we only need ONE endpoint!
   }),
   cache: new InMemoryCache(),
});
class App extends React.Component {
   public render () {
      // App contents
   }
}

Как и в предыдущем примере, мы используем ApolloClient, HttpLink и InMemoryCache. Я передаю объект конфигурации URI с конечной точкой FakerQL.

Я также проверяю, что корневой компонент заключен в ApolloProvider, и поэтому все компоненты в дереве могут использовать GraphQL.

Приступим к делу: мне нужен запрос для получения всех продуктов через FakerQL. Я предпочитаю иметь файл для каждого запроса в моей папке данных.

// data/queries/JAWN_QUERY.ts
import gql from "graphql-tag";
export default gql`
   query FindJawnProducts {
   // The FakerQL docs tell me I can query "allProducts" and get a  
   list of products back
   // I'm also specifying the fields I want returned for each
   Product: id, name, price
      allProducts {
         id
         name
         price
      }
   }
`;

Здесь я использую gql, чтобы превратить мой запрос в легко читаемую строку. Когда я смотрю документы FakerQL, они говорят мне, что я могу запросить allProducts и указать указанные выше поля, среди прочего, которые должны быть возвращены для каждого продукта.

Когда я запускаю npm run typegen, создаются следующие типы:

export interface FindJawnProducts_allProducts {
   __typename: "Product";
   id: string;
   name: string;
   price: string;
}
export interface FindJawnProducts {
   allProducts: (FindJawnProducts_allProducts | null)[] | null;
}

FindJawnProducts_allProducts представляет тип отдельного проекта или элемента, а FindJawnProducts - тип массива или списка продуктов в нашем магазине. Эти типы будут полезны для настройки нашего контекста и ввода компонентов, которые в конечном итоге используют эти данные.

Прежде чем я получу наши компоненты, использующие данные из GraphQL, я останавливаюсь, чтобы спросить себя: какая еще информация мне нужна, помимо сведений о продукте, полученных из FakerQL?

Как оказалось, я хочу поддерживать два разных рынка: США и Великобританию.

Чтобы правильно рассчитывать цены на продукты, мне нужно, чтобы мои компоненты знали о моем рынке. В этом случае я передам рынок как опору корневому компоненту.

class App extends React.Component {
   public render () {
      const { market } = this.props;
      return (
         <ApolloProvider client={client}>
            <Container fluid>
               <Row>
                  <Col xs={12} sm={6}>
                     <JawnList market={market}/>
                  </Col>
                  <Col xs={12} sm={6}>
                     <Cart market={market}/>
                  </Col>
               </Row>
            </Container>
         </ApolloProvider>
      );
   }
}
const HotApp = hot(module)(App);
render(<HotApp market="US" />, document.getElementById("root"));

Но я не хочу отказываться от корневого компонента только для того, чтобы обеспечить осведомленность о моем рынке.

У меня также есть два компонента - JawnList и Cart, которым потенциально необходимо знать о продуктах, которые я получаю из своего API, но я также не хочу передавать эти данные в качестве опоры.

Мои причины? Сверление опор может стать невероятно беспорядочным по мере увеличения размера вашего приложения. Мой MVP мог бы вырасти в гораздо более крупное приложение, и я не хочу заканчивать передачу деталей через компоненты, которые не заботятся о них.

Войдите в контекстный API!

Я создаю файл с именем JawnContext.tsx, в котором я определяю и создаю свой контекст для приложения:

Вот здесь и пригодятся типы, сгенерированные Apollo. Cart будет набором продуктов FakerQL. addToCart примет продукт FakerQL в качестве аргумента и добавит его к Cart. removeFromCart будет делать именно то, на что похоже. И, наконец, market можно ввести как "US" или "UK".

Тогда React.createContext творит чудеса! (null, кстати, мое значение по умолчанию для контекста).

Затем давайте подключим мой контекст к корневому компоненту.

Вы заметите, что App набирается как JawnState - тип контекста, поскольку одним из свойств компонента является market, который я теперь хочу получить из контекста.

Вы также заметите, что я обертываю компонент JawnContext.Provider и его объект значения, который содержит значения каждого из свойств контекста - реализации addToCart и removeFromCart, market, переданного в корень, и текущее состояние тележка.

Переходя к потребителям: здесь это личное предпочтение - некоторые люди предпочитают создавать новые функции для обертывания каждого потребляющего компонента - но я хочу настроить здесь поставщика reusableWithJawnContext, чтобы я мог легко скомпоновать его с поставщиком GraphQL и потребляющим компонентом всякий раз, когда необходимый.

Здесь мой Props extension JawnState, тип контекста, а функция принимает компонент React как дочерний. Затем он возвращает дочерний элемент, обернутый JawnContext.Consumer, который распространяет в нем заданные свойства и состояние контекста.

Чтобы позволить JawnList успешно использовать мой контекст безопасным для типов способом, мне нужно определить JawnListType как дочерний элемент, который объединяет атрибуты из контекста JawnState и автоматически сгенерированного типа данных GraphQL, FindJawnProducts.

Это дает мне доступ к данным из моей конечной точки GraphQL, а также к market и addToCart из моего контекста.

Внизу приведенного выше кода вы увидите, что я создал функцию для выполнения необходимого запроса GraphQL для данных о продукте. Я составляю это с помощью withJawnContext провайдера и моего компонента. React Apollo предоставляет мнеChildDataProps, общий тип для компонента, заключенного в ApolloProvider.

Точно так же мне нужно разрешить Cart использовать контекст.

Здесь я составляю withJawnContext провайдера с Cart, набранным как JawnState, что дает мне доступ к market, cart и removeFromCart из контекста.

Вот и все! Мое приложение позволяет пользователям добавлять и удалять товары из своих тележек и просматривать обновленные общие цены, и я могу избежать пропуска информации в моем приложении. Я выигрываю!

Выводы

  • Apollo помогает нам запрашивать одну конечную точку и генерировать типы GraphQL для нашей схемы, запросов и мутаций.
  • Контекстный API, работающий вместе с TypeScript, обеспечивает безопасный и легкий способ обмена состоянием и данными без детализации свойств.

Версия этой статьи изначально была опубликована на lilydbarrett.com. Вы можете найти полный исходный код Jawn Store здесь.

Дополнительные полезные ресурсы: