[Перевод] Как получить данные React Clots?

React.js

Оригинальная ссылка:Woohoo. Робин спрашивает IE о допуске. /react-hooks…

В этом уроке я хочу показать вам, как получать данные с помощью React Hooks, используя обработчики состояний и эффектов. Я буду использовать API Hacker News для получения актуальных технических статей. Вы создадите собственный хук для повторного использования в любом месте вашей программы или опубликуете его как пакет npm.

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

Примечание. В будущем React Hooks не будут использоваться для выборки данных в React, об этом позаботится функция под названием Suspense. Но следующий учебник все же позволит вам узнать больше о хуках состояний и эффектов в React.

Используйте React Cloots для получения данных

Если вы новичок, чтобы получить данные в реакции, вы можете проверить, мой другой.它将会引导你通过使用React的class组件来获取数据,并且还可以和render propsиликомпоненты более высокого порядкаиспользуются вместе и в сочетании с обработкой ошибок и состоянием загрузки. В этом посте я буду использовать React Hooks в функциональном компоненте, чтобы продемонстрировать эти возможности.

import React, { useState } from 'react';

function App() {
  const [data, setData] = useState({ hits: [] });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

Этот компонент приложений отображает список со многими элементами (хиты = статьи Hacker News). Функции государства и государственного обновления происходят от вызова USESTATE в Государственном крюке, который отвечает за управление локальным состоянием, который мы используем для рендеринга данных списка. Начальное состояние является пустым массивом, и государство пока не было установлено.

Мы будем использовать axios для получения данных, конечно, вы также можете использовать другие библиотеки или fetch API, если у вас не установлен axios, вы можете использовать командную строкуnpm install axiosУстановить его. Затем для достижения эффекта крюка для получения данных:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

Получите данные в useEffect через axios, затем поместите данные в локальное состояние компонента через setData и обработайте Promise через async/await.

Однако, когда вы запускаете программу, вы должны столкнуться с неприятным циклом.Хук эффекта запускается не только при монтировании компонента, но и при его обновлении.. Потому что после каждого сбора данных мы будем устанавливать состояние через setState, в это время компонент будет обновляться, а затем эффект будет запускаться снова, что приведет к тому, что данные будут собираться снова и снова. Мы просто хотим получить данные один раз при монтировании компонента, поэтому нам нужно указать пустой массив во втором параметре useEffect, чтобы сбор данных запускался только при монтировании компонента, а не при каждом обновлении.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

Второй параметр может определять переменные, от которых зависят хуки (назначаются в массиве).Если переменная изменяется, хуки будут выполняться один раз.Если это пустой массив, то хуки не будут выполняться при обновлении компонента. Потому что он не слушает никаких переменных.

Здесь есть еще одна проблема. В коде мы используем async/await для получения данных из стороннего API. Согласно документации, каждая асинхронная функция возвращает обещание. Объявление асинхронной функции определяет асинхронную функцию, которая возвращает asyncFunction. Объекты, асинхронные функции — это функции, которые работают асинхронно через цикл обработки событий, возвращая свой результат с помощью неявного промиса. Однако хук эффекта не должен ничего возвращать или очищать функцию, поэтому в консоли вы видите следующее предупреждение:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect..Вот почему запрещено использовать Async непосредственно в функции использования. Давайте реализуем его решение с помощью ASYNC-функции внутри эффекта.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

В двух словах, это получение данных с помощью React Hooks. Однако, если вы заинтересованы в обработке ошибок, подсказках при загрузке, как инициировать выборку данных из форм и как реализовать многоразовые обработчики выборки данных, продолжайте читать.

Как запустить хук программно/вручную?

Ну, как только мы получим данные в монтировании, однако, если вы используете поля ввода, чтобы сообщить API, какая тема нас интересует? «Redux» в качестве нашего запроса по умолчанию, если он включен «React»? Давайте реализуем элемент ввода, чтобы кто-то мог выйти за рамки темы «Redux». Таким образом, введение нового состояния для элемента ввода.

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

export default App;

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    ...
  );
}

export default App;

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    ...
  );
}

export default App;

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

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

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [search]);

  return (
    ...
  );
}

export default App;

Кроме того, начальное значение поиска также устанавливается равным запросу, поскольку компонент также получает данные при монтировании, поэтому результат должен отражать значение в поле ввода. Однако наличие похожих состояний запроса и поиска немного сбивает с толку. Почему бы не установить фактический URL-адрес для состояния вместо поиска?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

Это случай использования хуков эффектов для получения неявно запрограммированных данных. Вы можете решить, от какого состояния зависит эффект. Как только это состояние будет установлено в клике или другом эффекте, этот эффект будет запущен снова. В этом случае, если состояние URL-адреса изменится, эффект запустится снова, чтобы получить данные из API.

React Hooks и загрузка

Давайте представим советы по сбору данных о нагрузке. Это просто еще одно состояние, управляемое хуком состояния. загрузка Советы по загрузке используются для рендеринга сборки.

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

Как только эффект вызывается для выборки данных (что происходит, когда компонент монтируется или изменяется состояние URL-адреса), для состояния загрузки будет установлено значение true. После завершения запроса статус загрузки снова будет установлен на false.

React Hooks и обработка ошибок

Если вы добавите обработку ошибок в React Hooks, ошибка будет просто еще одним состоянием, инициализированным с помощью хука состояния. Компоненты приложения могут предоставлять обратную связь пользователю в случае возникновения ошибки. При использовании async/await блоки try/catch обычно используются для обработки ошибок. Вы можете сделать это внутри эффекта:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

Реагировать на получение данных в форме

Пока у нас есть только комбинация входов и кнопок. Как только появятся дополнительные элементы ввода, вам может понадобиться обернуть их элементом формы. Кроме того, формы также могут быть вызваны нажатием «ввода» на клавиатуре.

function App() {
  ...

  return (
    <Fragment>
      <form
        onSubmit={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

Но теперь браузер перезагружает страницу при нажатии кнопки отправки, потому что это присущее браузеру поведение при отправке формы. Чтобы предотвратить поведение по умолчанию, мы можем отменить поведение по умолчанию с помощью event.preventDefault(). Это также подход, реализованный в компонентах класса React.

function App() {
  ...

  const doFetch = () => {
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  };

  return (
    <Fragment>
      <form onSubmit={event => {
        doFetch();

        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

Теперь, когда вы нажимаете кнопку отправки, браузер больше не перезагружается. Он работает так же, как и раньше, но на этот раз с использованием формы вместо простого ввода и комбинации кнопок. Вы также можете нажать клавишу «Enter» на клавиатуре.

Пользовательский хук для сбора данных

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

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const doFetch = () => {
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  };

  return { data, isLoading, isError, doFetch };
}

Теперь вы можете использовать новый хук в компоненте приложения.

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();

  return (
    <Fragment>
      ...
    </Fragment>
  );
}

Далее пропустите статус URL снаружи функции DOFORT:

const useHackerNewsApi = () => {
  ...

  useEffect(
    ...
  );

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      ...
    </Fragment>
  );
}

Начальное состояние также может стать общим состоянием. Просто передайте его новому пользовательскому хуку:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useDataApi(
    'http://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

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

Крюк сбора данных редуктора

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

import React, {
  Fragment,
  useState,
  useEffect,
  useReducer,
} from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...
};

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

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [url]);

  ...
};

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

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return { ...state };
    case 'FETCH_SUCCESS':
      return { ...state };
    case 'FETCH_FAILURE':
      return { ...state };
    default:
      throw new Error();
  }
};

...Оператор используется для сохранения неизменного объекта состояния (это означает, что состояние никогда не меняется напрямую). Теперь давайте переопределим некоторые свойства, возвращаемые текущим состоянием, чтобы состояние менялось при каждом переходе состояния:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

Теперь каждый переход состояния (определяемый типом операции) будет возвращать новое состояние на основе предыдущего состояния и дополнительных полезных данных. Например, в случае успешного запроса полезная нагрузка используется для установки данных нового объекта состояния.

Таким образом, хук-редюсер гарантирует, что эта часть управления состоянием инкапсулирована собственной логикой. Предоставляя тип и необязательную полезную нагрузку, вы всегда окажетесь в предсказуемом состоянии. Кроме того, вы никогда не войдете в недопустимое состояние. Например, состояния isloading и isError ранее могли быть случайно установлены в значение true. Что должно отображаться в пользовательском интерфейсе в этом случае? Теперь каждый переход состояния, определенный функцией редуктора, приводит к допустимому объекту состояния.

Отключить выборку данных в ловушках эффектов

Установка состояния компонента является распространенной проблемой в реакции, даже если компонент был размонтирован (например, оставлен из-за навигации с использованием реактивного маршрутизатора). я былздесьНаписал этот вопрос, в нем описывается, как предотвратить размонтирование набора состояний сборки в различных сценариях. Давайте посмотрим, как устанавливается состояние для предотвращения данных в пользовательском хуке:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

У каждого хука эффекта есть функция очистки, которая запускается, когда компонент размонтирован. Функция очистки — это функция, возвращаемая хуком. В нашем случае мы используем логический флаг didCancel, чтобы наша логика выборки данных знала о состоянии компонента (подключен/размонтирован). Если компонент размонтирован, флаг должен быть установлен на «дерево», что приведет к сбою установки состояния компонента после окончательного асинхронного разрешения выборки.