diff --git a/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx b/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx
index 487a84bfb..0d203ab9b 100644
--- a/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx
+++ b/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx
@@ -1,85 +1,74 @@
-import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import React from 'react';
-import { render, screen } from '../../../../jest/test-helpers';
-import { normalizeTag } from '../../../../normalizers';
+import { __stub } from 'soapbox/api';
+
+import { queryClient, render, screen, waitFor } from '../../../../jest/test-helpers';
import TrendsPanel from '../trends-panel';
describe('', () => {
- it('renders trending hashtags', () => {
- const store = {
- trends: ImmutableRecord({
- items: ImmutableList([
- normalizeTag({
- name: 'hashtag 1',
- history: [{
- day: '1652745600',
- uses: '294',
- accounts: '180',
- }],
- }),
- ]),
- isLoading: false,
- })(),
- };
-
- render(, undefined, store);
- expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i);
- expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i);
- expect(screen.getByTestId('sparklines')).toBeInTheDocument();
+ beforeEach(() => {
+ queryClient.clear();
});
- it('renders multiple trends', () => {
- const store = {
- trends: ImmutableRecord({
- items: ImmutableList([
- normalizeTag({
- name: 'hashtag 1',
- history: ImmutableList([{ accounts: [] }]),
- }),
- normalizeTag({
- name: 'hashtag 2',
- history: ImmutableList([{ accounts: [] }]),
- }),
- ]),
- isLoading: false,
- })(),
- };
+ describe('with hashtags', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/trends')
+ .reply(200, [
+ {
+ name: 'hashtag 1',
+ url: 'https://example.com',
+ history: [{
+ day: '1652745600',
+ uses: '294',
+ accounts: '180',
+ }],
+ },
+ { name: 'hashtag 2', url: 'https://example.com' },
+ ]);
+ });
+ });
- render(, undefined, store);
- expect(screen.queryAllByTestId('hashtag')).toHaveLength(2);
+ it('renders trending hashtags', async() => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i);
+ expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i);
+ expect(screen.getByTestId('sparklines')).toBeInTheDocument();
+ });
+ });
+
+ it('renders multiple trends', async() => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryAllByTestId('hashtag')).toHaveLength(2);
+ });
+ });
+
+ it('respects the limit prop', async() => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryAllByTestId('hashtag')).toHaveLength(1);
+ });
+ });
});
- it('respects the limit prop', () => {
- const store = {
- trends: ImmutableRecord({
- items: ImmutableList([
- normalizeTag({
- name: 'hashtag 1',
- history: [{ accounts: [] }],
- }),
- normalizeTag({
- name: 'hashtag 2',
- history: [{ accounts: [] }],
- }),
- ]),
- isLoading: false,
- })(),
- };
+ describe('without hashtags', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/trends').reply(200, []);
+ });
+ });
- render(, undefined, store);
- expect(screen.queryAllByTestId('hashtag')).toHaveLength(1);
- });
+ it('renders empty', async() => {
+ render();
- it('renders empty', () => {
- const store = {
- trends: ImmutableRecord({
- items: ImmutableList([]),
- isLoading: false,
- })(),
- };
-
- render(, undefined, store);
- expect(screen.queryAllByTestId('hashtag')).toHaveLength(0);
+ await waitFor(() => {
+ expect(screen.queryAllByTestId('hashtag')).toHaveLength(0);
+ });
+ });
});
});
diff --git a/app/soapbox/features/ui/components/trends-panel.tsx b/app/soapbox/features/ui/components/trends-panel.tsx
index 49b68887d..9f582d891 100644
--- a/app/soapbox/features/ui/components/trends-panel.tsx
+++ b/app/soapbox/features/ui/components/trends-panel.tsx
@@ -1,40 +1,24 @@
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 { data: trends, isFetching } = useTrends();
- const trends = useAppSelector((state) => state.trends.items);
-
- const sortedTrends = React.useMemo(() => {
- 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) => (
+ {trends?.slice(0, limit).map((hashtag) => (
))}
diff --git a/app/soapbox/jest/test-helpers.tsx b/app/soapbox/jest/test-helpers.tsx
index 0894d4d40..721783879 100644
--- a/app/soapbox/jest/test-helpers.tsx
+++ b/app/soapbox/jest/test-helpers.tsx
@@ -37,6 +37,8 @@ const queryClient = new QueryClient({
},
defaultOptions: {
queries: {
+ staleTime: 0,
+ cacheTime: Infinity,
retry: false,
},
},
@@ -123,4 +125,5 @@ export {
rootReducer,
mockWindowProperty,
createTestStore,
+ queryClient,
};
diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts
index 15977dbb9..f38bf0dbc 100644
--- a/app/soapbox/queries/__tests__/suggestions.test.ts
+++ b/app/soapbox/queries/__tests__/suggestions.test.ts
@@ -4,7 +4,7 @@ import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import useOnboardingSuggestions from '../suggestions';
describe('useCarouselAvatars', () => {
- describe('with a successul query', () => {
+ describe('with a successful query', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet('/api/v2/suggestions')
@@ -26,7 +26,7 @@ describe('useCarouselAvatars', () => {
});
});
- describe('with an unsuccessul query', () => {
+ describe('with an unsuccessful query', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet('/api/v2/suggestions').networkError();
diff --git a/app/soapbox/queries/__tests__/trends.test.ts b/app/soapbox/queries/__tests__/trends.test.ts
new file mode 100644
index 000000000..784f928cb
--- /dev/null
+++ b/app/soapbox/queries/__tests__/trends.test.ts
@@ -0,0 +1,46 @@
+import { __stub } from 'soapbox/api';
+import { queryClient, renderHook, waitFor } from 'soapbox/jest/test-helpers';
+
+import useTrends from '../trends';
+
+describe('useTrends', () => {
+ beforeEach(() => {
+ queryClient.clear();
+ });
+
+ describe('with a successful 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 unsuccessful 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..afe780991
--- /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;
+}