diff --git a/app/soapbox/features/followed_tags/index.tsx b/app/soapbox/features/followed_tags/index.tsx
new file mode 100644
index 000000000..6745f5fc0
--- /dev/null
+++ b/app/soapbox/features/followed_tags/index.tsx
@@ -0,0 +1,52 @@
+import debounce from 'lodash/debounce';
+import React, { useEffect } from 'react';
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { fetchFollowedHashtags, expandFollowedHashtags } from 'soapbox/actions/tags';
+import Hashtag from 'soapbox/components/hashtag';
+import ScrollableList from 'soapbox/components/scrollable-list';
+import { Column } from 'soapbox/components/ui';
+import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag';
+import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
+
+const messages = defineMessages({
+ heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' },
+});
+
+const handleLoadMore = debounce((dispatch) => {
+ dispatch(expandFollowedHashtags());
+}, 300, { leading: true });
+
+const FollowedTags = () => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(fetchFollowedHashtags());
+ }, []);
+
+ const tags = useAppSelector((state => state.followed_tags.items));
+ const isLoading = useAppSelector((state => state.followed_tags.isLoading));
+ const hasMore = useAppSelector((state => !!state.followed_tags.next));
+
+ const emptyMessage = ;
+
+ return (
+
+ handleLoadMore(dispatch)}
+ placeholderComponent={PlaceholderHashtag}
+ placeholderCount={5}
+ itemClassName='pb-3'
+ >
+ {tags.map(tag => )}
+
+
+ );
+};
+
+export default FollowedTags;
diff --git a/app/soapbox/reducers/followed_tags.ts b/app/soapbox/reducers/followed_tags.ts
new file mode 100644
index 000000000..4f30a3f3a
--- /dev/null
+++ b/app/soapbox/reducers/followed_tags.ts
@@ -0,0 +1,47 @@
+import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
+
+import {
+ FOLLOWED_HASHTAGS_FETCH_REQUEST,
+ FOLLOWED_HASHTAGS_FETCH_SUCCESS,
+ FOLLOWED_HASHTAGS_FETCH_FAIL,
+ FOLLOWED_HASHTAGS_EXPAND_REQUEST,
+ FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
+ FOLLOWED_HASHTAGS_EXPAND_FAIL,
+} from 'soapbox/actions/tags';
+import { normalizeTag } from 'soapbox/normalizers';
+
+import type { AnyAction } from 'redux';
+import type { APIEntity, Tag } from 'soapbox/types/entities';
+
+const ReducerRecord = ImmutableRecord({
+ items: ImmutableList(),
+ isLoading: false,
+ next: null,
+});
+
+export default function followed_tags(state = ReducerRecord(), action: AnyAction) {
+ switch (action.type) {
+ case FOLLOWED_HASHTAGS_FETCH_REQUEST:
+ return state.set('isLoading', true);
+ case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
+ return state.withMutations(map => {
+ map.set('items', ImmutableList(action.followed_tags.map((item: APIEntity) => normalizeTag(item))));
+ map.set('isLoading', false);
+ map.set('next', action.next);
+ });
+ case FOLLOWED_HASHTAGS_FETCH_FAIL:
+ return state.set('isLoading', false);
+ case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
+ return state.set('isLoading', true);
+ case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
+ return state.withMutations(map => {
+ map.update('items', list => list.concat(action.followed_tags.map((item: APIEntity) => normalizeTag(item))));
+ map.set('isLoading', false);
+ map.set('next', action.next);
+ });
+ case FOLLOWED_HASHTAGS_EXPAND_FAIL:
+ return state.set('isLoading', false);
+ default:
+ return state;
+ }
+}