不要重复你的数据 - 从代码审查中学习获取重构的代码和上述组件的解释

2025-06-09

不要复制你的数据——从代码审查中学习

获取重构的代码和上述组件的解释

正确处理数据可能很困难。我们必须从 API 获取数据,必须将其与其他来源的数据聚合,并且必须高效地转换数据以便在 UI 中使用。

在过去的几个月里,我在这个新课程中为初级开发人员进行了许多代码审查。令我惊讶的是,同一个错误反复出现。这个错误可能会导致难以调试的严重错误

这篇文章是关于数据重复及其解决方法:单一事实来源

在我解释这意味着什么之前,让我们先看一个代码示例。

重复数据

以下组件呈现从其父级接收的博客文章列表。

用户可以选择过滤器,仅显示特定日期创建的帖子。组件会相应地过滤并渲染提供的帖子。

const PostList = ({ posts }) => {
  const [selectedDay, setSelectedDay] = useState(null);
  const [filteredPosts, setFilteredPosts] = useState(posts);

  const onChangeDay = (day) => {
    setSelectedDay(day);
    const postsForDay = posts.filter(
      (post) => isSameDay(post.createdAt, day)
    );
    setFilteredPosts(postsForDay);
  };

  return (
    <Wrapper>
      <Filter
        selectedDay={selectedDay}
        onChangeDay={onChangeDay}
      />
      {
        filteredPosts.map((post) => (
          <Post key={post.id} {...post} />
        ))
      }
    </Wrapper>
  );
};
Enter fullscreen mode Exit fullscreen mode

为了实现过滤,选定的日期存储在状态变量中。在选定的日期旁边,我们找到另一个状态变量来保存已过滤的帖子。

filteredPosts数组如下所示。每当onChangeDay回调函数中所选日期发生变化时,该数组都会更新。

你可能已经意识到这种方法的问题了:filteredPostsstate 只是postsprop 的一个子集。我们复制了数组的一部分posts,因此将数据存储在两个不同的地方。

好的,确实如此。

但这里的问题是什么?

我们必须使副本与原件保持同步。

想象一下以下情况:父组件允许用户编辑帖子。用户决定将帖子的标题从“数据重复太棒了!”更改为“数据重复糟透了!”。

现在会发生什么?

  1. 父组件使用更新后的数组重新渲染posts
  2. PostList组件使用更新后的 prop 重新渲染posts

到目前为止一切顺利。但请记住组件的样子:

const PostList = ({ posts }) => {
  const [selectedDay, setSelectedDay] = useState(null);
  const [filteredPosts, setFilteredPosts] = useState(posts);

  const onChangeDay = (day) => { ... };

  return (
    <Wrapper>
      <Filter ... />
      {
        filteredPosts.map((post) => (
          <Post key={post.id} {...post} />
        ))
      }
    </Wrapper>
  );
};
Enter fullscreen mode Exit fullscreen mode

实际上显示的是数组PostList中的数据filteredPosts。这是旧版prop的子集posts

这意味着用户界面仍会显示旧帖子及其过时的标题“数据复制很棒!”

问题是我们只更新了帖子的一个版本。我们的filteredPosts数组不同步。

单一事实来源

我们的组件的更好版本是什么样的?

我们不会将数据复制到另一个状态变量中。我们会尝试只使用一个来源:postsprop。单一事实来源。

function PostList({ posts }) {
  const [selectedDay, setSelectedDay] = useState(null);
  const filteredPosts = posts.filter(
    (post) => isSameDay(post.createdAt, selectedDay)
  );

  return (
    <Wrapper>
      <Filter
        selectedDay={selectedDay}
        onChangeDay={setSelectedDay}
      />
      {
        filteredPosts.map((post) => (
          <Post key={post.id} {...post} />
        ))
      }
    </Wrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

看看我们如何摆脱状态filteredPosts用正常变量代替它

此版本更简单,不太可能引入错误。

如果你担心性能问题,那你可能是对的。如果帖子数组很长或者筛选很复杂,应用可能会很慢。

但在这种情况下,我们可以简单地使用useMemo钩子。

const filteredPosts = useMemo(() => posts.filter(
  (post) => isSameDay(post.createdAt, selectedDay)
), [posts, selectedDay]);
Enter fullscreen mode Exit fullscreen mode

useMemo钩子返回一个记忆值。提供的函数仅在依赖项发生变化时运行。

这意味着上面示例中的过滤操作仅在posts数组发生变化时才会执行。如果组件重新渲染但posts数组保持不变,useMemo则只需返回已记忆的值,无需再次执行昂贵的过滤逻辑。

运动时间

这是另一个可以通过简化而受益的例子。

function Books() {
  const [data, setData] = useState(null);
  const [books, setBooks] = useState([]);

  useEffect(() => {
    fetchData().then((data) => setData(data));
  }, []);

  useEffect(() => {
    if (!data) {
      return;
    }

    const mappedBooks = mapBooks(data);
    setBooks(mappedBooks);
  }, [data]);

  return (
    <div>
      {
        books.map((post) => (
          <div key={post.id}>{post.title}</div>
        ))
      }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

我把它留给你作为练习,让你找到问题并重构这个组件以使用单一事实来源

获取重构的代码和上述组件的解释

点击上面的链接并留下你的邮箱,即可获取我的练习答案。我还会带你浏览原始代码,并详细解释它的作用。

鏂囩珷鏉ユ簮锛�https://dev.to/jkettmann/don-t-duplicate-your-data-learnings-from-code-reviews-caj
PREV
求职之路坎坷?优秀作品集项目清单
NEXT
常见排序算法一般资源:排序的稳定性、就地与非就地、冒泡排序、选择排序、插入排序、希尔排序、快速排序、合并排序、堆排序、奖励排序 AWS 安全 LIVE!