Rendering a Favorite Button Functional Component in React: useState, useEffect, localStorage, useSWR

This is my first technical document on React! I started building on Next.js before my bootcamp taught React Lectures 2 and React Lecture 3. React is not a requirement for our project and yet I decided to build my entire front-end with React utilizing Next.js. I’ve only been coding towards a career for 5 months, so bare with my communicability and feel free to offer love and support in the comments!

Unfilled hearts are unavailable to the free icon library, so I went with a broken heart. </3

To get a Favorite Button Functional Component to render using React (Next.js), you likely will run into a lot of hook errors. The main one that caused me problems is:

Unhandled Runtime Error Error: Rendered more hooks than during the previous render.

– Error Message

When you have hooks in the same component, they expect the same amount of hooks to render each time a render is called! That doesn’t really work well with favorite buttons. Especially if your favorite button has 3 hooks: the first renders once (to find and set the user), the second renders based on the response of the first one (to find favBool if the recipe is currently a favorite or not), and the last one renders every onClick (every time a user favorites and unfavorites)!

A work around is to identify all your hooks and find a way to split them into their own functional components. What if the hooks are all a part of the same component? That was exactly my frustration. ONE ity-bity 20x20px favorite button needs THREE hooks!

That’s where coding creativity lies until google searches and React documentation readily offers you a clearer solution. The favorite button code can be split into three functional components so essentially the component calls a component that calls a component!

Here is all my raw code on a github Gist that I’m having technical issues embedding on WordPress. Feel free to copy and paste into a code editor for better readability. Below I outline how my code works.

PART 1: My Imports

  • Next.js graciously holds all your CSS in a style folder, I used styles for my .favorite-button and .text_small which rendered if the User wasn’t logged in.
  • React, {useState, useEffect } are all being used. useState remembers the user logged in, whether or not the recipe is favorited, and toggling the recipe (I learned toggling from Codecademy’s state hook lesson!). useEffect checks localStorage if the user is logged in
  • useSWR is the Next.js solution to dynamic routing
  • Font Awesome Icons were suggested to me by my mentor! I ended up downloading their entire free library to use on my project. My project is now icon heavy because I am just amazed how easy it was to implement!

/components/favoritebutton.js

"use strict";
import styles from '../styles/Home.module.css';
import React, { useState, useEffect } from 'react';
import useSWR from 'swr'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHeart, faHeartBroken } from '@fortawesome/free-solid-svg-icons'

Part II: Check if a User is logged in

Let non-users see all your free app has to offer!
  • Debug with console.log often included props I passed from my parent component passed correctly. I passed the parameter recipeId.
  • I set the user to null using useState.
  • With useEffect, I retrieved user from localStorage that was set when the user logged in
  • Debug with console.log often including to see if the user is correctly stored.
  • If the user did not exist in local storage, the button is not rendered. Instead, return a link to log-in. (The span is there only because I’m highlighting my text overlayed on a background image of the recipe)
  • return another component with the next hook that finds the initial state of the user favorite! Pass it the userId and recipeId. Don’t be silly by passing all props! Also, remove props from the variable name so you don’t need to call the variable using props.props.props.....recipeId. >.<

/components/favoritebutton.js

function FavoriteButton(props) {

    console.log(props)

    const [user, setUser] = useState(null);

    useEffect(() => {
        const loggedInUser = localStorage.getItem('user');
        if (loggedInUser) {
            setUser(JSON.parse(loggedInUser));
        }
    }, []);

    console.log("This is the user saved in local storage:", user)

    // IF USER IS NOT IN LOCAL STORAGE, ASKED TO LOG IN INSTEAD
    if (user === null) {
        return (
            <p className={styles['text_small']}><span>
                <a href="/login">Log in to favorite this recipe!</a>
            </span></p>);
    }
    return <FindInitialState userId={user.user_id} recipeId={props.recipeId}/>
}

Part III: Render a favorite button if a User is logged in

  • Pass in the props from the previous component
  • Declare a favBool that will eventually have a value of true or false
  • fetch whether a user/recipe relationship exists
  • Debug with console.log often
  • Have the component show the user a preloader as the button is rendering
  • Since you used a hook (useSWR) to retrieve the data, do not use another hook in this component
  • return another component with the next hook that adds and deletes from your database using a Toggle. Pass in favBool, userId, and recipeId (or whatever variables are needed for your toggle to work)!

/components/favoritebutton.js


function FindInitialState(props) {

    let favBool;

    const fetcher = url => fetch(url).then(r => r.json())
    const { data, error } = useSWR(`/api/users/${props.userId}/recipes/${props.recipeId}`, fetcher)
    
    console.log("This user has this recipe favorited?", data)
    if (error) return <div>failed to load</div>
    if (data === undefined) return <div>loading...</div>

    favBool = data;

    return <SetStateAndToggle favBool={favBool} userId={props.userId} recipeId={props.recipeId}/>
}

Part IV: setFavorite and toggleFavorite modifying the GUI and your back-end!

What a cute free heart icon from fontawesome.com

I removed my comments in my code since this component is dense and the explanation is here:

  • Define the font awesome icons as variables (currentlyAFavorite or notCurrentlyAFavorite)
  • Set user’s favorite to the favBool found with the hook of the last component
  • Declare a toggleFavorite variable that takes in recipeId
  • If favorite is currently true, clicking will unfavorite and fetch a post request that removes the recipe to the User/Recipe datatable on the back-end
  • Debug using console.log often!
  • If the favorite is false to start, clicking will favorite and fetch a post request that adds the recipe that will add the recipe to the User/Recipe datatable on the backend
  • For the toggleFavorite function, return !favorite will toggle the button to the opposite boolean
  • return the favorite button that uses your CSS styling, onClick handles toggleFavorite which will using the ternary of favorite === true
  • Finally, export {FavoriteButton} component from Part II (which has FindInitialState component and SetStateAndToggle component nested into it) so you can render your button whatever parent component (e.g. DetailedRecipeComponent) not on this documentation.

/components/favoritebutton.js


function SetStateAndToggle(props) {
    const currentlyAFavorite = <FontAwesomeIcon icon={faHeart} />
    const notCurrentlyAFavorite = <FontAwesomeIcon icon={faHeartBroken}/>

    const [favorite, setFavorite] = useState(props.favBool);

    const toggleFavorite = (recipeId) => {
        setFavorite((favorite) => {
          if (favorite == true) {
            console.log("I clicked unfavorite")
            console.log(props)
            fetch(`/api/users/${props.userId}/recipes/${recipeId}/remove`, { method: 'POST' })
            .then(console.log("This was a favorited recipe, but now it isnt!"));

          }
          if (favorite == false) {
            console.log("I clicked favorite")
            fetch(`/api/users/${props.userId}/recipes/${recipeId}/add`, { method: 'POST' })
            .then(console.log("This was not a favorited recipe. Now it is!"));
          }

          return !favorite;
        });
    }

    return (
        <button
            className={styles['favorite-button']}
            onClick={() => toggleFavorite(props.recipeId)}
            key={props.recipeId}>
        { favorite === true ? currentlyAFavorite : notCurrentlyAFavorite} 
        </button>
    );
}

export {FavoriteButton};
console.log(“…is your friend!”)

I hope this was helpful and informative. Feel free to show me love below. This was today’s breakthrough learning moment as I’m 5 months into coding toward a career and Week 3 of building my first app, let alone with React! I wonder where all my learning will take me next.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: