Home > OS >  React state is undefined when fetching data from sanity cms
React state is undefined when fetching data from sanity cms

Time:05-25

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;
  • Related