gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.27k stars 10.32k forks source link

Setting state for Component? Gatsby v2 #10415

Closed lswest closed 5 years ago

lswest commented 5 years ago

I'm trying to configure my Layout component to include a state for whether the sidebar is open/closed. The problem I'm having is figuring out the syntax format in order to get everything working. I'm not super well versed in ES6 (or React for that matter), which makes the guides I've found online quite confusing (with the various syntaxes). I'd look for general React help online, except for the fact that I haven't yet seen any examples with StaticQuery, which I need to also integrate somehow (and is gatsby-specific).

Details are split up below. Thanks for any help!

What I've tried:

Added the constructor and toggleMenu functions into the Component, and wrapped the StaticQuery in a return() statement (to remove that error). Adding the functions blindly into the component results in a warning stating Read-only global 'constructor' should not be modified and an error 'toggleMenu' is not defined.

I've tried replacing the Component definition with class Layout extends React.Component{}, but then I was unable to get the data and the children to pass through successfully.

I also saw an older example (I believe it was in an issue here) where someone was just defining state like state = {showMenu: True}, which also doesn't work (errors claiming state isn't defined).

Files

The component (without state):

import React from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'
import { StaticQuery, graphql } from 'gatsby'

import Sidebar from './sidebar'
import '../index.css'
import Menu from './../images/icons/icon-menu.svg'

const Layout = ({ children, data }) => {
  <StaticQuery
    query={graphql`
      query SiteTitleQuery {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={data => (
      <>
        <Helmet
          title={data.site.siteMetadata.title}
          meta={[
            { name: 'description', content: 'Sample' },
            { name: 'keywords', content: 'sample, something' },
          ]}
          bodyAttributes={{
            class: "font-sans antialiased box-sizing text-black leading-tight min-h-screen"
        }}
        >
          <html lang="en"/>
        </Helmet>
        <div className="flex min-h-screen">
        <Sidebar siteTitle={data.site.siteMetadata.title}></Sidebar>
        <Menu className="sm:hidden w-10 h-10"/>
        <div className="w-full py-2 px-2 sm:px-0">
          <main className="mt-4 max-w-md md:mx-auto leading-normal">
            {children}
          </main>
        </div>
        </div>
      </>
      )}
  />
}

Layout.propTypes = {
  children: PropTypes.node.isRequired,
}

export default Layout

What I essentially want to add:

  constructor = (props) => {
    super(props);
    this.state.setState({ showMenu: true });
  }

  toggleMenu = () => {
      this.state.showMenu.setState(!this.state.showMenu) //Flips true/false
  }

The function toggleMenu I would then call using onClick.

jgierer12 commented 5 years ago

The correct syntax is:

constructor(props) {
  super(props);
  this.state = { showMenu: true };
}

toggleMenu() {
  this.setState(prevState => { showMenu: !prevState.showMenu })
}

You also need to use the class syntax, wrap your JSX in a render function, and get children and data from this.props:

class Layout extends React.Component  {
  // ... constructor and toggleMenu from above

  render() {
    const { children, data } = this.props;
    return (
      <StaticQuery ... />
    )
  }
}

If you want to know more about the how and why, I'd encourage you to look into the React tutorial. Of course, we are always happy to help as well!

nicobao commented 4 years ago

Hi, I think it is somehow a related issue.

I don't get how to initialize a state using a Gatsby GraphQL Query. I do it here: https://github.com/baozi-technology/baozi-web/blob/master/src/components/Header/Header.jsx

But for some reason, the "img" sometimes doesn't load the content. It seems arbitrary as if from time to time the graphql query is done AFTER the component is loaded: https://baozi.technology/

jonniebigodes commented 4 years ago

@NicoGim if you don't mind, i'm going to fork your repo and go over it and see if i can provide you with a answer, do you mind waiting a bit while i go over it?

nicobao commented 4 years ago

Actually, I think I just solved the bug.

How so?

I was using the following query

  query HeaderQuery {
    allFile(filter: {name: {eq: "baozi-technology-full-logo"}, extension: {regex: "/jpg|gif/"}}) {
      edges {
        node {
          relativePath
          extension
        }
      }
    }
  }

Instead, I now use

  query HeaderQuery {
    allFile(filter: {name: {eq: "baozi-technology-full-logo"}, extension: {regex: "/jpg|gif/"}}) {
      edges {
        node {
          publicURL
          extension
        }
      }
    }
  }

I fill the "img" tag with publicURL instead of relativePath in the "src" parameter. And now, it works fine.

@jonniebigodes thank you so much for your help! The error had nothing to do with Gatsby itself though - sorry for the inconvenience.

jonniebigodes commented 4 years ago

@NicoGim glad that you managed to get it to work. I checked your code and based on that and if you don't mind me offering a couple of alternatives to your code that will achieve the same end result.

1- The first alternative with gatsby useStaticQuery hook:

import React, { useState } from "react";
import { useStaticQuery, graphql, Link } from "gatsby";
import styles from "./Header.module.scss";
import Drawer from "../Drawer/Drawer";

const HeaderV1 = () => {
  const [hover, setHover] = useState(false);
  const logoResult = useStaticQuery(graphql`
    {
      mainLogo: file(
        relativePath: { eq: "logos/baozi-technology-full-logo.jpg" }
      ) {
        publicURL
      }
      gifLogo: file(
        relativePath: { eq: "logos/baozi-technology-full-logo.gif" }
      ) {
        publicURL
      }
    }
  `);
  // destructure the query result
  const {mainLogo,gifLogo}= logoResult
  return (
    <header className={styles.header}>
      <div className={styles.headerContainer}>
        <div title="Jump to home page" className={styles.logoContainer}>
          <Link to="/">
            <img
              onMouseOver={() => {
                console.log("entered mouse over");
                setHover(true);
              }}
              onMouseOut={() => {
                console.log("entered onMouseOut");
                setHover(false);
              }}
              className={styles.heightSet}
              src={!hover ? mainLogo.publicURL : gifLogo.publicURL}
              alt="Baozi Technology"
            />
          </Link>
        </div>
        <div className={styles.toggleMenuContainer}>
          <Drawer />
        </div>
        <div className={styles.navContainer}>
          <nav className={styles.siteNav}>
            <div className={styles.siteNavItemFirst}>
              <Link activeClassName={styles.linkActive} to="/">
                Home
              </Link>
            </div>
            <div className={styles.siteNavItemMiddle}>
              <Link
                activeClassName={styles.linkActive}
                to="/ranch-under-the-hood"
              >
                Ranch doc
              </Link>
            </div>
            <div className={styles.siteNavItemMiddle}>
              <Link activeClassName={styles.linkActive} to="/about">
                About
              </Link>
            </div>
            <div className={styles.siteNavItemLast}>
              <a
                title="nicolas.gimenez@baozi.technology"
                href="mailto:nicolas.gimenez@baozi.technology"
              >
                Message me
              </a>
            </div>
          </nav>
        </div>
      </div>
    </header>
  );
};

export default HeaderV1;

Breaking down the code above.

With this small change you're streamlining the code, removing the need for loop and avoiding managing multiple states.

2- Alternative 2, without any GraphQL: I saw that you added the content inside the static folder and with that in mind i created another component without any GraphQL that will achieve the same end result with only one useState hook making the component even leaner and simpler.

import React, { useState } from "react";
import { Link } from "gatsby";
import styles from "./Header.module.scss";
import Drawer from "../Drawer/Drawer";

const HeaderV2 = () => {
  const [hover, setHover] = useState(false);

  return (
    <header className={styles.header}>
      <div className={styles.headerContainer}>
        <div title="Jump to home page" className={styles.logoContainer}>
          <Link to="/">
            <img
              onMouseOver={() => {
                console.log("entered mouse over");
                setHover(true);
              }}
              onMouseOut={() => {
                console.log("entered onMouseOut");
                setHover(false);
              }}
              className={styles.heightSet}
              src={!hover ? '/logos/baozi-technology-full-logo.jpg':'/logos/baozi-technology-full-logo.gif'}
              alt="Baozi Technology"
            />
          </Link>
        </div>
        <div className={styles.toggleMenuContainer}>
          <Drawer />
        </div>
        <div className={styles.navContainer}>
          <nav className={styles.siteNav}>
            <div className={styles.siteNavItemFirst}>
              <Link activeClassName={styles.linkActive} to="/">
                Home
              </Link>
            </div>
            <div className={styles.siteNavItemMiddle}>
              <Link
                activeClassName={styles.linkActive}
                to="/ranch-under-the-hood"
              >
                Ranch doc
              </Link>
            </div>
            <div className={styles.siteNavItemMiddle}>
              <Link activeClassName={styles.linkActive} to="/about">
                About
              </Link>
            </div>
            <div className={styles.siteNavItemLast}>
              <a
                title="nicolas.gimenez@baozi.technology"
                href="mailto:nicolas.gimenez@baozi.technology"
              >
                Message me
              </a>
            </div>
          </nav>
        </div>
      </div>
    </header>
  );
};

export default HeaderV2;

What is happening here is the following, as you're using the static folder, Gatsby will pick on it and grabs the files and places them inside the public folder and with that you can grab them directly without the need of any graphql query. You can read up more about it in here and here.

I'll leave it up to you on how you wish to proceed. And it was no inconvenience whatsoever.

nicobao commented 4 years ago

@jonniebigodes Thanks a lot for your help. I agree that your solutions are much cleaner.

jonniebigodes commented 4 years ago

@NicoGim no need to thank, glad that i was able to offer you some insights.

nicobao commented 4 years ago

@jonniebigodes I actually have another small issue on my site. If you are happy to help, then I am happy too!

https://github.com/baozi-technology/baozi-web/blob/master/src/components/BioContent/BioContent.jsx

As you can see clicking on "eating baozi" in https://baozi.technology triggers a .gif of myself eating one. Cool. I preload the two avatars in order to make the change as smooth as possible for first time users of the site. However, for some reason, when clicking on "eating baozi" for the first time, it does take a little while for the avatar to change. I wonder whether it is because of my use of setTimeout... Do you have a clue?

With kind regards, Nicolas

N.B: have an awesome new year eve!

jonniebigodes commented 4 years ago

@NicoGim i took a look at the component and i used the same approach in this component that was used in my earlier response. Removing the need of almost all of the "moving parts" you have.

1- Starting with a simplified way with useStaticQuery, i created a new component with the following content:

import React, { useState } from "react";
import { useStaticQuery, graphql, Link } from "gatsby";
import styles from "./BioContent.module.scss";
import PropTypes from "prop-types";

// the {isLink} is used to destructure the prop inside, that is injected in the parent component
const BioContentV1 = ({ isLink }) => {
  const [eating,setIsEating]= useState(false)
  const bioResult = useStaticQuery(graphql`
    {
      mainBio: file(relativePath: { eq: "avatars/nico_catch_yuting.jpg" }) {
        publicURL
      }
      gifBio: file(
        relativePath: { eq: "avatars/me-eating-baozi-square-transparent.gif" }
      ) {
        publicURL
      }
    }
  `);

  // destructures the query result
  const { mainBio, gifBio } = bioResult;

  // click handler for the button triggers the 
  const handleBaoziClick = () => {
    setIsEating(true);
    setTimeout(() => {
      setIsEating(false);
    }, 2500);

  };
  return (
    <div className={styles.bioContentContainer}>
      <div className={styles.avatarContainer}>
        <img
          className={styles.avatar}
          src={!eating?mainBio.publicURL:gifBio.publicURL}
          alt="Oops, the profile pic did not load! Try refreshing the page."
        />
      </div>
      <div className={styles.bioContainer}>
        {isLink ? (
          <p className={styles.bioContent}>
            <span>
              Hi! I’m Nicolas – freelance fullstack software engineer based in
              France. My main area of expertise is concurrent backend services
              dealing with various non-standard protocols. My passions are
              solving complex problems using technology and{" "}
              <button type="button" className={styles.baozi} onClick={handleBaoziClick}>
                eating baozi
              </button>
              .
            </span>
            {"  "}
            <Link className={styles.arrow} to="/about">
              &rarr;
            </Link>
          </p>
        ) : (
          <p className={styles.bioContent}>
            {" "}
            <span>
              Hi! I’m Nicolas – freelance fullstack software engineer based in
              France. My main area of expertise is concurrent backend services
              dealing with various non-standard protocols. My passions are
              solving complex problems using technology and{" "}
              <button type="button" className={styles.baozi} onClick={handleBaoziClick}>
                eating baozi
              </button>
              .
            </span>
          </p>
        )}
      </div>
    </div>
  );
};

export default BioContentV1;

BioContentV1.propTypes = {
  isLink: PropTypes.bool
};

Same logic was applied, removed the excess useEffect and aliased the GraphQL to retrieve exactly what i need, without the need to complicate things and unnecessary loops. The click handler handleBaoziClick will set the state, to eating=true wait for two and a half seconds, that was based on the code you had and a rough estimate of the duration of the gif. Then revert back. This strategy was used as it's a bit tricky to handle gifs with React without some extra ammout of work, that would complicate the component's logic. Removed the vars assignement and moved to conditional rendering of the component based on the isLink prop, making the component more simple and leaner.

// the {isLink} is used to destructure the prop inside, that is injected in the parent component const BioContentV2 = ({ isLink }) => { const [eating,setIsEating]= useState(false) // click handler for the button triggers the const handleBaoziClick = () => { setIsEating(true); setTimeout(() => { setIsEating(false); }, 2500);

}; return (

Oops, the profile pic did not load! Try refreshing the page.
{isLink ? (

Hi! I’m Nicolas – freelance fullstack software engineer based in France. My main area of expertise is concurrent backend services dealing with various non-standard protocols. My passions are solving complex problems using technology and{" "} . {" "}

) : (

{" "} Hi! I’m Nicolas – freelance fullstack software engineer based in France. My main area of expertise is concurrent backend services dealing with various non-standard protocols. My passions are solving complex problems using technology and{" "} .

)}

); };

export default BioContentV2;

BioContentV2.propTypes = { isLink: PropTypes.bool };


As before as you're already using the `static` folder there was no need for the GraphQL query, you can just grab the assets directly and display them, making the component even more functional.

Once again i'll leave it up to you on how you wish to proceed. 

And an awesome new year eve to you aswell! 
mitkonikolov commented 2 years ago

The correct syntax is:

constructor(props) {
  super(props);
  this.state = { showMenu: true };
}

toggleMenu() {
  this.setState(prevState => { showMenu: !prevState.showMenu })
}

You also need to use the class syntax, wrap your JSX in a render function, and get children and data from this.props:

class Layout extends React.Component  {
  // ... constructor and toggleMenu from above

  render() {
    const { children, data } = this.props;
    return (
      <StaticQuery ... />
    )
  }
}

If you want to know more about the how and why, I'd encourage you to look into the React tutorial. Of course, we are always happy to help as well!

Note that you can now use state hooks in functions and don't need to change your function components to classes. Here is a link to React's docs including an example. There is also another example by Material-UI.