React Apollo: Понимание политики выборки с помощью useQuery

В эти дни я работал над клиентским проектом Apollo. Я не привык к GraphQL, поэтому вначале мне было трудно его понять.

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

Я погуглил и узнал, что клиент apollo использует cache-first в качестве политики выборки по умолчанию. Я изменил политику выборки в проекте на no-cache, потому что я думал, что это больше подходит для проекта. После этого я обнаружил несколько ошибок с no-cache и почувствовал, что что-то пошло не так. Я подумал, что было бы неплохо узнать больше о политике выборки.

Я собираюсь рассказать о

  • Политика выборки с помощью UseQuery
  • Изменение политики выборки по умолчанию

Я подготовил простой todo graphql сервер, используя nest. В нем нет базы данных. Сервер использует только массив в качестве хранилища, и я собираюсь использовать этот сервер для следующих тестов.

Код бэкенд-сервера можно посмотреть в этом репозитории.

Я установил "@apollo/client": "3.5.8" в клиенте.

Политика выборки с useQuery

Существует шесть политик выборки, которые доступны в useQuery.

ИМЯ ОПИСАНИЕ
cache-first Apollo Client сначала выполняет запрос к кэшу. Если все запрошенные данные присутствуют в кэше, то они возвращаются. В противном случае Apollo Client выполняет запрос к вашему GraphQL-серверу и возвращает данные после их кэширования. Приоритет отдается минимизации количества сетевых запросов, отправляемых вашим приложением. Это политика выборки по умолчанию.
только кэширование Apollo Client выполняет запрос только к кэшу. В этом случае он никогда не запрашивает ваш сервер. Запрос только с кэшем выдает ошибку, если кэш не содержит данных для всех запрошенных полей.
кэш и сеть Apollo Client выполняет полный запрос как к кэшу, так и к вашему GraphQL-серверу. Запрос автоматически обновляется, если результат запроса на стороне сервера изменяет кэшированные поля. Обеспечивает быстрый ответ, а также помогает поддерживать соответствие кэшированных данных с данными сервера.
только для сети Apollo Client выполняет полный запрос к вашему GraphQL-серверу без предварительной проверки кэша. Результат запроса сохраняется в кэше. Приоритет отдается согласованности с данными сервера, но не может обеспечить практически мгновенный ответ при наличии кэшированных данных.
без кэша Аналогично только для сети, за исключением того, что результат запроса не сохраняется в кэше.
резервный Использует ту же логику, что и cache-first, за исключением того, что этот запрос не обновляется автоматически при изменении значений базовых полей. Вы можете вручную обновлять этот запрос с помощью refetch и updateQueries.

Источник: Документация Apollo

Я покажу вам, как работает каждая политика выборки.

cache-first

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

Я написал код для этого теста. Есть две кнопки. Одна используется для создания элемента todo, а другая — для показа или скрытия таблицы данных (mount и unmount). Таблица данных получает данные с помощью useQuery.

Вот код.

import { useCallback, useState } from "react";
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  useMutation,
  gql,
} from "@apollo/client";

let suffixIndex = 1;

const GET_TODOS = gql`
  query {
    getTodos {
      id
      content
      checked
    }
  }
`;

const CREATE_TODO = gql`
  mutation CreateTodo($content: String!) {
    ct1: createTodo(content: $content) {
      id
      content
      checked
    }
  }
`;

const client = new ApolloClient({
  uri: "http://localhost:3000/graphql",
  cache: new InMemoryCache(),
});

function TodosTable() {
  const { data: todosData, loading: todosLoading } = useQuery(GET_TODOS);

  if (todosLoading) return <span>Loading...</span>;

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}

const Provider = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

export default Provider;
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте посмотрим, как это работает шаг за шагом.

1. Нажмите кнопку переключения

2. Дважды нажмите кнопку создать

Вы можете увидеть созданные данные на вкладке сети.

3. Нажмите кнопку переключения дважды (Для перемонтирования компонента).

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

4. Перезагрузите вкладку и переключите таблицу.

Теперь вы можете видеть таблицу. Позвольте мне объяснить это.

При первом запросе клиент получил от сервера пустой массив, и он сохранил данные в кэше.

Я перемонтировал таблицу (шаг 3), и он нашел пустой массив в кэше, поэтому таблица все еще была пустой.

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

только кэш

Используется только кэш. Если нет кэшированных данных, то выдает ошибку.

Я переписал код для тестирования этого варианта.

function TodosTable() {
  const {
    data: todosData,
    loading: todosLoading,
    error,
  } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-only",
  });

  if (todosLoading) return <span>Loading...</span>;

  console.log({ todosData, todosLoading, error });
  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [fetchTodos] = useLazyQuery(GET_TODOS);
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleFetchTodos = useCallback(() => {
    fetchTodos();
  }, [fetchTodos]);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleFetchTodos}>
        Fetch Todos
      </button>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}
Войти в полноэкранный режим Выход из полноэкранного режима

1. Нажмите кнопку переключения

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

2. Перезагрузитесь и нажмите кнопку fetch.

Вы можете увидеть данные ответа на вкладке «Сеть».

3. Нажмите кнопку переключения.

Теперь вы можете видеть данные.

4. Нажмите кнопку создать, затем перемонтируйте (нажмите кнопку переключения дважды) таблицу.

Все остается по-прежнему. cache-only использует только кэшированные данные, как вы видели.

Если вы получите данные вручную, они также отобразятся, но что если вы получите часть данных? Как она будет отображаться?

Давайте посмотрим, как это будет выглядеть.

const GET_TODOS2 = gql`
  query {
    getTodos {
      id
      checked
    }
  }
`;

const [fetchTodos] = useLazyQuery(GET_TODOS2);
Вход в полноэкранный режим Выход из полноэкранного режима

Данные отображаются в зависимости от того, какие данные находятся в кэше.


Извините, я не заметил, что были пустые столбцы и все числа были 2. Я изменил часть кода с

<td>{todo.checked}</td>

...

const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);
Войти в полноэкранный режим Выйти из полноэкранного режима

на

<td>{todo.checked ? "checked" : "unchecked"}</td>

...

const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex}`,
      },
    });
    suffixIndex++;
  }, [createTodo]);
Войти в полноэкранный режим Выйти из полноэкранного режима

кэш-и-сеть

При такой политике сначала используются данные из кэша и выполняется запрос. Запрос автоматически обновляет данные.

Для этого теста я удалил код, который отображает текст загрузки в TodosTable.

function TodosTable() {
  const {
    data: todosData,
    error,
  } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-and-network",
  });

  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked ? "checked" : "unchecked"}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Во время загрузки компонент использовал бы данные из кэша.

Поскольку мы живем в будущем, скорость интернета мы не сможем распознать. Поэтому давайте сначала замедлим интернет до 3G, а затем начнем тест.

1. Создайте два элемента и нажмите кнопку переключения

2. Создайте два элемента и перемонтируйте стол

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

только для сети

Использует данные, поступающие с сервера, а затем обновляет кэш.

1. Нажмите кнопку переключения несколько раз.

Имеется задержка до получения запроса.

Для следующего теста, обновляет ли network-only кэш или нет, я изменил свой код следующим образом.

function TodosTable() {
  const { data: todosData, error } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-only",
  });

  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked ? "checked" : "unchecked"}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [fetchTodos] = useLazyQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleFetchTodos = useCallback(() => {
    fetchTodos();
  }, [fetchTodos]);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex}`,
      },
    });
    suffixIndex++;
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleFetchTodos}>
        Fetch Todos
      </button>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

1. Нажмите кнопку fetch, затем нажмите кнопку переключения.

В таблице отображаются данные с cache-only. Это означает, что network-only обновил кэш.

no-cache

Это похоже на network-only, но не обновляет кэш. В приведенном выше коде я изменил строку, которая является опцией ленивого запроса.

 const [fetchTodos] = useLazyQuery(GET_TODOS, {
    fetchPolicy: "no-cache",
  });
Войти в полноэкранный режим Выйдите из полноэкранного режима
  1. Нажмите кнопку fetch, затем нажмите кнопку toggle.

В таблице с cache-only ничего не отображается, потому что no-cache не обновляет кэш.

Изменение политики выборки по умолчанию

Как я уже упоминал, опцией по умолчанию в useQuery и useLazyQuery является cache-first. Если вы хотите изменить политику выборки по умолчанию, используйте defaultOptions.

const client = new ApolloClient({
  uri: "http://localhost:3000/graphql",
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "cache-only",
      errorPolicy: "ignore",
    },
    query: {
      fetchPolicy: "network-only",
      errorPolicy: "all",
    },
    mutate: {
      errorPolicy: "all",
    },
  },
});
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Было много вещей, которые я должен был узнать больше об Apollo Client. Я не понимал, почему они используют cache по умолчанию. Поэтому я установил политику выборки по умолчанию в своем проекте на no-cache. Однако при использовании no-cache у меня возникли некоторые проблемы. Одна из них заключается в том, что useQuery не использует defaultOptions. Хотя проблема была решена в коммите, кажется, что есть еще несколько проблем, связанных с no-cache. Я думал, что будет нормально использовать специфическую политику, когда это необходимо, но система кэширования apollo делает нечто большее, чем я ожидал (например, automatically update and making a rendering, refetchQueries). Я думаю, что cache может быть ключом к использованию клиента apollo, но мне придется узнать об этом больше. Я надеюсь, что этот пост поможет вам в какой-то момент. Спасибо, что прочитали этот пост.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *