Введение

Технология GraphQL, в отличие от стандартной REST API, позволяет делать запросы на сервер на по множеству ендпоинтов, а по одному и для получения и изменения данных используются query и mutations. Попробуем создать простое приложение для демонстрации как это работает.

Cоздаем проект

yarn create react-app react-apollo-hasura --template typescript

Пока создается проект заходим на сайт hasura и создаем новый проект.

Создаем простую таблицу с телефонами. Добавляем поля id, name, image, description и price.

Переходим в закладку api.

Там есть ссылка на нашу апишку и хедеры, которые необходимы для доступа к ней.

Добавляем apollo client в проект

yarn add @apollo/client

Создадим файл createApolloClient.ts куда вписыаем

import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
} from "@apollo/client";

export const createApolloClient = () => {
  return new ApolloClient({
    link: new HttpLink({
      uri: "https://living-osprey-95.hasura.app/v1/graphql",
      headers: {
        'content-type': 'application/json',
        'x-hasura-admin-secret': `************`,
      },
    }),
    cache: new InMemoryCache(),
  });
};

туда мы пишем нашу ссылку на апишку и хеддеры.

Для UI я использовал charka.io.

Создаем папку Components где добавим файл types.ts

export type GoodType = {
  id: number
  name: string
  price: number
  image: string
  description: string | null
}

export type GoodTypeFromQuery = {
  id?: number
  name?: string
  price?: number
  image?: string
  description?: string | null 
}

А также файл queries.ts

import { gql } from "@apollo/client";
import { GoodType } from "./types";

export const getGoodsQuery = (fields: Array<keyof GoodType>) => {
  return gql`
    query {
      goods {
        ${fields.join(',\n')}
      }
    }
  `
}

В файле App.ts добавим ApolloProvider а также стейт ключей, которые мы сможем изменять, чтобы продемонстрировать работу GraphQL

import React, { useCallback, useState } from "react";
import { createApolloClient } from "./createApolloClient";
import { ApolloProvider } from "@apollo/client";
import { Box, Heading, Stack } from "@chakra-ui/react";
import Goods from "./Components/Goods";
import { GoodType } from "./Components/types";
import Control from "./Components/Control";

function App() {
  const [client] = useState(createApolloClient());

  const keys: Array<keyof GoodType> = [
    "id",
    "name",
    "price",
    "image",
    "description",
  ];

  const [fields, setFields] = useState<Array<keyof GoodType>>(keys);

  const setField = useCallback(
    ({ key, active }: { key: keyof GoodType; active: boolean }) => {
      if (active) {
        if (!fields.includes(key)) {
          setFields([...fields, key]);
        }
      } else {
        setFields(fields.filter((el) => el !== key));
      }
    },
    [fields, setFields]
  );

  return (
    <ApolloProvider client={client}>
      <Box m={[5, 5]}>
        <Heading>HASURA GRAPHQL EXAMPLE</Heading>
      </Box>
      <Box m={[20, 5]}>
        {keys.map((el, idx) => (
          <Control
            key={idx}
            _key={el}
            isActive={fields.includes(el)}
            setField={setField}
          />
        ))}
      </Box>
      <Stack direction="column">
        <Goods fields={fields} />
      </Stack>
    </ApolloProvider>
  );
}

export default App;

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

import { useQuery } from "@apollo/client";
import { getGoodsQuery } from "./queries";
import { CircularProgress, Stack, Text, StackDivider } from "@chakra-ui/react";
import { GoodType, GoodTypeFromQuery } from "./types";
import Good from "./Good";
import { Fragment, useEffect } from "react";

interface Props {
  fields: Array<keyof GoodType>;
}

export default function Goods({ fields }: Props) {
  const { loading, error, data, refetch } = useQuery<{ goods : GoodTypeFromQuery[]}>(
    getGoodsQuery(fields)
  );

  useEffect(() => {
    refetch()
  }, [fields])

  console.log(fields);

  return (
    <Stack
      direction="column"
      m={[10, 10]}
      divider={<StackDivider borderColor="gray.300" />}
    >
      {loading && <CircularProgress isIndeterminate color="green.300" />}
      {error && (
        <Text fontSize={"40px"} color="tomato">
          {error.message}
        </Text>
      )}

      {data && (
        <Fragment>
          {data.goods.map((el) => (
            <Good key={el.id} data={el} />
          ))}
        </Fragment>
      )}
    </Stack>
  );
}

Компонент одного товара Good.tsx

import { Center, Image, Text, VStack } from "@chakra-ui/react";
import { getPriceString } from "../helpers/getPriceString";
import { GoodTypeFromQuery } from "./types";

export default function Good({ data }: { data: GoodTypeFromQuery }) {
  return (
    <Center border={"1px solid gray"} marginBottom={10} p={[10, 10]}>
      {data.name && <Text fontWeight={700} marginRight={10}>
        {data.name}
      </Text>}
      {data.image && <Image src={data.image} w="50px" h="50px" m={[5, 5]}/>}
      {data.price && <Text m={[5, 5]}>{getPriceString(data.price)}</Text>}
      {data.description && <Text>{data.description}</Text>}
    </Center>
  );
}

Control.tsx

import { Checkbox } from "@chakra-ui/react";
import { GoodType } from "./types";

export default function Control({
  setField,
  _key,
  isActive,
}: {
  setField: (data: { key: keyof GoodType; active: boolean }) => void;
  isActive: boolean;
  _key: keyof GoodType;
}) {

  return (
    <Checkbox
      colorScheme={"green"}
      checked={isActive}
      defaultChecked
      onChange={() => {
        setField({ key:_key, active: !isActive });
      }}
      m={[2, 2]}
    >
      {_key}
    </Checkbox>
  );
}

Весь фокус в том, что изначально мы получаем все поля товаров.

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

Вот и вся наука. GraphQL делает работу с апишкой приятной и удобной, а apollo/client под капотом делает все грязную работу за нас.