博客文章列表过滤

博客文章列表过滤

Content #

我们需要展示一个博客文章的列表,并且有一列要显示文章的分类。同时,我们还需要提供表格过滤功能,以便能够只显示某个分类的文章。

为了支持过滤功能,后端提供了两个 API:一个用于获取文章的列表,另一个用于获取所有的分类。这就需要我们在前端将文章列表返回的数据分类 ID 映射到分类的名字,以便显示在列表里。

这时候,如果按照直观的思路去实现,通常都会把逻辑都写在一个组件里,比如类似下面的代码:

function BlogList() {
  // 获取文章列表...
  // 获取分类列表...
  // 组合文章数据和分类数据...
  // 根据选择的分类过滤文章...

  // 渲染 UI ...
}

这会造成某个函数组件特别长。改变这个状况的关键仍然在于开发思路的转变。我们要真正把 Hooks 就看成普通的函数,能隔离的尽量去做隔离,从而让代码更加模块化,更易于理解和维护。

那么针对这样一个功能,我们甚至可以将其拆分成 4 个 Hooks,每一个 Hook 都尽量小,代码如下:

import React, { useEffect, useCallback, useMemo, useState } from "react";
import { Select, Table } from "antd";
import _ from "lodash";
import useAsync from "./useAsync";

const endpoint = "https://myserver.com/api/";
const useArticles = () => {
  // 使用上面创建的 useAsync 获取文章列表
  const { execute, data, loading, error } = useAsync(
    useCallback(async () => {
      const res = await fetch(`${endpoint}/posts`);
      return await res.json();
    }, []),
  );
  // 执行异步调用
  useEffect(() => execute(), [execute]);
  // 返回语义化的数据结构
  return {
    articles: data,
    articlesLoading: loading,
    articlesError: error,
  };
};
const useCategories = () => {
  // 使用上面创建的 useAsync 获取分类列表
  const { execute, data, loading, error } = useAsync(
    useCallback(async () => {
      const res = await fetch(`${endpoint}/categories`);
      return await res.json();
    }, []),
  );
  // 执行异步调用
  useEffect(() => execute(), [execute]);

  // 返回语义化的数据结构
  return {
    categories: data,
    categoriesLoading: loading,
    categoriesError: error,
  };
};
const useCombinedArticles = (articles, categories) => {
  // 将文章数据和分类数据组合到一起
  return useMemo(() => {
    // 如果没有文章或者分类数据则返回 null
    if (!articles || !categories) return null;
    return articles.map((article) => {
      return {
        ...article,
        category: categories.find(
          (c) => String(c.id) === String(article.categoryId),
        ),
      };
    });
  }, [articles, categories]);
};
const useFilteredArticles = (articles, selectedCategory) => {
  // 实现按照分类过滤
  return useMemo(() => {
    if (!articles) return null;
    if (!selectedCategory) return articles;
    return articles.filter((article) => {
      console.log("filter: ", article.categoryId, selectedCategory);
      return String(article?.category?.name) === String(selectedCategory);
    });
  }, [articles, selectedCategory]);
};

const columns = [
  { dataIndex: "title", title: "Title" },
  { dataIndex: ["category", "name"], title: "Category" },
];

export default function BlogList() {
  const [selectedCategory, setSelectedCategory] = useState(null);
  // 获取文章列表
  const { articles, articlesError } = useArticles();
  // 获取分类列表
  const { categories, categoriesError } = useCategories();
  // 组合数据
  const combined = useCombinedArticles(articles, categories);
  // 实现过滤
  const result = useFilteredArticles(combined, selectedCategory);

  // 分类下拉框选项用于过滤
  const options = useMemo(() => {
    const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
      value: c.name,
      label: c.name,
    }));
    arr.unshift({ value: null, label: "All" });
    return arr;
  }, [categories]);

  // 如果出错,简单返回 Failed
  if (articlesError || categoriesError) return "Failed";

  // 如果没有结果,说明正在加载
  if (!result) return "Loading...";

  return (
    <div>
      <Select
        value={selectedCategory}
        onChange={(value) => setSelectedCategory(value)}
        options={options}
        style={{ width: "200px" }}
        placeholder="Select a category"
      />
      <Table dataSource={result} columns={columns} />
    </div>
  );
}

通过这样的方式,我们就把一个较为复杂的逻辑拆分成一个个独立的 Hook 了,不仅隔离了业务逻辑,也让代码在语义上更加明确。比如说有 useArticles、 useCategories 这样与业务相关的名字,就非常易于理解。

虽然这个例子中抽取出来的 Hooks 都非常简单,甚至看上去没有必要。但是实际的开发场景一定是比这个复杂的,比如对于 API 返回的数据需要做一些数据的转换,进行数据的缓存,等等。那么这时就要避免把这些逻辑都放到一起,而是就要拆分到独立的 Hooks,以免产生过于复杂的组件。到时候你也就更能更体会到 Hooks 带给你的惊喜了。

Viewpoints #

From #

06|自定义Hooks :四个典型的使用场景