React

Примечание: на данный момент хуки в React являются экспериментальными


Недавно на React Conf 2018 был представлен выпуск новых API для React, что повлекло за собой шквал дискуссий. Также на основе возможностей, ставших доступными React-разработчикам благодаря хукам, было создано множество open source библиотек.

Я узнал, что хуки, по сути, позволяют разработчикам делать две вещи, которые до создания нового API были либо намного сложнее, либо невозможны без применения дополнительной библиотеки:

  1. Использование состояния (state) в функциональных React-компонентах.
  2. Превращение одинакового поведения для разных компонентов в хук, который ранее нельзя было преобразовать в общий код.

Поскольку я много работал с GraphQL (в особенности с AWS AppSync), я сразу же подумал об использовании хуков для упрощенного создания подписок (subscriptions) GraphQL.

После того, как подписки заработали, я задался вопросом: “Что насчет запросов (queries) и мутаций (mutations)?”

Хотите верьте, хотите нет, но из-за того, что я не до конца понял, как работать с явными асинхронными вызовами с помощью хуков React, идея подписок оказалась гораздо проще для понимания, чем совместное использование мутаций и подписок.

В этой статье я расскажу вам о том, что я узнал и о способах работы с запросами, мутациями и подписками GraphQL при помощи пользовательских React-хуков.

Хуки

Мы будем работать с тремя хуками:

useEffect

Из документации: функция, переданная в useEffect, вызовется после рендеринга компонента. Эффекты можно воспринимать как переход от мира чистых функций React в мир императивный.

Если провести аналогию, хук useEffect похож по своей работе на методы жизненного цикла componentDidMount и componentDidUpdate.

useState

Из документации: возвращает значение, содержащее состояние, а также функцию для обновления состояния.

Мы будем использовать хук useState, чтобы следить за актуальностью состояния в наших функциональных компонентах.

useReducer

Из документации: является альтернативой хуку useState. Принимает reducer вида (state, action) => newState и возвращает текущее состояние вместе с методом dispatch.

Хук useReducer работает точно так же, как редьюсеры в Redux. Мы будем использовать useReducer , когда будет необходимо поддерживать состояние между несколькими частями нашего хука.

Если вы хотели бы узнать побольше о хуках, я могу посоветовать два ресурса, которые помогли мне понять, как они работают — это документация и докладРайана Флоренса на React Conf.


В своем примере я буду использовать клиент для GraphQL от AWS Amplify и AWS AppSync API, но если вы хотите повторить код по моему примеру с клиентом от Apollo, вы можете им воспользоваться совместно с подобным API при помощи следующей конфигурации:

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';

// optional, this configuration is only necessary if you're working with AWS AppSync
const middlewareLink = setContext(() => ({
  headers: {
    'X-Api-Key': 'YOUR_API_KEY'
  }
}));

const httpLink = new HttpLink({
  uri: 'YOUR_URL',
});

const link = middlewareLink.concat(httpLink);

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
});

Запросы

Обновление: в будущем React Suspense и React-cache будут иметь первоклассный метод обработки асинхронного получения данных и предоставит, пожалуй, лучший API для запросов. Здесь вы можете посмотреть рабочий пример, если вам интересны новые и нестабильные вещи 😀.

Первое, что мы рассмотрим, это выполнение запроса GraphQL. Есть два варианта того, как это сделать:

1. Запрос отправляется сразу же после рендера компонента.

2. Имеется кнопка или событие, которое инициирует запрос.

Давайте посмотрим на оба варианта.

Хук для отправки запроса после рендера компонента

import React, { useEffect, useState } from 'react'
import { API, graphqlOperation } from 'aws-amplify'

const query = `
  query {
    listTodos {
      items {
        id
        name
        description
      }
    }
  }
`

export default function() {
  const [todos, updateTodos] = useState([])

  useEffect(async() => {
    try {
      const todoData = await API.graphql(graphqlOperation(query))
      updateTodos(todoData.data.listTodos.items)
    } catch (err) {
      console.log('error: ', err)
    }
  }, [])

  return todos
}

В этом примере мы создали хук, который сразу же после вызова будет делать запрос из GraphQL API.

Мы используем хук useState для создания начального состояния, делая массив todos пустым.

При использовании хука срабатывает useEffect, делая запрос к API и обновляя массив todos. В данном случае мы используем useEffect аналогично тому, как вы могли бы использовать componentDidMount в классовом компоненте.

Наконец, хук возвращает самую последнюю версию массива todos.

Итак, как использовать этот хук? На самом деле, это довольно просто:

import useQuery from './useQuery'

const MainApp = () => {
  const todos = useQuery()
  return (
    <div>
      <h1>Hello World</h1>
      {
        todos.map((todo, i) => <p key={i}>{todo.name}</p>)
      }
    </div>
  )
}

Когда мы вызываем useQuery, возвращается массив с самыми актуальными данными нашего todo list. Затем в представлении мы проходимся по массиву todos методом map.

Отправка запроса вручную

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

import React, { useState } from 'react'
import { API, graphqlOperation } from 'aws-amplify'

const query = `
  query {
    listTodos {
      items {
        id
        name
        description
      }
    }
  }
`

export default function() {
  const [todos, updateTodos] = useState([])

  async function queryTodos() {
    try {
      const todoData = await API.graphql(graphqlOperation(query))
      updateTodos(todoData.data.listTodos.items)
    } catch (err) {
      console.log('error: ', err)
    }
  }

  return [todos, queryTodos]
}

В этом хуке у нас есть функция под названием queryTodos, которую мы будем использовать для вызова API. Основное отличие здесь в том, что мы больше не используем хук useEffect для обработки каких-либо побочных эффектов. При загрузке хука нам не нужно делать ничего, кроме установки начального состояния.

Теперь мы возвращаем массив значений вместо одного значения. Первое значение — это массив todos, а второе — вызов функции для запуска операции API.

Сейчас мы можем импортировать эти два значения из хука, когда хотим его использовать:

import useCallQuery from './useCallQuery'

const MainApp = () => {
  const [todos, queryTodos] = useCallQuery()
  return (
    <div>
      <h1>Hello World</h1>
      <button onClick={() => queryTodos()}>Query Todos</button>
      {
        todos.map((todo, i) => <p key={i}>{todo.name}</p>)
      }
    </div>
  )
}

Мутации

Теперь, когда мы знаем, как запрашивать данные, рассмотрим, как создавать мутации.

По правде говоря, для мутаций нет необходимости в хуках. Мутации могут быть созданы непосредственно в компоненте:

import { API, graphqlOperation } from 'aws-amplify'

const mutation = `
  mutation create($input: CreateTodoInput!) {
    createTodo(input: $input) {
      name
      description
    }
  }
`

const MainApp = () => {
  async function createTodo(CreateTodoInput) {
    try {
      await API.graphql(graphqlOperation(mutation, {input: CreateTodoInput}))
      console.log('successfully created todo')
    } catch (err) {
      console.log('error creating todo: ', err)
    }
  }

  const input = {
    name: "Todo from React",
    description: "Some description"
  }

  return (
    <div>
      <h1>Hello World</h1>
      {
        todos.map((todo, i) => <p key={i}>{todo.name}</p>)
      }
      <button onClick={() => createTodo(input)}>Create Todo</button>
    </div>
  )
}

Подписки

Один очень классный вариант использования хуков (и тот, который идеально вписывается в парадигму хуков) — это обработка GraphQL-подписок.

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

В этом примере мы сначала запрашиваем исходный массив todos и сохраняем его в состоянии (state) после того, как он возвращается в хуке useEffect при загрузке компонента.

Мы создадим еще один хук useEffect для создания подписки GraphQL. Подписка слушает новые создаваемые todo. Когда создается новый todo, подписка будет срабатывать, и мы будем обновлять массив todos для добавления нового todo к данным подписки.

Способ управления состоянием здесь отличается от того, когда мы использовали useState. Здесь мы используем редьюсер, задействуя хук useReducer, так как нам нужно разделить состояние между несколькими эффектами, но только чтобы подписка срабатывала при загрузке компонента. Для того, чтобы сделать это, мы будем управлять всем нашим состоянием через один редьюсер, который будет использован в обоих хуках useEffect.

const initialState = { todoList: [] }

function reducer(state, action) {
  switch (action.type) {
    case "set":
      return { todoList: action.payload }
    case "add":
      return { todoList: [...state.todoList, action.payload] }
  }
}

export function useSubscription() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(async () => {
    const todoData = await API.graphql(graphqlOperation(query))
    dispatch({ type: "set", payload: todoData.data.listTodos.items })
  }, [])

  useEffect(() => {
    const subscriber = API.graphql(graphqlOperation(subscription)).subscribe({
      next: data => {
        const {
          value: {
            data: { onCreateTodo }
          }
        } = data
        dispatch({ type: "add", payload: onCreateTodo })
      }
    });
    return () => subscriber.unsubscribe()
  }, []);

  return state.todoList
}

В приведенном выше хуке для подписки мы сначала извлекаем начальный массив todos. Как только список todo возвращается из API, мы обновляем массив todos.

Мы также создали подписку для прослушивания новых todos, когда они создаются, мы обновляем массив todos в состоянии.

import { useSubscription } from './useSubscription'

const MainApp = () => {
  const todos = useSubscription()

  return (
    <div>
      <h1>Hello World</h1>
      {
        todos.map((todo, i) => <p key={i}>{todo.name}</p>)
      }
    </div>
  )
}

В основном приложении мы импортируем todos и проходимся по нему в UI методом map.

Заключение

В этой статье не были затронуты темы кэширования, optimistic UI или работы с хранилищем Apollo.

В наших примерах работы с хуками мы использовали клиент AWS Amplify GraphQL, который еще не поддерживает кэширование, но клиент Apollo и AWS AppSync JS SDK поддерживают и могут быть использованы с аналогичным API с помощью client.query, client.mutate, & client.subscribe (см. документацию).

Перевод статьи Nader Dabit: Writing Custom React Hooks for GraphQL