Мой друг, инвестор в rental properties, замучил меня сообщениями: «Эй, это хорошая сделка?» — и скриншотом с Zillow. После 30-го такого запроса я решил автоматизировать процесс и собрал для него property analysis dashboard. Но неожиданно главным героем проекта стала не бизнес-логика, а Tanstack Query, который сделал data layer почти невидимым.
Бекенд как proxy, фронтенд как кэширующий слой
Серверная часть — это express-прокси для Zillow API (через сторонний сервис ZillApi). На входе адрес, на выходе 300+ полей: zestimate, rent estimates, tax history. Бекенд добавляет лишь deal score — мою формулу оценки выгодности сделки.
Главная магия началась на фронтенде. Вот как выглядит базовый запрос данных о property:
function useProperty(address: string) {
return useQuery({
queryKey: ['property', address],
queryFn: () => fetch(`/api/property/${address}`).then(r => r.json()),
staleTime: 1000 * 60 * 60, // 1 hour
enabled: !!address,
});
}
Что здесь важно:
staleTimeв час — потому что zestimate обновляется раз в сутки- Кэширование по
queryKeyавтоматически дало instant navigation при повторных запросах - Друг сразу заметил: «Почему второй раз грузится мгновенно?» — хотя я даже не планировал эту фичу
Параллельные запросы без boilerplate
Настоящий вызов начался с фичи сравнения 3-4 properties. Я ожидал кошмара с управлением состоянием параллельных запросов, но useQueries решил всё:
function useCompare(addresses: string[]) {
return useQueries({
queries: addresses.map(addr => ({
queryKey: ['property', addr],
queryFn: () => fetch(`/api/property/${addr}`).then(r => r.json()),
staleTime: 1000 * 60 * 60,
})),
});
}
Что сработало идеально:
- Каждый адрес резолвится независимо
- Если property уже в кэше — показываем сразу, остальные догружаются
- Таблица сравнения рендерится прогрессивно, без кастомной логики
- Ноль усилий на loading states и error handling
Оптимистичные updates как бонус
Для сохранения избранных properties добавил useMutation с optimistic updates:
const saveMutation = useMutation({
mutationFn: (property) => fetch('/api/saved', {
method: 'POST',
body: JSON.stringify(property),
}),
onMutate: async (newProperty) => {
await queryClient.cancelQueries({ queryKey: ['saved'] });
const previous = queryClient.getQueryData(['saved']);
queryClient.setQueryData(['saved'], old => [...old, newProperty]);
return { previous };
},
onError: (err, vars, context) => {
queryClient.setQueryData(['saved'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['saved'] });
},
});
Вся логика — 15 строк вместо типичных 50+ с useReducer. При этом:
- Instant UI feedback при сохранении
- Автоматический rollback при ошибке
- Инвалидация кэша после успешного запроса
Что это дало на практике
- Скорость разработки: весь data layer уместился в 40 строк против ~100 с классическим подходом
- UX «из коробки»: кэширование, background refetch, deduplication запросов
- Лёгкий рост: когда другие инвесторы попросили доступ, добавление auth (через Clerk) не сломало логику
Где Tanstack Query не silver bullet:
- Для простых GET-запросов без кэширования может быть overkill
- Требует перехода на «менталитет ключей» вместо ручного управления состоянием
- Не заменяет state management для клиентских состояний (например, формы фильтров)
Что попробовать дальше
Если ваш проект:
- Работает с API, который возвращает «тяжёлые» данные
- Требует кэширования одинаковых запросов
- Имеет сложные сценарии типа параллельных запросов или optimistic updates
— Tanstack Query сэкономит вам десятки часов. В моём случае он превратил dashboard из «одноразового инструмента» в продукт, которым пользуются несколько инвесторов ежедневно. И всё это — без перфоманс-оптимизаций вручную.
Источник: https://www.reddit.com/r/reactjs/comments/1twnc3a/built_a_property_analysis_dashboard_for_a_friend/