React使用Hacker News API开发如何按标题筛选已渲染故事列表
解决方案
核心问题:你当前的实现中,所有故事详情的接口请求是分散在每个<Story>子组件内部完成的,父组件<StoriesContainer>只存储了故事ID列表,没有存储已加载的完整故事数据,自然无法拿到可用于筛选的数组。
不推荐直接操作已渲染的DOM组件做筛选,正确的实现方式是调整数据流向,把故事数据的请求和存储统一放到父组件处理,具体修改步骤如下:
步骤1:调整父组件状态
在StoriesContainer中新增三个状态:
- 已加载的完整故事数据数组
loadedStories - 搜索关键词
searchKeyword - 避免重复请求的已请求ID集合
requestedIds
步骤2:把故事详情请求逻辑迁移到父组件
监听故事ID列表、滚动加载的count变化,批量拉取对应ID的故事数据,存入loadedStories。
步骤3:实现筛选逻辑
基于已加载的故事数据,匹配标题中包含搜索关键词的项,再渲染筛选后的结果。
修改后的代码示例
1. StoriesContainer组件代码
import { Fragment, useEffect, useState, useMemo } from "react"; import { GlobalStyle } from "../styles/GlobalStyles"; import { getStoryIds, newStoriesUrl, bestStoriesUrl, showStoriesUrl, jobStoriesUrl, getStory } from "../services/Api"; import { Story } from "./Story"; import { useInfiniteScroll } from "../hooks/useInfiniteScroll"; import { StoriesContainerStyle, ContainerTitle, } from "../styles/StoriesContainer"; import { ButtonContainerStyle } from "../styles/ButtonContainer"; import { ButtonStyle } from "../styles/ButtonStyle"; export const StoriesContainer = () => { const count = useInfiniteScroll(); const [storyIds, setStoryIds] = useState([]); const [loadedStories, setLoadedStories] = useState([]); const [requestedIds, setRequestedIds] = useState(new Set()); const [searchKeyword, setSearchKeyword] = useState(""); const handleClick = (url) => (event) => { // 切换分类时重置所有状态 setStoryIds([]); setLoadedStories([]); setRequestedIds(new Set()); getStoryIds(url).then((data) => setStoryIds(data)); }; // 拉取故事详情数据 useEffect(() => { const currentIds = storyIds.slice(0, count); // 过滤未请求过的ID const needFetchIds = currentIds.filter(id => !requestedIds.has(id)); if (needFetchIds.length === 0) return; // 标记为已请求避免重复拉取 setRequestedIds(prev => { const newSet = new Set(prev); needFetchIds.forEach(id => newSet.add(id)); return newSet; }); // 批量拉取数据 needFetchIds.forEach(id => { getStory(id).then(storyData => { if (storyData?.url) { setLoadedStories(prev => [...prev, storyData]); } }) }) }, [storyIds, count, requestedIds]); useEffect(() => { getStoryIds(newStoriesUrl).then((data) => setStoryIds(data)); }, []); // 筛选逻辑:标题包含搜索关键词的故事 const filteredStories = useMemo(() => { if (!searchKeyword.trim()) return loadedStories; const keyword = searchKeyword.toLowerCase(); return loadedStories.filter(story => story.title.toLowerCase().includes(keyword)); }, [loadedStories, searchKeyword]); return ( <Fragment> <GlobalStyle /> <StoriesContainerStyle> <ContainerTitle>Hacker News</ContainerTitle> {/* 搜索输入框 */} <input type="text" placeholder="搜索故事标题" value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} style={{ margin: '12px 0', padding: '8px', width: '100%' }} /> <ButtonContainerStyle> <ButtonStyle onClick={handleClick(newStoriesUrl)}>New</ButtonStyle> <ButtonStyle onClick={handleClick(bestStoriesUrl)}>Best</ButtonStyle> <ButtonStyle onClick={handleClick(showStoriesUrl)}>Show</ButtonStyle> <ButtonStyle onClick={handleClick(jobStoriesUrl)}>Jobs</ButtonStyle> </ButtonContainerStyle> {/* 渲染筛选后的故事 */} {filteredStories.map((story) => ( <Story key={story.id} story={story} /> ))} </StoriesContainerStyle> </Fragment> ); };
2. 调整Story组件,接收父组件传入的完整故事数据,不需要再自己发请求
import { memo } from "react"; import { StoryCard, StoryDetail, StoryTitle } from "../styles/StoryStyle"; import { formatTime } from "../utils/formatTime"; export const Story = memo(function Story({ story }) { return ( <StoryCard href={story.url} target="_blank" rel="noreferrer"> <StoryTitle>{story.title}</StoryTitle> <StoryDetail> By: {story.by} | Posted: {formatTime(story.time)} </StoryDetail> </StoryCard> ); });
额外优化说明
- 用
useMemo缓存筛选结果,避免不必要的重复计算 - 新增
requestedIds集合防止同一个ID重复发起接口请求 - 切换分类时清空原有数据,避免不同分类的故事混杂
内容的提问来源于stack exchange,提问作者Hardt
相关产品推荐
相关产品推荐

