Вступление
В современном веб-деве скорость и производительность приложений имеют решающее значение. Пользователи ожидают, что страницы будут загружаться мгновенно, а разработчики стремятся к созданию максимально отзывчивых интерфейсов. Одним из эффективных методов повышения производительности является кэширование данных.
Кэширование позволяет значительно сократить время загрузки и уменьшить нагрузку на сервер, сохраняя часто используемые данные в local storage. В данной статье мы рассмотрим один из способов кэширования в React-приложениях — используя хуки.
Внедрим технологию кэширования в существующее React приложение
Суть приложения: на странице сделаем форму, в которой пользователь впишет id покемона и будут возможны 3 варианта:
-
Покемон будет обнаружен в кэше и его компонент подгрузится моментально, используя данные из local storage
-
Покемон не будет найден в хранилище. Тогда мы отправим запрос на сервер и дадим пользователю возможность сохранить данные в кэш
-
id покемона не будет найден ни в хранилище, ни на сервере — отправим ошибку
Также, данные, которые будут хранится в кэше больше часа (или любого другого времени) будут удалены, чтобы не захламлять хранилище.
Для апи буду использовать https://pokeapi.co/ — моковый апи, который как раз подойдет для наших задач.
Также, воспользуюсь библиотекой reactuse. Это самый большой набор переиспользуемых react хуков. Используя эту библиотеку вы сможете перестать париться о дефолтном веб функционале, ведь эту работу за вас возьмут хуки из пакета. Из reactuse возьмем 3 хука: useLocalStorage, useQuery, useMount.
Ставим библиотеку:
$ npm i @siberiacancode/reactuse --save # или $ yarn add @siberiacancode/reactuse
Создадим развертку для формы с input элементом (для ввода id покемона)
import { useState } from "react"; function App() { const [inputText, setInputText] = useState(""); const [pokemonId, setPokemonId] = useState(""); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputText(event.target.value); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setPokemonId(inputText); }; return ( <div> <form onSubmit={handleSubmit}> <label>Enter id of the pokemon </label> <input onChange={handleChange} type="text" /> <button type="submit">Find Pokemon</button> </form> </div> ); }
Здесь мы используем useState, чтобы хранить значение из поля ввода в состоянии inputText. На onChange обновляем значение этого стэйта до актуального. Также создадим стэйт pokemonId, если в случае с inputText мы просто храним любую информацию, которую пользователь вводит в инпут, то здесь мы уже храним конкректный айди покемона, по которому будем отправлять запрос.
Создаем функцию handleSubmit. Она будет вызываться, когда форма будет отправлена. В ней мы вписываем, что как только форма будет подтверждена — мы запишем в pokemonId значение из нашего поля ввода. Мы используем e.preventDefault() чтобы страница не перезагружалась, как только форма будет отправлена.
Теперь создадним новый компонент Pokemon и в пропсах будем принимать id покемона
interface PokemonProps { id: string } const Pokemon = ({id}:PokemonProps) => { return <div>Pokemon!</div>; }; export default Pokemon;
Вернемся в главный компонент и вызывем Pokemon (только если стэйст pokemonId не пустой) и передадим в пропсы стэйт pokemonId.
</form> {pokemonId && <Pokemon id={pokemonId} />} ...
Теперь мы будем работать только с компонентом Pokemon. Здесь мы должны реализовать следующую логику:
-
Найти id покемона в кэше
-
в случае, если он найден — отрисовать данные из кэша
-
в случае, если его там нет — отправить запрос на сервер, получить данные, и отрисовать кнопку записи в кэш
-
-
Если прошло больше определенного времени — удалить определенного покемона из кэша
Импортируем хуки:
import { useLocalStorage, useMount, useQuery} from "@siberiacancode/reactuse";
Создадим интерфейс покемона
interface IPokemon { id: string; name: string; imageURL: string; expiresAt: number; }
Создидим константу, показывающую, через сколько секунд данные из кэша должны удалиться
const CACHE_EXPIRATION_TIME = 3600; // seconds
Используем хук useLocalStorage по ключу “pokemons”. И сразу создадим константу cachedPokemon в ней мы будем хранить данные покемона из кэша, по запрошенному айди (если он конечно там имеется)
const { value, set } = useLocalStorage<IPokemon[]>("pokemons"); const cachedPokemon = value?.find((p) => p.id === id);
Создаем запрос к апи, используя useQuery:
const { data, isLoading, isError, error } = useQuery( () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then((res) => res.json() ), { keys: [id], enabled: !cachedPokemon } );
Мы делаем запрос по айди, который берем в пропсах. Важно: в options хука указываем в keys: айди, чтобы запрос шел заново, как только обновится проп id. Также добавляем enabled: !cachedPokemon, это можно перевести так: “отправлять запрос только в том случае, если не получилось достать покемона из кэша”
Теперь делаем ряд проверок и отрисовываем ui
if (cachedPokemon) return ( <div> <h1>{cachedPokemon.name}</h1> <h3>{cachedPokemon.id}</h3> <p style={{ color: "green" }}>Loaded from cache</p> <img src={cachedPokemon.imageURL} alt={cachedPokemon.name} /> </div> );
Если получилось достать покемона из кэша, то отрисуем его (зеленым текстом допишем, что данные были взяты из хранилища, и подгружаются оптимизировано).
Проверки на ошибку и loading данных:
if (isLoading) return <h3>Fetching Pokemon...</h3>; if (isError) return <h3>Error: {error?.message}</h3>;
И в случае если не получилось достать покемона и нам пришлось делать запрос на сервер, то отрисуем компонент покемона, добавим кнопку “Save in cache”, и красным текстом отметим, что данные были взяты с сервера.
// В expiresAt прописываем текущее время (Date.now() ) + константу CACHE_EXPIRATION_TIME , помноженную на 1000 (чтобы означала секунды)
if (data && !cachedPokemon) { const fetchedPokemon: IPokemon = { id: String(data.id), name: data.name, imageURL: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${data.id}.png`, expiresAt: Date.now() + CACHE_EXPIRATION_TIME * 1000, }; return ( <div> <h1>{fetchedPokemon.name}</h1> <h3>{fetchedPokemon.id}</h3> <div> <p style={{ color: "red" }}>Loaded from server.</p> <button onClick={() => { let arrayOfPokemons = value; arrayOfPokemons?.push(fetchedPokemon); arrayOfPokemons ? set(arrayOfPokemons) : set([fetchedPokemon]); }} > Save in cache </button> </div> <img src={fetchedPokemon.imageURL} alt={data.name} /> </div> ); }
При нажатии на кнопку создадим отдельную локальную переменную ArrayOfPokemons, равную текущему значению состояния из кэша, добавим в массив запрошенного с сервера покемона (fetchedPokemon) и поместим в сторэдж этот массив, включающий и прошлое состояние из кэша, и нового покемона.
Теперь вернемся в самое начало кода, и перед объявлением useLocalStorage пропишем следующее:
useMount(() => { let arrayOfPokemons = value; arrayOfPokemons?.map((pokemon, index) => { if (pokemon.expiresAt <= Date.now()) { arrayOfPokemons?.splice(index); } }); arrayOfPokemons && set(arrayOfPokemons); });
На маунт компонента, мы будем проходиться по массиву покемонов из кэша, и если какой то из покемонов уже expired (Date.now() ≥ expiresAt), то удалим покемона из массива.
На этом все!
Демо работы приложения
Весь код
//App.tsx import { useState } from "react"; import Pokemon from "./Pokemon"; function App() { const [inputText, setInputText] = useState(""); const [pokemonId, setPokemonId] = useState(""); const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setInputText(event.target.value); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setPokemonId(inputText); }; return ( <div> <form onSubmit={handleSubmit}> <label>Enter id of the pokemon </label> <input onChange={handleChange} type="text" /> <button type="submit">Find Pokemon</button> </form> {pokemonId && <Pokemon id={pokemonId} />} </div> ); } export default App;
//Pokemon.tsx import { useLocalStorage, useMount, useQuery } from "@siberiacancode/reactuse"; interface PokemonProps { id: string; } interface IPokemon { id: string; name: string; imageURL: string; expiresAt: number; } const CACHE_EXPIRATION_TIME = 3600; // seconds const Pokemon = ({ id }: PokemonProps) => { const { value, set } = useLocalStorage<IPokemon[]>("pokemons"); useMount(() => { let arrayOfPokemons = value; arrayOfPokemons?.map((pokemon, index) => { if (pokemon.expiresAt <= Date.now()) { arrayOfPokemons?.splice(index); } }); arrayOfPokemons && set(arrayOfPokemons); }); const cachedPokemon = value?.find((p) => p.id === id); const { data, isLoading, isError, error } = useQuery( () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then((res) => res.json() ), { keys: [id], enabled: !cachedPokemon } ); if (cachedPokemon) return ( <div> <h1>{cachedPokemon.name}</h1> <h3>{cachedPokemon.id}</h3> <p style={{ color: "green" }}>Loaded from cache</p> <img src={cachedPokemon.imageURL} alt={cachedPokemon.name} /> </div> ); if (isLoading) return <h3>Fetching Pokemon...</h3>; if (isError) return <h3>Error: {error?.message}</h3>; if (data && !cachedPokemon) { const fetchedPokemon: IPokemon = { id: String(data.id), name: data.name, imageURL: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${data.id}.png`, expiresAt: Date.now() + CACHE_EXPIRATION_TIME * 1000, }; return ( <div> <h1>{fetchedPokemon.name}</h1> <h3>{fetchedPokemon.id}</h3> <div> <p style={{ color: "red" }}>Loaded from server.</p> <button onClick={() => { let arrayOfPokemons = value; arrayOfPokemons?.push(fetchedPokemon); arrayOfPokemons ? set(arrayOfPokemons) : set([fetchedPokemon]); }} > Save in cache </button> </div> <img src={fetchedPokemon.imageURL} alt={data.name} /> </div> ); } return <div>Sorry...</div>; }; export default Pokemon;
reactuse ссылки
ссылка на оригинал статьи https://habr.com/ru/articles/828996/
Добавить комментарий