React - toggle css class but do not rerender everything - reactjs

I have a Gatsby website and I want to use it to show flashcards. I have found multiple existing solutions online, but I would like to try it a different way. However, I am completely stuck.
Desired result:
Show one random word at a time. When the users clicks on a button 'show', the meaning of the word should be shown.
Issue:
I can generate a random combination of the words. But when the user clicks on 'show', a new combination is loaded instead of the answer. I understand why this happens. However, I do not understand how to fix this.
I have tried this (stripped to the bare essentials):
export const Flashcard = () => {
const [flip, setFlip] = useState(false)
const onButtonClick = () => {
setFlip(!flip)
}
let word = words[GenerateRandomNumber(0, words.length - 1)] // words is an array containing all the possible combinations
return (
<div className={`card ${flip ? "flip" : ""}`}>
<div className="front">
{word["language1"]}
</div>
<div className="back">
{word["language2"]}
</div>
<button onClick={onButtonClick}>Show answer</button>
</div>
)
}
export default Flashcard
I feel like I am on a totally wrong path, but don't see how to fix it. Should I structure my logic different? Should I use some other hooks? Or anything else?
In addition, I would also like the user having the possibility to say he knew the meaning (clicking on a button 'correct') or not (button 'wrong'). If correct, a new word is shown, if wrong, the word is stored to be shown later again. How should I trigger this logic?

I think you should add a state to store the picked word
const [word, setWord] = useState("");
and When you do a click you need to get a random word and store in word
useEffect(() => {
const randomNumber = GenerateRandomNumber(0, words.length - 1);
const newWord = words[randomNumber];
setWord(newWord);
}, [flip]); //This useEffect will call everytime when flip change.
in return you can refer the word
{word["language1"]}
here is a demo

Related

React event listener callback functions don't use updated states

When accessed from reDraw the resizableList is an empty array, when accessed from addImageClick it shows the actual array. the TextInput element contains a text input which calls eventBus.dispatch('addtext') on change.
So, after I add an image, I have two TextInput elements, and two Resizable elements. I change the text in one of the TextInput elements, empty state array. I trigger the add image button which logs the array before resetting it, array has two elements in it before reset.
import React,{useState,useRef,useEffect} from "react";
import Resizable from "./Resizable";
import TextInput from "./TextInput";
import eventBus from "../eventbus/EventBus";
export default function Meme(){
const [image, setImage] = useState(new Image());
const [resizableList, setResizableList] = useState([]);
const hiddenFileInput = useRef(null);
const cvs = useRef(null);
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
for (let i=0;i<resizableList.length;i++){
let el = document.getElementById(`${i}--textbox`);
console.log('test');
}
console.log('ran');
console.log(image);
}
function addResizable(width){
let resizable = {
cvs:cvs,
imgwidth:width,
image:image
}
setResizableList((prevResizableList)=>[...prevResizableList, resizable]);
}
function draw() {
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
console.log(image.width);
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
addResizable(width);
addResizable(width);
};
function addImageClick(e){
e.preventDefault();
console.log(resizableList);
setResizableList([]);
hiddenFileInput.current.click();
};
useEffect(()=>{
eventBus.on('addtext',reDraw);
image.onload = draw;
console.log('useeffect');
},[])
return (
<main>
<form className="form">
<div className='form--textboxes'>
{resizableList.map((item,index)=>{
return (<TextInput cvs={item.cvs} image={item.image} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
<button className='form--button--textboxes'>Add Text</button>
</div>
<button className="form--button" onClick={addImageClick}>Add Image</button>
<div className="form--image">
<canvas id='meme' ref={cvs} className="form--meme" height="600" width="1200"></canvas>
{resizableList.map((item,index)=>{
return (<Resizable cvs={item.cvs} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
</div>
</form>
<input
type="file"
name="myImage"
style={{display:'none'}}
ref={hiddenFileInput}
onChange={(event) => {
image.src = URL.createObjectURL(event.target.files[0]);
}}
/>
</main>
)
};
I apologize if the explanation above is too long. So far I've been able to get by just by reviewing questions, it's the first time I actually had to write one.
Update: What I basically want to do is loop over the resizableList array, and update the text on the canvas based on the position and size of each resizable. I know how to do that but I can't, because when I access the array from the reDraw function, the array shows as empty. I tried looking for alternatives but I couldn't find any. I create an id for each textbox/resizable of format: arrayindex--textbox and arrayindex--box. That's how the textboxes and resizables relate to eachother.
2nd Update: Sooooooo...
function addTextClick(e){
e.preventDefault();
console.log(resizableList);
}
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
button.current.click();
}
As I said previously, reDraw is triggered by an event fired by the text input's onChange. I also made this button which has the addTextClick as it's onClick. And yes, I trigger input's onchange, I get one empty array followed by one length 2 array(which is the resizableList array) in my console. ?????
I guess this kinda fixes my problem as I can just create a hidden button but this is more of a workaround and I would still like to understand why this is happening.
Updated with solution:
So, I ran into this same issue while working on some other functionality of my webapp.
The problem is that when you add an event listener the callback function is set with the state values present at the time. So it doesn't matter how many times your states are updated after setting the event listener, it's still going to use the states it had when it was set up.
Funny enough, after understanding what was causing the problem I found some posts from others running into this problem and using exactly the same workaround I used above, which is to create a reference to a hidden button and have the event listener's callback function trigger the onClick.
Another solution is to use the useEffect hook and recreate the event listener every time a state is updated, but in my case that doesn't really work that well since I have a lot of states that are updated constantly.

Handling form with single state in React for web app

I have an idea to create simple web app for my own needs. I would like user to be able to use picklist to define number of form fields needed (ex. 1 - 10). Picklist would create chosen number of components with text inputs. User can type string into text input and all data would be stored as single state. I could do it just simply saying string1, string2 and having 10 states but I wonder if this would be doable as single state because I would like all text inputs to create single string in form of token.
Example:
User choose 3 text inputs:
hi
hello
goodbye
This would form token of {hi/hello/goodbye}.
Please let me know how to handle that with React.
Thanks.
Probably you can solve this using an array of objects inside your state and latter assessing to the values. I'm not sure what do you want to do, but if you want to create an app that generates dynamically inputs and after set the values in the corresponding state, you can do something like this.
export default function DinamicLinks() {
const [quantity, setQuantity] = useState(0)
const [messages, setMessages] = useState([])
const createInputs = (quantity) => {
const params = []
for (let i = 1; i <= quantity; i++){
let param = {name:i};
params.push(param)
}
return params
}
const handleMessages = (passedMessage) => {
const filteredMessages = messages.filter((message) => message.name !== passedMessage.name)
setMessages([...filteredMessages, passedMessage])
}
return (
<div className="App">
<button onClick={() => setQuantity(quantity + 1)}>More inputs</button>
<button onClick={() => setQuantity(quantity - 1)}>Less inputs</button>
{createInputs(quantity).map(({name}) => <input key={name} onChange={(e) => handleMessages({name, message:e.currentTarget.value})}></input>)}
<button onClick={() => console.log(messages)}>See messages</button>
</div>
);
}
This code will go to generate inputs dynamically and will go to store his values inside an state, that store objects inside an array, later you can implement the logic to handle the text in the way you whatever you want. By the way, this code isn't the best in performance, but I want to give you and easy solution quickly. Undoubtedly there are better ways to do this, and also is valid to mention, don't abuse the arrow functions like I did in this example, are an easy ways to have trouble of performance in React.

React hooks - Prevent rerender if parent state that holds children props changed

I have an issue with my current project that uses react hooks.
What I'm trying to do is just to select my tasks by using (shift+click). Look like this:
Here is the code:
...
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
const selectTask = useCallback(
(e: MouseEvent<HTMLDivElement>, taskId: string): void => {
e.stopPropagation()
const previousTaskId = selectedTaskIds[selectedTaskIds.length - 1]
if (previousTaskId && e.shiftKey) {
// handle shift+click
const previousIdx = tasks.findIndex((task) => task.id === previousTaskId)
const selectedIdx = tasks.findIndex((task) => task.id === taskId)
const rangeTasks =
previousIdx < selectedIdx
? tasks.slice(previousIdx, selectedIdx + 1)
: tasks.slice(selectedIdx, previousIdx + 1)
const rangeIds = rangeTasks.map((task) => task.id)
setSelectedTaskIds([...new Set([...selectedTaskIds, ...rangeIds])])
} else {
// if no key clicked, just select 1 task item
setSelectedTaskIds([taskId])
}
},
[selectedTaskIds, tasks] // <==== in here I notice that activeTaskIds is changed overtime that causes all of my <TaskItem> rerender
)
return (
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={selectTask} // <=== selectTask will be different if user click on one of the task items
active={selectedTaskIds.includes(task.id)}
/>
))}
)
The problem is, to know which tasks should I select when the user uses shift+click, I need to know the currently selected task ids, so that I need to pass selectedTaskIds as a useCallback() deps.
That makes whenever the user selects the tasks or even just a click on one of the task items to select the task, it will re-render all of my <TaskItem> since the selectTask() function change due to useCallback's deps changed.
How can I solve this without rerender all of my <TaskItem>s? Thank you so much!
I tested your code on my machine and tested out a few scenarios. As far as I can tell, it looks natural for the component to re-render the all of the <TaskItem>s because any change in the selectedTaskIds state will guarantee everything inside the component that holds selectedTaskIds to render. To show you a concrete example,
<div className="App">
<TaskItems />
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
Let's say you have the above code. (I named your component that holds multiple <TaskItem/>s as <TaskItems/>) When onClick of <TaskItem/> triggers, only <TaskItems/> will re-render. The two other divs are not re-rendered. However, if you place the two divs inside the <TaskItems/> component, they will re-render:
// assuming this is inside <TaskItems/>
...
return (
<div>
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={(e) => { selectTask2(e, task.id)}} // <=== selectTask will be different if user click on one of the task items
// active={selectedTaskIds.includes(task.id)}
active={true}
title={task.title}
/>
))}
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
);
above code will re-render the two divs.
I have tried to fulfill your request to get rid of the re-renders of the tasks that weren't changed, but it was really hard to do so. When I try to prevent re-rendering I usually use one of the two techniques:
create a child component and separate the code base to isolate groups of states. (since states are what triggers renders, you can
separate unrelated ones into different groups.)
useCallback/useMemo
Either techniques I failed to implement for your case, but there may be a way to apply the above techniques. I will follow the thread to see if anyone else gets a solution.

Toggle property of different states

Small question here.
I have 3 div classes that contain images that I want to toggle the state property for each picture every time a user presses on one of the images (div actually).
The states are as follows:
constructor(props) {
super(props);
this.state = {
img_1: 0,
img_2: 0,
img_3: 0
};
}
And I want to have a single handle function that can toggle for all the different images that I have.
Here is the div code (its the same for every image):
<div className="pics" onClick={(e) => this.handlePic(e)}>
<h2>First picture</h2>
<img alt="" src={pic1} className="tier2"/>
</div>
And The handle function is empty at the moment, because I have no idea how to pass into it the name of this.state.img_1. The value of course should toggle between 0 and 1, but I want to be able to use a single function for the toggle of all 3 images.
I am not sure if my question makes a lot of sense, please let me know if you want me to explain a little bit more of my situation.
Thank you!
You have 3 different div, in each of your dives define an onClick={()=>this.myhandle(nameofpic)} (you can hardcode nameofpic here,for example in each div put a name like "img1","img2" ... instead of nameofpic)
Then in your myhandle() put your ifs ,like this :
myhandle(nameofpic){
if(nameofpic==="img_1")
{this.setState({img_1:"valuechange"})
}
if(nameofpic==="img_2")
{this.setState({img_2:"valuechange"})
}
if(nameofpic==="img_3")
{this.setState({img_3:"valuechange"})
}}
I hope you get the idea and works for you
You should create a separate component for Image, which will handle it's own state. This way you can manage any number of Images not just 3.
function Image({url}) {
const [overlay, setOverlay] = useState(false);
const toggleState = () => {
setOverlay(!overlay)
}
return <img src={url} alt={url} onClick={toggleState} />
}
Here overlay can be anything.
Now you can use this component any number of time and it will have it's own state management.
Here is the solution if you are not using hooks then,
In render method(passing static string in parameter for handler method):
<div className="pics" onClick={e => this.handlePic('img1')}>
<h2>First picture</h2>
<img alt="" src={pic1} className="tier2" />
</div>
Your handler(Setting dynamic key):
handlePic = key => {
this.setState(prevState => ({ [key]: !prevState[key] })); // This will toggle 0 or 1 simultaneously.
};

React design question about creating remove functionality

This is a little long winded.
I would like advice on how to go about making the functionality of a remove button. I'm relatively new to react and should have planned this project before I embarked. Either way I am here now and looking for some advice before I remove functionality tomorrow.
In this component I create a method called "displayFood" in which I take an array from props that has in it string values of the names of foods that the user wanted to add to the refrigerator. For example: [yogurt, milk, egg, yogurt, yogurt]. I then create an object that maps this array to key value pairs based on name and quantity, for example: {"yogurt": 3, "milk": 1, "egg": 1}. After this I create an array that holds what I want to render to the user which is each item that they put in the fridge and the quantity of that item. I also have a remove button. I have been thinking about how to remove Items but do not know how to go about doing so.
If, for example the user deletes yogurt I want the value to decrease by one, and if the user deletes a item with quantity 1 then it should go away.
This is a pretty specific question thank you for your time.
import React from 'react';
import "./style.scss";
function InFrige(props) {
const handleRemove = (e, counts) => {
console.log(e.target.name)
}
const displayFood = () => {
var counts = {};
props.foodAddedByUser.forEach(function(x) { counts[x] = (counts[x] || 0) + 1; });
let foodItems = []
for(var index in Object.entries(counts)){
foodItems.push(
<div className="inFrige-food-item" key={index}>
<h3>{Object.keys(counts)[index]} x{Object.values(counts)[index]}</h3>
<button onClick={handleRemove} name={Object.keys(counts)[index]}>Remove</button>
</div>
)
}
return foodItems
}
return (
<div>
<div className="inFrige-food-container">
{displayFood()}
</div>
</div>
)
}
export default InFrige;
Your problem is you are trying to alter your props from within the component. You could either handle this inside the component with state or give a callback via props from the parent component and handle the deletion there, something like this:
<button onClick={()=>{this.props.handleRemove(Object.keys(counts)[index])}} name={Object.keys(counts)[index]}>Remove</button>
in parents render:
<InFridge handleRemove={(item)=>{foodAddedByUser.delete(item)} foodAddedByUser={foodAddedByUser} />

Resources