skip to Main Content

I am trying to update the value of a few useState() variables from input given in the child of a child component but keep getting an error when it is called stating that the setItem() or setValue() function from the useState() declaration is not a function.

Here is the code of the parent where they are declared and referenced:

import React, {useEffect, useState} from "react"
import "./Creation.css"
import Header from "../components/Header"
import Footer from "../components/Footer"
import CreatorSurface from "../components/Creator/CreatorSurface"
import ProfilePic from "../assets/profile.png"
// import { AttributeContextProvider, useAttributeContext } from "../contexts/AttributeContext"


export default function Creation() {
    const [strength, setStrength] = useState("10")
    const [dexterity, setDexterity] = useState("10")
    const [constitution, setConstitution] = useState("10")
    const [intelligence, setIntelligence] = useState("10")
    const [wisdom, setWisdom] = useState("10")
    const [charisma, setCharisma] = useState("10")

    const [item, setItem] = useState()
    const [value, setValue] = useState()

    useEffect(() => {
        switch (item) {
            case "Strength":
                setStrength(value)
                break;
            case "Dexterity":
                setDexterity(value)
                break;
            case "Constitution":
                setConstitution(value)
                break;
            case "Intelligence":
                setIntelligence(value)
                break;
            case "Wisdom":
                setWisdom(value)
                break;
            case "Charisma":
                setCharisma(value)
                break;
            default:
                console.log("selector not recognized")
        }
    }, [item, value])

    return (
        <>
            <Header />
            <div className="creator">
                <h1 className="creator-title">Character Creator</h1>
                <div className="creator-banner">
                    <img className="profile" alt="Profile" src={ProfilePic}/>
                    <div className="character-details">
                        <h1 className="character-name">Character Name</h1>
                        <h2>race class</h2>
                    </div>
                    <div className="banner-atr">
                        <label for="str">Strength: </label>
                        <p name="str">{strength}</p>
                        <label for="dex">Dexterity: </label>
                        <p name="dex">{dexterity}</p>
                        <label for="con">Constitution: </label>
                        <p name="con">{constitution}</p>
                        <label for="int">Intelligence: </label>
                        <p name="int">{intelligence}</p>
                        <label for="wis">Wisdom: </label>
                        <p name="wis">{wisdom}</p>
                        <label for="cha">Charisma: </label>
                        <p name="cha">{charisma}</p>
                    </div>
                </div>
                <CreatorSurface setItem={setItem} setValue={setValue}/>
            </div>
            <Footer />
        </>
    )
}

The "middle man" code:

import React, { useState } from "react"
import CreatorDetails from "./CreatorDetails"
import CreatorClasses from "./CreatorClasses"
import CreatorRaces from "./CreatorRaces"
import CreatorEquipment from "./CreatorEquipment"
import "./CreatorSurface.css"


export default function CreatorSurface({ setItem, setValue }) {
    const [isSelected, setIsSelected] = useState()

    const handleClick = (e) => {
        const id = e.target.id;
        setIsSelected(id);
        console.log(id);
    };

    const renderChild = () => {
        switch (isSelected) {
            case "details":
                return <CreatorDetails setItem={setItem} setValue={setValue}/>
            case "race":
                return <CreatorRaces />
            case "class": 
                return <CreatorClasses />
            case "equipment":
                return <CreatorEquipment />
            default:
                return <CreatorDetails />
        }
    }

    return (
        <div className="parent">
            <div className="selector">
                <h1>Create your character using the sections below</h1>
                <button onClick={handleClick} id="details" >Details</button>
                <button onClick={handleClick} id="race" >Race</button>
                <button onClick={handleClick} id="class" >Class</button>
                <button onClick={handleClick} id="equipment" >Equipment</button>
                <button onClick={handleClick} id="save" >Save Character</button>
                <button onClick={handleClick} id="sheet" >Character Sheet</button>
            </div>
            {renderChild()}
        </div>
    )
}

and the component where I want to do the updating:

import React, {useState} from "react";
import "./CreatorSub.css"
import Details from "./Details/Details";
// import { useAttributeContext } from "../../contexts/AttributeContext"


export default function CreatorDetails({ setItem, setValue }) {

    // list of options
    const options = [
        {
            label: '16',
            value: '16'
        },
        {
            label: '14',
            value: '14'
        },
        {
            label: '14',
            value: '14-1'
        },
        {
            label: '12',
            value: '12'
        },
        {
            label: '12',
            value: '12-1'
        },
        {
            label: '10',
            value: '10'
        }
    ]

    // list of select names
    const selectNames = [
        "Strength",
        "Dexterity",
        "Constitution",
        "Intelligence",
        "Wisdom",
        "Charisma"
    ]

    const [chosenOptions, setChosenOptions] = useState({});


    // remove the option when chosen by another selector
    const isChosenByOther = (optionValue, selectName) => {
        for (let key in chosenOptions) {
            if (key !== selectName) {
                if (chosenOptions[key] === optionValue) {
                    return true;
                }
            }
        }
        return false;
    };

    const handleChange = (ev) => {
        setChosenOptions({...chosenOptions, [ev.target.name]: ev.target.value});
        let value = ""
        if (ev.target.value === "14-1") {
           value = "14"
        } else if (ev.target.value === "12-1") {
            value = "12"
        } else {value = ev.target.value}
        setItem(ev.target.name)
        setValue(value)
    };

    // Runs through the list of selector names provided above and creates the neccessary elements with corresponding labels
    const selectRender = selectNames.map((name, index) => {
        return (
            <>
                <label for={name}>{name}:</label>
                <select name={name} key={index} onChange={handleChange} value={chosenOptions[name] || ''}
                        required={index === 0} id={name}>
                    <option value=''/>
                    {options.filter(({value}) => !isChosenByOther(value, name))
                        .map(({label, value}, oIndex) =>
                            <option value={value} key={oIndex}>{label}</option>)
                    }
                </select>
            </>
        )
    })

    return (
        <div className="sub-parent">
            <div className="sub-selector">
                <div id="char-info">
                    <label for="char-name">Name:</label>
                    <input type="text" id="char-name" name="char-name" />
                    <label for="level">Level:</label>
                    <select name="level" id="level" type="submit">
                        <option value="1">1</option>
                        <option value="1">2</option>
                        <option value="1">3</option>
                        <option value="1">4</option>
                        <option value="1">5</option>
                        <option value="1">6</option>
                        <option value="1">7</option>
                        <option value="1">8</option>
                        <option value="1">9</option>
                        <option value="1">10</option>
                        <option value="1">11</option>
                        <option value="1">12</option>
                        <option value="1">13</option>
                        <option value="1">14</option>
                        <option value="1">15</option>
                        <option value="1">16</option>
                        <option value="1">17</option>
                        <option value="1">18</option>
                        <option value="1">19</option>
                        <option value="1">20</option>
                    </select>
                </div>
                <div id="attributes" >
                    <h1>Character Attributes</h1>
                    <p>Characters have 6 Defining Scores: Strength (Str), Constitution (Con), Dexterity (Dex), 
                        Intelligence (Int), Wisdom (Wis), and Charisma (Cha). As your character will essentially 
                        be a combination of statistics and ability scores, it is important to define which scores 
                        should be higher or lower to match what type of character you want to play. 
                    </p>
                    <p>Each character will be given the same pool of 6 numbers to choose from to divide between 
                        the Defining Scores (be aware that your choice of race will increase or decrease different scores); 
                        16, 14, 14, 12, 12, and 10.
                    </p>
                    <div className="atr-selectors">
                        {selectRender}
                    </div>
                </div>
            </div>
            <Details />
        </div>
        
    )
}

When I call the setItem and setValue functions within the CreatorDetails component through the handleChange Function I get the error that it is not a function. The hope is that when the <select> tags in the CreatorDetails component change value it will be reflected in the Creation component immediately as they are on the same page view. Not sure what is keeping it from recognizing it as a function. any help would be appreciated.

2

Answers


  1. In your renderChild method, the default case isn’t passing in any values for the setItem and setValue props. Since isSelected is being initialized to null, that default case is what is rendering initially.

    Suggested change:

    const renderChild = () => {
            switch (isSelected) {
                case "details":
                    return <CreatorDetails setItem={setItem} setValue={setValue} />
                case "race":
                    return <CreatorRaces />
                case "class": 
                    return <CreatorClasses />
                case "equipment":
                    return <CreatorEquipment />
                default:
                    return <CreatorDetails setItem={setItem} setValue={setValue} />
            }
        }
    

    PS — this is a textbook example of why TypeScript is valuable. If you had a type specified for your CreatorDetails props, your IDE would show you an error in the appropriate place 😉

    Login or Signup to reply.
  2. The issue arises when you handle the default case in your CreatorSurface component.

          default:
            return <CreatorDetails />;
    

    Doing this causes the setItem and setValue to be undefined inside the CreatorDetails component

    function CreatorDetails({ setItem, setValue }) //setItem and setValue is required here
    

    When the JSX tries to execute undefined, it gives the error that setItem is not a function.

    Instead, you can handle this by providing the state setters like this:

        switch (isSelected) {
          ...
          default:
            return <CreatorDetails setItem={setItem} setValue={setValue} />;
        }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search