I am developing a portfolio website with filtering work. so I'm currently struggling with using useStates, useEffect, and unsure with how they work. I have been working on it for the 5 days and have watch/read different concepts and how-tos/other's answers but still struggling with how to pass useState to fetch and filter api data.
The idea is when the user clicks on a link in index.tsx
like an apple, orange, or banana, the website filters posts according to what they clicked on, but I can only currently filter posts by hardcoding it into the function 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>
Then for this ghostCMS.tsx, this is where I fetch the api data and add filter strings in function 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
I'm not sure if I got my logic down right but in summary, I'm still learning how to filter according to what the user wants to see. If you have any suggestions or resources, please feel free tell me and explain. Sorry if it sounds like a dumb question, trying my best to learn as I do this portfolio website and your help is greatly appreciated. Thank you.
EDITED: After receiving advise from @wongz, I changed my anchors to buttons, but an error shows up saying: Unhandled runtime Error ReferenceError: process is not defined
Is the error showing up because I called the wrong function? I used getPosts()
, so I tried replacing it with setServerSideProps(setFilter)
but shows a Server Error
Error: Error serializing .posts
returned from getServerSideProps
in "/".
Reason: undefined
cannot be serialized as JSON. Please use null
or omit this value. so I just reverted back to just placing the buttons down without functions...
I was thinking if its possible to place the const getServerSideProps
inside the const Home
in the index.tsx? Or will useEffect be possible? If so, how can I do it properly? Thank you for your time.
There are a number of things to switch up a bit.
1: Conceptually think of your index.tsx as the main component, so I removed passing params into it because it will keep changing Home every time param updates
2: The buttons should have a value. When you click the button, it goes to a handleClick function. That handleClick function then determines which fruit value is desired via e?.currentTarget?.value
. The question marks means if it doesn't exist, the whole thing will be undefined - just a safer way to call items in objects. We then call your getPosts function, passing in the filter tag and then set it to the posts variable via the setPosts
function.
The way useState works is it provides you the value and the setter function to set the value of that variable. So posts
is the variable name and setPosts
is a function where you pass in a new value to determine the value of posts.
So here we're setting the posts as what is returned from the api call. We're also using .then().catch()
syntax because it's asynchronous.
Change the res.data as needed based on how the data looks when received.
3: The useEffect
hook is added here because we need to run posts one time when the page loads. The empty square brackets in the useEffect is where you put variables where the useEffect function will run whenever one of those variables changes. So in this case, we want it to only run once when this component loads, so we leave it empty so that it only runs once when the component loads.
What we have inside the useEffect hook is the getPosts function that is called, and then we call setPosts
to update the posts
variable. Then your JSX where it says {posts && } will be true so the posts will appear. This should happen in a second or so.
4: In the posts section, you need to only render it when posts are available otherwise the browser will error since posts is undefined. So that's where {posts && <div>,<div>}
is needed.
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: In terms of your config, the error is because it doesn't know what process
is. In React, in order to get the variables you need to have REACT_APP_ before them. So add REACT_APP_ in your variable names in your .env file and call them as such. Along with this, based on the error you'll likely need to download the package dotenv
and then calling it before you get the environment variables as per below. So npm install dotenv
and then add the code below.
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
Your .env file will now look like this
REACT_APP_BLOG_URL="example.com"
REACT_APP_API_KEY="apiKeyExample"
Based on making your app work, I would do #5 first to fix your issue and then update the rest bit by bit for your own understanding.