useState method not updating state with onClick? - reactjs

I am creating a custom multiple choice question, but I am having difficulties updating my selection choice using useState.
const QuestionPage = ({ audioFiles }) => {
const [choice, setChoice] = useState(-1); // -1 is when none of the choices are selected
const updateChoice = val => {
setChoice(val);
}
return (
// each MultipleChoice contains an audio file and a radio button
<MultipleChoice audioFiles={audioFiles} choice={choice} updateChoice={updateChoice} />
)
};
const MultipleChoice = ({ audioFiles, choice, updateChoice }) => {
const answerOption = audioFiles.map((item, key) =>
<AudioButton file={file} index={key} choice={choice} updateChoice={updateChoice} />
);
return (
{answerOption}
);
}
const AudioButton = ({ file, index, choice, updateChoice }) => {
const handleClick = (val) => {
updateChoice(val);
};
const radioButton = (
<div className={`${index === choice ? "selected" : ""}`} onClick={() => handleClick(index)}>
</div>
);
return (
<>
{radioButton}
<Audio file={file} />
</>
);
}
In the first function, QuestionPage within updateChoice, when I use console.log(val), it updates according to the selections I make (i.e. 0 and 1). However, when I call console.log(choice), it keeps printing -1.
In addition, I keep getting an error message that says updateChoice is not a function.
Any advice? Thanks in advance!

Looks like you did not pass the value of audioFiles in MultipleChoice function

Related

Why Separate function's returned value mutates props values in React?

I am following a course, sorting logic comes from "sortQuotes" function and storing its returned data to "sortedQuotes" constant, so how is it possible that even though I am not passing that modified data to JSX and still rendering original props.quotes - code works and pressing sorting button changes list to ascending/descending correctly?
const sortQuotes = (quotes, ascending) => {
return quotes.sort((quoteA, quoteB) => {
if (ascending) {
return quoteA.id > quoteB.id ? 1 : -1;
} else {
return quoteA.id < quoteB.id ? 1 : -1;
}
});
};
// props.quotes -----> [{id:'q1'},{id:'q2'},{id:'q3'}]
const QuoteList = props => {
const navigate = useNavigate();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const isSortingAscending = queryParams.get('sort') === 'asc';
console.log(props.quotes);
// Even before "sortQuotes" function call on the first render, props.quotes are still changed in order
const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
const changeSortingHander = () => {
navigate(`/quotes?sort=${!isSortingAscending ? 'asc' : 'des'}`);
};
return (
<>
<div className={classes.sorting}>
<button onClick={changeSortingHander}>
Sort {isSortingAscending ? 'Descending' : 'Ascending'}
</button>
</div>
<ul className={classes.list}>
{props.quotes.map(quote => (
<QuoteItem
key={quote.id}
id={quote.id}
author={quote.author}
text={quote.text}
/>
))}
</ul>
</>
);
};

How to refresh only the updated item in a list, using useQuery() to get the list

I retrieve a list of jobs using useQuery(), each one have a Favourite icon (filled depending if it's favourited)
If I click that button, I managed to refresh the item Favourite icon, but it refreshes all the items.
Whats the correct way to avoid that? Because it appears the Loading wheel again, and I think it has to be a more elegant way.
const Openings = () => {
const onToggleFav = () => {
setFavCount(prev => prev + 1)
}
const [favCount, setFavCount] = useState(0);
const { isLoading, data } = useQuery(
['getRecruiterOpenings', favCount],
() => getRecruiterOpenings()
);
return (
<div>
{ isLoading ? <Loading /> : (
<>
{ data && data.openings && data.openings.map((opening) => (
<>
<Opening {...opening} onToggleFav={() => onToggleFav()} key={opening.id}/>
</>
))}
</>
)}
</div>
)
}
export default Openings;
Inside Opening component I have a method that dispatches when you click the fav icon:
const toggleFav = async (e) => {
e.preventDefault();
await toggleFavOpening(fav, id).then(() => {
if(onToggleFav) onToggleFav()
});
}

How can I prevent initial state array from being mutated?

In my container component I have a state that gets initialized with an object that I use as data.
I clone the state array to prevent the initial state from being mutated but it still gets mutated, which I don't want to happen since I will need to compare the current state with the initial state later on.
The who system is kept inside the CubeOfTruthSystem component
function CubeOfTruthSystem() {
const [cubeIndex, setCubeIndex] = useState(0);
const [faceIndex, setFaceIndex] = useState(0);
return (
<React.Fragment>
<CubeSelector handleClick={(index) => setCubeIndex(index)} />
<CubeContainer cubeIndex={cubeIndex} faceIndex={faceIndex} />
<FaceSelector handleClick={(index) => setFaceIndex(index)} />
<button id="reset-face" onClick={() => console.log(CubeOfTruth)}>
Reset
</button>
</React.Fragment>
);
}
The parent component for the state looks like this:
function CubeContainer({ cubeIndex, faceIndex }) {
const [cube, setCube] = useState(CubeOfTruthData);
const handleCellClick = (id, row) => {
const cubeClone = [...cube];
const item = cubeClone[cubeIndex].faces[faceIndex].data[0].find(
(item) => item.id === id
);
item.state = "active";
cubeClone[cubeIndex].faces[faceIndex].data = activateAdjacentCells(
id,
row,
cubeClone[cubeIndex].faces[faceIndex].data,
item
);
setCube(cubeClone);
};
return (
<div id="cube-container">
{cube[cubeIndex].faces[faceIndex].data.map((row) => {
return row.map((item) => {
return (
<CubeItem item={item} handleClick={handleCellClick} key={item.id} />
);
});
})}
</div>
);
}
And this is the child component
function CubeItem({ item, handleClick }) {
const handleBgClass = (cellData) => {
if (cellData.state === "inactive") {
return cellData.bg + "-inactive";
} else if (cellData.state === "semi-active") {
return cellData.bg + "-semi-active";
} else {
return cellData.bg;
}
};
return (
<button
className={`cell-item ${handleBgClass(item)}`}
disabled={item.state === "inactive" ? true : false}
onClick={() => handleClick(item.id, item.row)}
/>
);
}
In the CubeOfTruth component, I'm trying to get the initial state (which is the CubeOfTruth array), but after changing the state, cube, cubeClone and CubeOfTruth all have the same values.
How can I make sure CubeOfTruth never gets mutated?
You're trying to clone cube array but you're making a shallow copy of it.
If you want to prevent mutation of nested properties you should make a deep copy instead.
Replace this:
const cubeClone = [...cube];
With this:
const cubeClone = JSON.parse(JSON.stringify(cube));
Or use some library like lodash
const cubeClone = _.cloneDeep(cube)

make onHover affect only one element in reactjs

im trying to show a button everytime a string is hovered, what im doing is working fine, but when i hover the string it will show every button in available string, i tried to pass the key but it still wont work, here is my code
const _showButton = () => {
setButton(true);
};
const _hideButton = () => {
setButton(false);
};
const _options = (uid) => {
return isButton ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p) => {
return (
<div>
<Typography onMouseEnter={_showButton} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
})}
)
any help will be appreciated, thanks before, i know this question might be already asked before but i cant find the one that use a functional like me instead a class
it seems like button state is only one boolean, who controlled all mapped elements.
So you have two options,
First, change button state to an array
const [ button, setButton ] = useState(Array.from({ length: isProject.length }, _ => false))
and pass the index to functions and use specific slot to determine if a button should be visible
const _toggleButton = (i) => {
setButton(prev => prev.map((bool, idx) => i == idx ? !bool : bool);
};
const _options = (uid, i) => {
return button[i] ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p, i) => {
return (
<div>
<Typography onMouseEnter={()=>_toggleButton(i)} onMouseLeave={()=>_toggleButton(i)}>
{p.title} {_options(p.uid, i)}
</Typography>
</div>
);
})}
)
Second is refactor mapped elemto it own component and declare state there, that way each elem will have it own state
{isProject.map((p) => <Component p={p} /> )}
function Component({p}) {
const [button, setButton] = useState(false)
const _showButton = () => {
setButton(true);
};
const _hideButton = () => {
setButton(false);
};
const _options = (uid) => {
return button ? <button key={uid}> ... </button> : null;
};
return (
<div>
<Typography onMouseEnter={_showButton} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
}
This is because _option runs in map and iterate over the whole list and the state is a single state which enables it for every item. You should consider setting "p's uid" in state variable (instead of true/false) and compare uid with the one is state in "_options" method.
const [selectedButtonUid, setSelectedButtonUid] = useState('');
const _showButton = (uid) => {
setSelectedButtonUid(uid);
};
const _hideButton = () => {
setSelectedButtonUid('');
};
const _options = (uid) => {
return selectedButtonUid === uid ? <button key={uid}> ... </button> : null;
};
return(
{isProject.map((p) => {
return (
<div>
<Typography onMouseEnter={()={_showButton(p.uid)}} onMouseLeave={_hideButton}>
{p.title} {_options(p.uid)}
</Typography>
</div>
);
})}
)

How can you make a createRef/useRef not target the last value. But rather go to where its suppose to

Can't manage to make useRef/createRef to get any other div's other then what was added last. How can i make it so when the button is clicked the ref to the div changes.
I've tried with both useRef and createRef. Since I want to make a new instance of ref, i've looked more into createRef rather then useRef.
I've also played around useEffect. But my solution didn't help me with my biggest problem
I have made a small project containing 3 components to help you understand what I'm trying to explain.
I also have a database containing mock data -> in my real project this isn't the problem. It's an array containing objects.
[{'id':'1', 'name':'first'},...]
Main:
const MainComponent = () => {
const dataRef = React.createRef(null)
React.useEffect (() => {
if(dataRef && dataRef.current){
dataRef.current.scrollIntoView({ behavior:'smooth', block:'start' })
}
},[dataRef])
const _onClick = (e) => {
dataRef.current.focus();
}
return(
<>
{data && data.map((entry, index) =>{
return <ButtonList
key={index}
entry={entry}
onClick={_onClick}
/>
})}
{data && data.map((entry, index) =>{
return <ListingAllData
key={index}
dataRef={dataRef}
entry={entry}
index={index}/>
})}
</>
)
}
Button Component
const ButtonList = ({ entry, onClick }) => {
return <button onClick={onClick}>{entry.name}</button>
}
Listing data component
const ListingAllData = (props) => {
const {entry, dataRef } = props;
return (
<div ref={dataRef}>
<p>{entry.id}</p>
<p>{entry.name}</p>
</div>
);
}
I've console logged the data.current, it only fetches the last element. I hoped it would fetch the one for the button I clicked on.
I think the main idea here is to create dynamic refs for each element (array of refs), that's why only the last one is selected when app renders out.
const MainComponent = () => {
const dataRefs = [];
data.forEach(_ => {
dataRefs.push(React.createRef(null));
});
const _onClick = (e, index) => {
dataRefs[index].current.focus();
dataRefs[index].current.scrollIntoView({
behavior: "smooth",
block: "start"
});
};
return (
<>
{data &&
data.map((entry, index) => {
return (
<ButtonList
key={index}
entry={entry}
onClick={e => _onClick(e, index)}
/>
);
})}
{data &&
data.map((entry, index) => {
return (
<>
<ListingAllData
key={index}
dataRef={dataRefs[index]}
entry={entry}
index={index}
/>
</>
);
})}
</>
);
};
Created working example in code sandbox.
https://codesandbox.io/s/dynamic-refs-so25v
Thanks to Janiis for the answer, my solution was:
in MainComponent
...
const refs = data.reduce((acc, value) => {
acc[value.id] = React.createRef();
return entry;
}, {});
const _onClick = id => {
refs[id].current.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
....
then i passed it through to the child and referred like
<div ref={refs[entry.id]}>

Resources