I followed a tutorial recently about integrating a cms into your website. The tutorial used sanity cms which made the process very intuitive. Once I was done with the tutorial I was ready to use it in my own projects.
however when I try to fetch data with the useEffect hook I get an error: Cannot read properties of undefined. I know this is because fetching data is done async. But the thing I can't wrap my head around is I did it the exact same way as the tutorial. He didn't use any state for loading or isFetched. So my question is what did I do different than the tutorial and how should I solve it?
I don't really want to use a loading state because that doesn't really look that good...
This is the JSON object I receive from the api:
[{…}]
0:
buttonlabel: "Start learning"
description: "Ranging from beginner to pro level tricks. Wanna know the best way to learn a trick? You can search for it down below and find a tutorial from one of our trainers as well as a detailed explanation. Still stuck? Come ask us at a Westsite training moment."
intro: "Welcome to the Westsite trick progression guide. Here you can find a collection of all the wakeboarding tricks you can think of. "
_createdAt: "2022-05-24T16:26:13Z"
_id: "a4f8cf02-4b86-44d5-a63d-c95a3a7d3293"
_rev: "QYLgvM20Eo53w3noOOj0MB"
_type: "hero"
_updatedAt: "2022-05-24T17:29:10Z"
This is the tutorial component:
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { urlFor, client } from "../../client";
import { AppWrap, MotionWrap } from "../../wrapper";
import "./About.scss";
const About = () => {
const [abouts, setAbouts] = useState([]);
useEffect(() => {
const query = '*[_type == "abouts"]';
client.fetch(query).then((data) => setAbouts(data));
}, []);
return (
<div>
<h2 className="head-text">
I know that
<span> Good Design </span>
<br />
means
<span> Good Business</span>
</h2>
<div className="app__profiles">
{abouts.map((about, index) => {
return (
<motion.div
whileInView={{ opacity: 1 }}
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.5, type: "tween" }}
className="app__profile-item"
key={about.title index}
>
<img src={urlFor(about.imgUrl)} alt={about.title} />
<h2 className="bold-text" style={{ marginTop: 20 }}>
{about.title}
</h2>
<p className="p-text" style={{ marginTop: 10 }}>
{about.description}
</p>
</motion.div>
);
})}
</div>
</div>
);
};
export default AppWrap(
MotionWrap(About, "app__about"),
"about",
"app__whitebg"
);
And this is mine:
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { BiRightArrowAlt } from "react-icons/bi";
import { client } from "../../client";
import "./Hero.scss";
const Hero = () => {
const [heroContent, setHeroContent] = useState([]);
useEffect(() => {
const query = '*[_type == "hero"]';
client.fetch(query).then((data) => setHeroContent(data));
}, []);
const content = heroContent[0];
return (
<div className="app__hero">
<motion.div
className="app__hero-content-container"
whileInView={{ opacity: [0, 1], x: [500, 0] }}
transition={{ duration: 1, ease: "easeOut" }}
>
<div className="app__hero-content">
<h2 className="heading-text">
Learn
<span className="highlighted"> wakeboarding </span>
the right way
</h2>
<p className="p-text">{content.intro}</p>
<p className="p-text">{content.description}</p>
<button className="primary-btn p-text app__flex">
{content.buttonlabel}
<BiRightArrowAlt />
</button>
</div>
</motion.div>
</div>
);
};
export default Hero;
CodePudding user response:
This line will cause issues before the data is applied to state asynchronously
const content = heroContent[0];
On the initial render, heroContent
is an empty array, so content
will be undefined until your data is loaded. A couple options -
1 - render some sort of loading state until heroContent
has been populated -
if (!heroContent.length) return <LoadingSpinner />
2 - wrap the portion that is trying to use content
with a guard clause
{content && (
<p className="p-text">{content.intro}</p>
<p className="p-text">{content.description}</p>
<button className="primary-btn p-text app__flex">
{content.buttonlabel}
<BiRightArrowAlt />
</button>
)}
CodePudding user response:
The issue comes when you try to access properties from content
when it's undefined. If you don't want to show any loading indicator, I would go with showing some fallback for when content is not defined. e.g.
Instead of:
<p className="p-text">{content.intro}</p>
You could go with:
<p className="p-text">{content?.intro ?? '-'}</p>
or something like that.
CodePudding user response:
Problem is that you breakdown your state on different level that create problem with state changes. So, you have to do this Either you call state as map function or save your state with specfic index 0.
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { BiRightArrowAlt } from "react-icons/bi";
import { client } from "../../client";
import "./Hero.scss";
const Hero = () => {
const [heroContent, setHeroContent] = useState([]);
useEffect(() => {
const query = '*[_type == "hero"]';
// this is giving response as array
client.fetch(query).then((data) => setHeroContent(data));
}, []);
return (
<div className="app__hero">
{heroContent.map((content,index)=>
<motion.div
className="app__hero-content-container"
whileInView={{ opacity: [0, 1], x: [500, 0] }}
transition={{ duration: 1, ease: "easeOut" }}
key={index}
>
<div className="app__hero-content">
<h2 className="heading-text">
Learn
<span className="highlighted"> wakeboarding </span>
the right way
</h2>
<p className="p-text">{content.intro}</p>
<p className="p-text">{content.description}</p>
<button className="primary-btn p-text app__flex">
{content.buttonlabel}
<BiRightArrowAlt />
</button>
</div>
</motion.div>}
</div>
);
};
export default Hero;