如何使用 Next.js 和 Typescript 动态过滤在 API 中获取的帖子?

How to dynamically filter posts fetched in a API using Next.js and Typescript?

我正在开发一个具有过滤功能的投资组合网站。所以我目前正在努力使用 useStates、useEffect,并且不确定它们是如何工作的。我已经为此工作了 5 天,并且有 watch/read 不同的概念和 how-tos/other 的答案,但仍在努力解决如何通过 useState 来获取和过滤 api 数据。

这个想法是当用户点击 index.tsx 中的 link 时,例如 苹果、橙子或香蕉 ,网站会根据他们点击了什么,但我目前只能通过将帖子硬编码到函数 getPosts('tag:apple').

中来过滤帖子
index.tsx

import {  getPosts,  PostType } from "./api/ghostCMS"
import moment from 'moment'
//import { useState, useEffect } from 'react'

export const getServerSideProps = async ({params}) => {
    const posts = await getPosts('tag:apple')
    return {
        props: { 
            revalidate: 10, 
            posts }
    }
}

const Home: React.FC<{ posts: PostType[] }> = (props) => {
    const { posts } = props

    return (
    <div>
        <div> {/* links to filter posts */}
                <button onClick={() => getPosts('tag:apple')}>apple</button> 
                <button>orange</button>
                <button>banana</button>
            </div>

            <div className={styles.gridContainer}>
                {posts.map((post, index) => {
                    return (
                        <li className={styles.postitem} key={post.slug}>
                            <Link href="/post/[slug]" as={`/post/${post.slug}`}>
                                <a>{post.title}</a>                             
                            </Link>
                            <p>{moment(post.created_at).format('MMMM D, YYYY [●] h:mm a')}                                                       
                            </p>
                            <p>{post.custom_excerpt}</p>
                            {!!post.feature_image && <img src={post.feature_image} />}
                        </li>
                    )
                })}
            </div> {/*End of Post Container */}
        </div>

然后对于这个 ghostCMS.tsx,这是我获取 api 数据并在函数 getPost.

中添加过滤字符串的地方
./api/ghostCMS.tsx

const { BLOG_URL, CONTENT_API_KEY } = process.env

export interface PostType {
    title: string
    slug: string
    custom_excerpt: string
    feature_image: string
    created_at: string
    tag: string
} 
export async function getPosts(setFilter?: string) {
    
    if (typeof setFilter !== 'undefined' ) {
        const res = await fetch(
            `${BLOG_URL}/ghost/api/v4/content/posts/?key=${CONTENT_API_KEY}
            &fields=title,slug,custom_excerpt,feature_image,created_at,tag
            &limit=all&filter=${setFilter}`
        ).then((res) => res.json())
        const posts = res.posts
        return posts

    } else {
        const res = await fetch(
            `${BLOG_URL}/ghost/api/v4/content/posts/?key=${CONTENT_API_KEY}
            &fields=title,slug,custom_excerpt,feature_image,created_at,tag
            &limit=all`
        ).then((res) => res.json())
        const posts = res.posts
        return posts
    }
} 
export default getPosts

我不确定我的逻辑是否正确,但总而言之,我仍在学习如何根据用户希望看到的内容进行过滤。如果您有任何建议或资源,请随时告诉我并解释。抱歉,如果这听起来像是一个愚蠢的问题,请尽我最大努力学习这个投资组合网站,非常感谢您的帮助。谢谢。

已编辑: 在收到@wongz 的建议后,我将锚点更改为按钮,但出现错误:未处理的运行时错误参考错误:未定义进程

出现错误是因为我调用了错误的函数吗?我使用 getPosts(),所以我尝试用 setServerSideProps(setFilter) 替换它,但显示 服务器错误 错误:序列化从“/”中的 getServerSideProps 返回的 .posts 时出错。 原因:undefined 无法序列化为 JSON。请使用 null 或省略此值。 所以我只是恢复到只放下按钮而没有功能...

我在想是否可以将const getServerSideProps放在index.tsx的const Home里面?或者 useEffect 是可能的吗?如果是这样,我该怎么做?谢谢你的时间。

有很多东西需要改变一下。

1:从概念上将您的 index.tsx 视为主要组件,因此我删除了传递给它的参数,因为每次参数更新时它都会不断更改 Home

2:按钮应该有一个值。当您单击按钮时,它会转到 handleClick 函数。该 handleClick 函数然后通过 e?.currentTarget?.value 确定所需的水果值。问号意味着如果它不存在,整个事情将是未定义的——只是一种更安全的调用对象中的项目的方法。然后我们调用您的 getPosts 函数,传入过滤器标签,然后通过 setPosts 函数将其设置为 posts 变量。

useState 的工作方式是它为您提供值和 setter 函数来设置该变量的值。所以 posts 是变量名, setPosts 是一个函数,你可以在其中传入一个新值来确定帖子的值。

所以在这里我们将帖子设置为从 api 调用返回的内容。我们还使用 .then().catch() 语法,因为它是异步的。

根据收到数据时的外观,根据需要更改 res.data。

3:这里添加了useEffect钩子,因为我们需要在页面加载时运行发帖一次。 useEffect 中的空方括号是您放置变量的地方,只要其中一个变量发生变化,useEffect 函数就会 运行 。所以在这种情况下,我们希望它在组件加载时仅 运行 一次,因此我们将其留空,以便在组件加载时它仅 运行 一次。

我们在 useEffect 钩子中调用的是 getPosts 函数,然后我们调用 setPosts 来更新 posts 变量。然后你的 JSX 上写着 {posts && } 的地方就会是真的,这样帖子就会出现。这应该会在一秒钟左右发生。

4: 在帖子部分,你只需要在帖子可用时渲染它,否则浏览器会因为帖子未定义而出错。所以这就是需要 {posts && <div>,<div>} 的地方。

import React from 'react';
import type {PostType} from "./ghostCMS";
import { getPosts } from './ghostCMS';

const Home:React.FC = () => {
    const [posts, setPosts] = React.useState<PostType[] | null>(null);

  React.useEffect(()=> {
    getPosts().then(res=>{
      setPosts(res.data);
    }).catch(err=>console.log(err));
  },[]);

    const handleClick = (e:React.MouseEvent<HTMLButtonElement>) => {
      const fruit = e?.currentTarget.value;
      getPosts("tag:"+fruit).then(res=>setPosts(res.data)).catch(err=>console.log(err));
    }

    return (
    <div>
        <div>
            <button value="apple" onClick={(e) => handleClick(e)}>apple</button> 
            <button value="orange" onClick={(e) => handleClick(e)}>orange</button>
            <button value="banana" onClick={(e) => handleClick(e)}>banana</button>
        </div>

          {posts && <div>
              {posts?.map((post, index) => {
                  return (
                    <li key={post?.slug}>
                        <p>{post?.custom_excerpt}</p>
                    </li>
                  )
              })}
          </div>}
        </div>
    );
};

export default Home;

5: 根据你的配置,错误是因为它不知道 process 是什么。在 React 中,为了获取变量,您需要在它们前面加上 REACT_APP_。因此,在 .env 文件的变量名中添加 REACT_APP_ 并这样调用它们。除此之外,根据错误,您可能需要下载包 dotenv,然后在获取环境变量之前调用它,如下所示。所以npm install dotenv然后添加下面的代码。

import dotenv from 'dotenv';

dotenv.config();

const BLOG_URL = process.env.REACT_APP_BLOG_URL;
const CONTENT_API_KEY = process.env.REACT_APP_CONTENT_API_KEY;

// your other code

您的 .env 文件现在看起来像这样

REACT_APP_BLOG_URL="example.com"
REACT_APP_API_KEY="apiKeyExample"

基于让您的应用正常运行,我会先做#5 来解决您的问题,然后根据您自己的理解一点一点地更新其余部分。

感谢@wongz,我能够动态过滤我获取的 API 帖子。最初,我想我对使用 getStaticProps/getStaticServer 的需要感到困惑,因此我意识到我可以只调用函数 getPostssetPosts.

我开始修复 process.env,包括必须在我的变量前面添加 NEXT_PUBLIC_(而不是 REACT_APP_,因为我的项目基于 Next.js)。

NEXT_PUBLIC_CONTENT_API_KEY = apikey
NEXT_PUBLIC_BLOG_URL = example.com
index.tsx
const Home:React.FC = () => {
    const [posts, setPosts] = React.useState<PostType[] | null>(null);
    const [filter, setFilter] = React.useState('')

    // when the page loads for the first time
    React.useEffect(() => {
        getPosts().then(res=>{
            setPosts(res.posts); 
        }).catch(err=>console.log(err));
    },[]);


    const handleClick = (e:React.MouseEvent<HTMLButtonElement>) => {
        const dataFilter = e?.currentTarget.value
        getPosts("tag:"+dataFilter, page).then(res=>{
            setPosts(res.posts); 
        }).catch(err=>console.log(err));
        setFilter(dataFilter);
    }

    return (
        <div>
                <button value="apple" onClick={(e) => handleClick(e)}>apple</button> 
                <button value="orange" onClick={(e) => handleClick(e)}>orange</button>
                <button value="banana" onClick={(e) => handleClick(e)}>banana</button> 
            </div>

              {posts && <div>
              {posts?.map((p, index) => {
                  return (
                    <li key={p?.slug}>
                        <p>{p?.custom_excerpt}</p>
                    </li>
                  )
              })}
          </div>}
        </div>
    );
};

export default Home