Hey, in short:
I want to make an Article page and pass the current article to a child component called "ArticleButtons".
This is how the Article object looks like, it also contains an Author object as property, I show below some init objects I made so that I can so how the page would look like even without any data fom the backend
const initAuthor: TUser = {
id: null,
username: "author",
email: "",
password: "",
image: "",
bio: "",
followersCount: 0,
followingCount: 0,
createdAt: null,
updatedAt: null
}
const initArticle: TArticle = {
id: null,
userId: null,
title: "title",
author: initAuthor,
description: "",
body: "<h1>Hello arti</h1>",
slug: "",
tagList: [],
isFav: false,
favoritesCount: 0,
createdAt: null,
updatedAt: null
}
Now, I’m passing the current article as a state with its correspondingly setArticleState function. But when my UseEffect() function fetch data from the backend the ArticleButtons throws exception because "author is undefined, here is an screenshot of the error:console error, und 58 is the line who launchs that error, the return statement.
function ArticleButtons({ article = initArticle, setArticle } : {
article: TArticle,
setArticle: React.Dispatch<SetStateAction<TArticle>>
}) {
const { author } = article;
const { authState } = useAuth() as TAuthContext;
const { loggedUser } = authState;
const { slug } = useParams();
const checkFollow = (authorId: number | null) => {
if (!authorId) return false;
return false;
}
const handleFollow = (author: TUser) => {
setArticle((prev) => ({...prev, author}))
};
const handleFav = (article: TArticle) => {
setArticle((prev) => ({...prev, article}))
}
//>>>>line 58 is below<<<<
return loggedUser.username === author.username ? (
<AuthorButtons {...article } slug={slug}/>
) : (
<div className="article-buttons">
<FollowButton isFollowing={checkFollow(author.id) } {...author} handler={handleFollow}/>
<FavButton {...article} handleFav={handleFav}/>
</div>
)
}
export default ArticleButtons;
As you can see in the parameters, I tried givin an initial value of initArticle to the ArticleButtons component so it don’t blow up at first, but when the data from the backend is retrieved then it gives that undefined error.
I put some console.logs in there to see the data from the backend, and it seems to be just exactly as it has to be, here they are the screenshots
Up is the initArticle data, below is the article data retrieved from the backend:
article data, as you can see, the author has all its properties defined
well, the problem seems to be happening when setArticle is called, I also tried another things like calling "setArticle(initArticle2)" with another fake initArticle2, and it works… there should be an issue when retrieving data from the backend?
- I also planned to then simply pass the slug to the ArticleButton component and then it will retrieve the article itself but I would like to make this work and do not use that workaround, most important, would like to learn why this is happening.
Thanks for your time mate
PD: here is the Article parent component
function Article() {
const { state } = useLocation();
const navigate = useNavigate();
const [ article, setArticle ] = useState<TArticle>(state || initArticle);
const { title, description, body, createdAt, author, tagList } = article || {};
const { authState } = useAuth() as TAuthContext;
const { headers, isAuth } = authState;
const { slug } = useParams();
useEffect(() => {
console.log("article ", article)
if (state) return;
getArticleBySlug({slug: slug || "", headers: headers })
.then((articleData) => {
console.log("artData", articleData)
setArticle(articleData)})
.catch((error) => {
console.error(error);
navigate("/not-found", { replace:true })
});
}, [slug, headers, isAuth, state, navigate])
return (
<div className="article-page">
<BannerContainer>
<h1>{title}</h1>
</BannerContainer>
<ArticleMeta
createdAt={createdAt}
author={author}>
<ArticleButtons article={article} setArticle={setArticle}/>
</ArticleMeta>
<ContainerRow addClass={"articlecont"}>
{body && <Markdown options={{ forceBlock:true }}>{body}</Markdown>}
<ArticleTags tagList={article.tagList}/>
<div className="row">
<ArticleMeta
createdAt={createdAt}
author={author}>
<ArticleButtons article={article} setArticle={setArticle}/>
</ArticleMeta>
</div>
</ContainerRow>
<Outlet/>
</div>
);
}
export default Article;
2
Answers
The solution was to learn the basics of states with the docs, and the issue was the following:
getArticleBySlug
is supposed to return aTArticle
object.a
TArticle
object has a nested objectTUser
(the 'author' property).My data was fetched correctly as you can see in the logs, but react can not figure out the type of 'author'
Why? A nested object is not an object living inside of another, like
A third object could also point to the first one,
and if you would change obj3.tools.brand = "Another brand", it would affect both obj2.tools.brand and the obj1 itself, because it is all the same object.
So I assumed that when I fetched the article data, I was going to get a TAuthor object as well just because it was inside of my article, it does not work that way.
So I basically have to set two things, 1. Article, 2. Author in order to render my component,
getArticleBySlug
gives me only a TArticle, and it is what it does, it does not give a TUser just because it is inside of TArticle, so you get it asundefined
, even if my data was correct and had all the expected properties.So the solution was, once TArticle is fetched, make another call to get TUser data, like so:
Is a async function inside of another, but it solved the issue. Notice that I could not simply call
setArticle(articleData)
and then mutate likearticle.author = authorData
, because despite of objects being mutable, you are not actually re-rendering the page again so you won't see those changes.Recommend to experiment, fail yourself and read the docs, it works.
Hope this is useful.
Instead of putting some
initArticle
you should make a check when the actual article is fetched (in the parent).Basically put a flag before the child component to optionally render it:
While fetching things from the Backend you usually have to display some sort of loading image/spinner instead, because the operation is async. Also the
initArticle
then is not needed anymoreYou don’t posted which outcome you expect, but this solution should atleast fix the error from happening.