You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 18:06:04