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
In your
renderChild
method, the default case isn’t passing in any values for thesetItem
andsetValue
props. SinceisSelected
is being initialized tonull
, that default case is what is rendering initially.Suggested change:
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 😉The issue arises when you handle the
default
case in yourCreatorSurface
component.Doing this causes the setItem and setValue to be undefined inside the
CreatorDetails
componentWhen 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: