I've been stuck with this idea of setting the array to an empty one after the function fires, but no matter what I did the results were the same. I want to empty an array if it contains the element that was pressed, but It keeps logging the old array, making it impossible to check if array already contains it in the first place. I'm new to React and I've already tried using useEffect() but I don't understand it that much even after studying and trying to use it several times.
import React, { useState, useEffect } from "react";
import { Card } from "./Card";
import "../styles/Playboard.css";
import { Scoreboard } from "./Scoreboard";
export function Playboard(props) {
const [score, setScore] = useState(0);
const [bestScore, setBestScore] = useState(0);
const [clicked, setClicked] = useState([]);
const [cards, setCards] = useState([
<Card name="A" key={1} handleCardClick={handleCardClick}></Card>,
<Card name="B" key={2} handleCardClick={handleCardClick}></Card>,
<Card name="C" key={3} handleCardClick={handleCardClick}></Card>,
<Card name="D" key={4} handleCardClick={handleCardClick}></Card>,
<Card name="E" key={5} handleCardClick={handleCardClick}></Card>,
<Card name="F" key={6} handleCardClick={handleCardClick}></Card>,
]);
const increment = () => {
setScore((c) => c + 1);
};
function checkScore() {
if (score > bestScore) {
setBestScore(score);
}
}
function handleStateChange(state) {
setClicked(state);
}
useEffect(() => {
setCards(shuffleArray(cards));
handleStateChange();
}, []);
useEffect(() => {
setCards(shuffleArray(cards));
checkScore();
}, [score]);
function handleCardClick(e) {
console.log(clicked);
if (clicked.includes(e.target.querySelector("h1").textContent)) {
console.log(clicked);
resetGame();
} else {
increment();
clicked.push(e.target.querySelector("h1").textContent);
console.log(clicked);
}
}
function resetGame() {
setScore(0);
setClicked([]);
console.log(clicked);
}
const shuffleArray = (array) => {
return [...array].sort(() => Math.random() - 0.5);
};
return (
<div>
<div className="container">{cards}</div>
<Scoreboard score={score} bestScore={bestScore}></Scoreboard>
</div>
);
}
Looks like this is a memory game of sorts, where you have to remember which cards you've clicked and they shuffle after every click? Okay!
As my comment says, you don't want to put JSX elements in state; state should be just data, and you render your UI based on that.
Here's a cleaned-up version of your game (here on CodeSandbox) –
Card is a simple component that renders the name of the card, and makes sure clicking the button calls handleCardClick with the name, so you need not read it from the DOM.
shuffleArray doesn't need to be in the component, so it's... not.
initializeCards returns a shuffled copy of the A-F array.
useState(initializeCards) takes advantage of the fact that useState allows a function initializer; i.e. initializeCards will be called once by React when Playboard mounts.
useEffect is used to (possibly) update bestScore when score updates. You don't really need it for anything else in this.
cards.map(... => <Card...>) is the usual React idiom to take data and turn it into elements.
I'm using JSON.stringify() here as a poor man's scoreboard plus debugging tool (for clicked).
I'm making liberal use of the functional update form of setState for efficiency (not that it matters a lot in this).
import React, { useState, useEffect } from "react";
function Card({ name, handleCardClick }) {
return <button onClick={() => handleCardClick(name)}>{name}</button>;
}
function shuffleArray(array) {
return [...array].sort(() => Math.random() - 0.5);
}
function initializeCards() {
return shuffleArray(["A", "B", "C", "D", "E", "F"]);
}
function Playboard() {
const [score, setScore] = useState(0);
const [bestScore, setBestScore] = useState(0);
const [clicked, setClicked] = useState([]);
const [cards, setCards] = useState(initializeCards);
useEffect(() => {
setBestScore((bestScore) => Math.max(bestScore, score));
}, [score]);
function handleCardClick(name) {
if (clicked.includes(name)) {
resetGame();
} else {
setScore((c) => c + 1);
setClicked((c) => [...c, name]);
setCards((cards) => shuffleArray(cards));
}
}
function resetGame() {
setScore(0);
setClicked([]);
setCards(initializeCards());
}
return (
<div>
<div className="container">
{cards.map((name) => (
<Card name={name} key={name} handleCardClick={handleCardClick} />
))}
</div>
{JSON.stringify({ score, bestScore, clicked })}
</div>
);
}
Related
I am building an application that has a database of videos that can be filtered by category and sorted by rating.
Filtering works after changing the options. However, when I change the categories of the video the filtering does not start automatically. I added useEffect but I don't know what else I can change and why it happens. Please help how to make the sorting not disappear when changing the cateogry.
UPDATE:
import * as _ from "lodash";
import { useEffect, useState } from "react";
import { getAllPrograms } from "../../helpers/getData";
import { TVProgram } from "../../models/models";
import Filters from "../Filters/Filters";
import ProgramsList from "../ProgramsList/ProgramsList";
import Sorting from "../Sorting/Sorting";
import "./HomePage.scss";
const HomePage = () => {
const [programs, setPrograms] = useState<Array<TVProgram>>([]);
const [category, setCategory] = useState<string>("movie,series");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>(programs);
const getPrograms = async (category: string) => {
const programs = await getAllPrograms(category);
setPrograms(programs);
};
useEffect(() => {
getPrograms(category);
}, [category]);
const updateCategory = (categoryName: string): void => {
setCategory(categoryName);
console.log("catName", categoryName);
};
const updatePrograms = (sortedPrograms: TVProgram[]): void => {
setSortedPrograms(sortedPrograms);
console.log("sortedPrograms", sortedPrograms);
};
return (
<div className="container">
<div>
<Filters
updateCategory={updateCategory}
currentCategory={category}
></Filters>
<Sorting programs={programs} setPrograms={updatePrograms}></Sorting>
</div>
<ProgramsList programs={sortedPrograms}></ProgramsList>
</div>
);
};
export default HomePage;
import _ from "lodash";
import { ChangeEvent, useEffect, useState } from "react";
import { sortProgramsByOrder } from "../../helpers/helpers";
import { TVProgram } from "../../models/models";
import "./Sorting.scss";
interface SortingListProps {
programs: TVProgram[];
setPrograms: (programs: TVProgram[]) => void;
}
const Sorting = ({ programs, setPrograms }: SortingListProps) => {
const OPTIONS = ["imdb rating descending", "imdb rating ascending"];
const [selectedOption, setSelectedOption] = useState<string>("");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>([]);
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
console.log();
setSelectedOption(event.target.value);
setPrograms(sortedPrograms);
};
return (
<div>
<select value={selectedOption} onChange={handleChange}>
<option selected>Sortuj</option>
{OPTIONS.map((option) => (
<option
key={option}
value={option}
selected={option === selectedOption}
>
{option}
</option>
))}
</select>
</div>
);
};
export default Sorting;
useEffect() is a hook that prevents updates to a variable except in specific cases. Any variable passed into the array at the end of the useEffect() hook will cause the code inside to be run again when its value changes. The problem looks, at first glance, to be in the following part of your code:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
sortPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
sortPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption]);
The [selectedOption] is telling the hook to only do the sorting if the sorting order has changed. However, you want to call this hook if the order or the contents changes. As such, you want to replace this array with [selectedOption, programs] so that changes to the contents of the programs variable will also lead to the sorting being re-run.
If programs is updated in the hook and also set by the hook, this leads to a recursive call which is not good. Instead, let's change the displayed value to be a new variable (defined with useState) called sortedPrograms. Then your hook should look like this:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);
I was watching a tutorial on how to make todos, though my main focus was local storage use.
But when he made the delete button then I was a bit confused, the code below shows how he did it but I am not getting it.
Can anyone explain that I tried using the splice method to remove items from the array but I am not able to remove the items from the page?
Can you also suggest what should I do after using splice to return the array on the page?
Below is the code,
import "./styles.css";
import { useState, useEffect } from 'react'
import Todoform from './TodoForm'
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (e) => {
// TODO: items.splice(e-1, 1);
// Is there any other way I can do the below thing .i.e
// to remove todos from page.
// this is from tutorial
setitems((e1)=>{
return e1.filter((er , index)=>{
return index!=e-1;
})
})
}
return (
<>
<div className='display_info'>
<h1>TODO LIST</h1>
<br />
<input onChange={itemevent} value={list} type="text" name="" id="" />
<br />
<button onClick={listofitem} >Add </button>
<ul>
{
items.map((e, index) => {
index++;
return (
<>
<Todoform onSelect={deleteItems} id={index} key={index} index={index} text={e} />
</>
)
})
}
</ul>
</div>
</>
)
}
And this is the TodoForm in this code above,
import React from 'react'
export default function Todoform(props) {
const { text, index } = props;
return (
<>
<div key={index} >
{index}. {text}
<button onClick={() => {
props.onSelect(index)
}} className="delete">remove</button>
</div>
</>
)
}
Here is the codeSandbox link
https://codesandbox.io/s/old-wood-cbnq86?file=/src/TodoForm.jsx:0-317
I think one issue with your code example is that you don't delete the todo entry from localStorage but only from the components state.
You might wanna keep localStorage in sync with the components state by using Reacts useEffect hook (React Docs) and use Array.splice in order to remove certain array elements by their index (Array.splice docs).
// ..
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
/* As this `useEffect` has an empty dependency array (the 2nd parameter), it gets called only once (after first render).
It initially retrieves the data from localStorage and pushes it to the `todos` state. */
useEffect(() => {
const todos = JSON.parse(localStorage.getItem("notes"));
setitems(todos);
}, [])
/* This `useEffect` depends on the `items` state. That means whenever `items` change, this hook gets re-run.
In here, we set sync localStorage to the current `notes` state. */
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(items));
}, [items])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (index) => {
// This removes one (2nd parameter) element(s) from array `items` on index `index`
const newItems = items.splice(index, 1)
setitems(newItems)
}
return (
<>
{/* ... */}
</>
)
}
There are multiple ways to remove an item from a list in JS, your version of splicing the last index is correct too and it is able to remove the last item. What it can't do is update your state.
His code is doing two things at the same time: Removing the last item of the Todo array and then, setting the resulted array in the state which updates the todo list.
So, change your code as
const deleteItems = (e) => {
let newItems = [...items];
newItems.splice(e-1, 1);
setitems(newItems);
}
I am passing the increment function to a list of children (Row), but the count is never actually changed, I know that something about doing this in the children's useEffect is off. But I am still not able to understand this behavior.
Also, I am not setting the dependency array, because in this case, the count will run infinitely.
import { useCallback, useEffect, useState } from "react";
import "./styles.css";
const list = ["One", "Two", "Three"];
export default function App() {
const [count, setCount] = useState(0);
const handleOnClick = useCallback(() => {
setCount(count + 1);
}, [count]);
useEffect(() => {
if (list.length === count) {
alert("yaaay!");
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row = ({ addOne, name }) => {
useEffect(() => {
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <p>{name}</p>;
};
The output is:
Count is: 1
One
Two
Three
Expected:
Count is: 3
One
Two
Three
okay i've created a demo
Couple of things. first, you need to set the useState values through the callback function since we are updating the counter value continuously
Secondly, need to use useRef in the child component to make sure child component is visited In each iteration. Do the checking inside child component useEffect
export default function App() {
const [count, setCount] = React.useState(0);
const handleOnClick = React.useCallback(() => {
setCount((count) => count + 1);
}, [count, setCount]);
React.useEffect(() => {
if (list.length === count) {
alert('yaaay!');
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row =({ addOne, name }) => {
const dataFetchedRef = React.useRef(false);
React.useEffect(() => {
if (dataFetchedRef.current) return;
dataFetchedRef.current = true;
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
return <p>{name}</p>;
};
I have got a dependency imageNo in useEffect() as I want the element to go up when it's being hidden, but scrollIntoView() does not work properly whenever imageNo changes, but it works when clicking a button.
Updated
import React, { useEffect, useRef, useState } from 'react';
const Product = ({ product }) => {
const moveRef = useRef(product.galleryImages.edges.map(() => React.createRef()));
const [imageNo, setImageNo] = useState(0);
useEffect(() => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
console.log('imageNo', imageNo); // <<<<----- This is also called whenever scrolling excutes.
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [imageNo]);
const test = () => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
// This changes `imageNo`
const handleScroll = () => {
let id = 0;
console.log('refs.current[id]?.current?.getBoundingClientRect().y', refs.current[id]?.current?.getBoundingClientRect().y);
const temp = imgArr?.find((el, id) => refs.current[id]?.current?.getBoundingClientRect().y >= 78);
if (!temp) id = 0;
else id = temp.id;
if (refs.current[id]?.current?.getBoundingClientRect().y >= 78) {
setImageNo(id);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div className="flex flex-row layout-width ">
{/* aside */}
<div className="sticky flex self-start top-[76px] overflow-y-auto !min-w-[110px] max-h-[90vh]">
<div className="">
{product.galleryImages.edges.map((image, i) => {
return (
<div ref={moveRef.current[i]} key={image.node.id}>
<Image />
</div>
);
})}
</div>
</div>
<button onClick={test}>btn</button>
</div>
);
};
export default Product;
Any suggestion will be greatly appreciated.
I couldn't see where the imageNo is coming from?
If it is just a normal javascript variable then it wouldn't trigger re-render even after putting it inside the useEffect's dependencies array.
If you want to make the re-render happen based on imageNo then make a useState variable for imageNo and change it using setter function that useState provides.
Helpful note - read about useState and useEffect hooks in React.
Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes