diff --git a/app/soapbox/features/ui/components/trends-panel.tsx b/app/soapbox/features/ui/components/trends-panel.tsx
index 49b68887d..ee0d8d763 100644
--- a/app/soapbox/features/ui/components/trends-panel.tsx
+++ b/app/soapbox/features/ui/components/trends-panel.tsx
@@ -1,40 +1,32 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { useDispatch } from 'react-redux';
-import { fetchTrends } from 'soapbox/actions/trends';
import Hashtag from 'soapbox/components/hashtag';
import { Widget } from 'soapbox/components/ui';
-import { useAppSelector } from 'soapbox/hooks';
+import useTrends from 'soapbox/queries/trends';
interface ITrendsPanel {
limit: number
}
const TrendsPanel = ({ limit }: ITrendsPanel) => {
- const dispatch = useDispatch();
-
- const trends = useAppSelector((state) => state.trends.items);
+ const { data: trends, isFetching } = useTrends();
const sortedTrends = React.useMemo(() => {
- return trends.sort((a, b) => {
+ return trends?.sort((a, b) => {
const num_a = Number(a.getIn(['history', 0, 'accounts']));
const num_b = Number(b.getIn(['history', 0, 'accounts']));
return num_b - num_a;
}).slice(0, limit);
}, [trends, limit]);
- React.useEffect(() => {
- dispatch(fetchTrends());
- }, []);
-
- if (sortedTrends.isEmpty()) {
+ if (trends?.length === 0 || isFetching) {
return null;
}
return (
}>
- {sortedTrends.map((hashtag) => (
+ {sortedTrends?.slice(0, limit).map((hashtag) => (
))}
diff --git a/app/soapbox/queries/__tests__/trends.test.ts b/app/soapbox/queries/__tests__/trends.test.ts
new file mode 100644
index 000000000..52f319d26
--- /dev/null
+++ b/app/soapbox/queries/__tests__/trends.test.ts
@@ -0,0 +1,42 @@
+import { __stub } from 'soapbox/api';
+import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
+
+import useTrends from '../trends';
+
+describe('useTrends', () => {
+ describe('with a successul query', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/trends')
+ .reply(200, [
+ { name: '#golf', url: 'https://example.com' },
+ { name: '#tennis', url: 'https://example.com' },
+ ]);
+ });
+ });
+
+ it('is successful', async() => {
+ const { result } = renderHook(() => useTrends());
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ expect(result.current.data?.length).toBe(2);
+ });
+ });
+
+ describe('with an unsuccessul query', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/trends').networkError();
+ });
+ });
+
+ it('is successful', async() => {
+ const { result } = renderHook(() => useTrends());
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ expect(result.current.error).toBeDefined();
+ });
+ });
+});
diff --git a/app/soapbox/queries/trends.ts b/app/soapbox/queries/trends.ts
new file mode 100644
index 000000000..8613719b0
--- /dev/null
+++ b/app/soapbox/queries/trends.ts
@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { fetchTrendsSuccess } from 'soapbox/actions/trends';
+import { useApi, useAppDispatch } from 'soapbox/hooks';
+import { normalizeTag } from 'soapbox/normalizers';
+
+import type { Tag } from 'soapbox/types/entities';
+
+export default function useTrends() {
+ const api = useApi();
+ const dispatch = useAppDispatch();
+
+ const getTrends = async() => {
+ const { data } = await api.get('/api/v1/trends');
+
+ dispatch(fetchTrendsSuccess(data));
+
+ const normalizedData = data.map((tag) => normalizeTag(tag));
+ return normalizedData;
+ };
+
+ const result = useQuery(['trends'], getTrends, {
+ placeholderData: [],
+ staleTime: 600000, // 10 minutes
+ });
+
+ return result;
+}
\ No newline at end of file