Let's say I have 2 react elements: componentSender and componentReceiver. That need to be generated in a loop N times.
The special thing they have is that every time someone click in one componentSender, a prop will change in the respective componentReceiver.
This pair of components could be as simple as:
function ComponentReceiver(props) {
return (
<div>{`Listening to "Sender ${props.indexVar}" and it last received: ${props.showVar}`}</div>
);
}
function ComponentSender(props) {
return (
<input type="button" onClick={() => {props.onChangeValue(props.indexVar);}}
value={`SENDER for ${props.indexVar}> `}
/>
);
}
I am using React.createElement in a loop and creating the pairs, you can see it here:
https://codepen.io/danieljaguiar/pen/bGVJbGw?editors=1111
The big problem in my demo is that, when I change the state in the parent (APP), the child components don't re-render.
You have to fix up few things:
try not to store jsx in state. Iterate and render directly in render.
in handleChangeValue function, the show state reference is not changed at all and hence the component is not re-rendered. Make sure to take a copy of show (use spread operator) and then update state.
remove unnecessary code in useEffect and
Working & simplified copy of your code is here in the codesandbox
Code Snippet with fixes
function ComponentReceiver(props) {
return (
<div>{`Listening to "Sender ${props.indexVar}" and I received: ${
props.showVar
}`}</div>
);
}
function ComponentSender(props) {
return (
<input
type="button"
onClick={() => {
props.onChangeValue(props.indexVar);
}}
value={`SENDER for ${props.indexVar} ----------------------> `}
/>
);
}
export default function App() {
const [show, SetShow] = React.useState([]);
const [pageElements, setPageElements] = React.useState([]);
const handleChangeValue = val => {
const updatedShow = [...show];
updatedShow[val] = !updatedShow[val];
SetShow(updatedShow);
};
React.useEffect(() => {
let index = 0;
let elements = [];
while (index < 5) {
show[index] = true;
SetShow([...show]);
elements.push(
<div key={index} style={{ display: "flex", margin: "20px" }}>
<ComponentSender
key={index + "s"}
indexVar={index}
onChangeValue={handleChangeValue}
/>
<ComponentReceiver
key={index + "R"}
indexVar={index}
showVar={show[index]}
/>
</div>
);
index++;
SetShow([...show]);
}
setPageElements(elements);
}, []);
return (
<div>
{[...Array(5).keys()].map((_, index) => {
return (
<div key={index} style={{ display: "flex", margin: "20px" }}>
<ComponentSender
key={index + "s"}
indexVar={index}
onChangeValue={handleChangeValue}
/>
<ComponentReceiver
key={index + "R"}
indexVar={index}
showVar={show[index]}
/>
</div>
);
})}
</div>
);
}
Related
I'm trying develop a little app in which on you can select multiple music album, using Next.js.
I display my albums like the image below, and I would like to add a check mark when clicked and hide it when clicked again.
My code looks like that :
import Image from "next/image";
import {Card,CardActionArea} from "#mui/material";
import { container, card } from "../styles/forms.module.css";
import album from "../public/album.json"
export default function Album() {
const albumList = {} ;
function addAlbum(albumId, image){
if ( !(albumId in albumList) ){
albumList[albumId] = true;
//display check on image
}
else{
delete albumList[albumId]
//hide check on image
}
console.log(albumList)
}
return (
<div className={container}>
{Object.keys(album.albums.items).map((image) => (
<Card className={card}>
<CardActionArea onClick={() => addAlbum(album.albums.items[image].id)}>
<Image alt={album.albums.items[image].artists[0].name} width="100%" height="100%" src={album.albums.items[image].images[1].url} />
</CardActionArea>
</Card>
))}
</div>
);
}
I know I should use useState to do so, but how can I use it for each one of my albums?
Sorry if it's a dumb question, I'm new with Hook stuff.
I think there are a few ways to go about this, but here is a way to explain the useState in a way that fits the question. CodeSandbox
For simplicity I made a Card component that knowns if it has been clicked or not and determines wither or not it should show the checkmark. Then if that component is clicked again a clickhandler from the parent is fired. This clickhandle moves the Card into a different state array to be handled.
The main Component:
export default function App() {
const [unselectedCards, setUnselectedCards] = useState([
"Car",
"Truck",
"Van",
"Scooter"
]);
const [selectedCards, setSelectedCards] = useState([]);
const addCard = (title) => {
const temp = unselectedCards;
const index = temp.indexOf(title);
temp.splice(index, 1);
setUnselectedCards(temp);
setSelectedCards([...selectedCards, title]);
};
const removeCard = (title) => {
console.log("title", title);
const temp = selectedCards;
const index = temp.indexOf(title);
temp.splice(index, 1);
setSelectedCards(temp);
setUnselectedCards([...unselectedCards, title]);
};
return (
<div className="App">
<h1>Current Cards</h1>
<div style={{ display: "flex", columnGap: "12px" }}>
{unselectedCards.map((title) => (
<Card title={title} onClickHandler={addCard} key={title} />
))}
</div>
<h1>Selected Cards</h1>
<div style={{ display: "flex", columnGap: "12px" }}>
{selectedCards.map((title) => (
<Card title={title} onClickHandler={removeCard} key={title} />
))}
</div>
</div>
);
}
The Card Component
export const Card = ({ onClickHandler, title }) => {
const [checked, setChecked] = useState(false);
const handleClickEvent = (onClickHandler, title, checked) => {
if (checked) {
onClickHandler(title);
} else {
setChecked(true);
}
};
return (
<div
style={{
width: "200px",
height: "250px",
background: "blue",
position: "relative"
}}
onClick={() => handleClickEvent(onClickHandler, title, checked)}
>
{checked ? (
<div
id="checkmark"
style={{ position: "absolute", left: "5px", top: "5px" }}
></div>
) : null}
<h3>{title}</h3>
</div>
);
};
I tried to make the useState actions as simple as possible with just a string array to help you see how it is used and then you can apply it to your own system.
You do not need to have a state for each album, you just need to set albumList as a state:
const [albumList, setAlbumList] = setState({});
function addAlbum(albumId, image) {
const newList = {...albumList};
if(!(albumId in albumList)) {
newList[albumId] = true;
} else {
delete albumList[albumId]
}
setAlbumList(newList);
}
And then in your loop you can make a condition to display the check mark or not by checking if the id is in albumList.
I'm trying to display fields based on the value of a props so let's say my props value = 2 then I want to display 2 inputs but I can't manage to get it work.
This is what I tried
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
<div>
<p style={{color:'black'}}>Tier {tier + 1}</p>
<InputNumber />
</div>
})
}
const onPropsValueLoaded = (value) => {
let tmp = value
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(value).keys()
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}
useEffect(() => {
onPropsValueLoaded(props.numberOfTiers);
}, [])
return (
<>
<Button type="primary" onClick={showModal}>
Buy tickets
</Button>
<Modal
title="Buy ticket"
visible={visible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<p style={{ color: 'black' }}>{props.numberOfTiers}</p>
{loadFields.length ? (
<div>{addField()}</div>
) : null}
<p style={{ color: 'black' }}>Total price: </p>
</Modal>
</>
);
so here props.NumberOfTiers = 2 so I want 2 input fields to be displayed but right now none are displayed even though loadFields.length is not null
I am displaying this inside a modal (even though I don't think it changes anything).
I am doing this when I load the page that's why I am using the useEffect(), because if I use a field and update this onChange it works nicely.
EDIT:
I changed the onPropsValueLoaded() function
const generateArrays = Array.from({length : tmp}, (v,k) => k)
instead of
const generateArrays = Array.from(value).keys()
There are couple of things you should fix in here,
First, you need to return div in addField function to render the inputs.
Second, you should move your function onPropsValueLoaded inside useEffect or use useCallback to prevent effect change on each render.
Third, your method of creating array using Array.from is not correct syntax which should be Array.from(Array(number).keys()).
So the working code should be , I also made a sample here
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
return (
<div key={tier}>
<p style={{ color: "black" }}>Tier {tier + 1}</p>
<input type="text" />
</div>
);
});
};
useEffect(() => {
let tmp = 2; // tier number
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(Array(tmp).keys());
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}, [numberOfFields]);
return (
<>
<button type="button">Buy tickets</button>
<p style={{ color: "black" }}>2</p>
{loadFields.length ? <div>{addField()}</div> : null}
<p style={{ color: "black" }}>Total price: </p>
</>
);
}
I am learning react and i am trying to build the tic tac toe game using the documentation, however using functional components on my own.
This is what I have done till now
app.js
function App() {
return (
<div className="App">
<Game />
</div>
);
}
Game.js
const styles = {
width: "200 px",
margin: "20px auto",
};
function Game() {
const [board, setBoard] = useState(Array(9).fill(""));
const [xIsNext, setXIsNext] = useState(true);
const winner = calculateWinner(board);
const handleClick = (i) => {
const boardCopy = [...board];
if (winner || boardCopy[i]) return;
boardCopy[i] = xIsNext ? "X" : "O";
setBoard(boardCopy);
setXIsNext(!xIsNext);
};
function renderMoves() {
return <button onClick={() => Array(9).fill(null)}> Start Game</button>;
}
return (
<>
<Board onClick={handleClick} squares={board} />
<div style={styles}>
<p>
{winner ? "Winner " + winner : "Next player" + (xIsNext ? "X" : "O")}
{renderMoves()}
</p>
</div>
</>
);
}
Board.js
function Board({ onClick, squares }) {
const style = {
border: "4px solid darkblue",
borderRadius: "10px",
width: "250px",
height: "250px",
margin: "0 auto",
display: "grid",
gridTemplate: "repeat(3, 1fr) / repeat(3, 1fr)",
};
return (
<div style={style}>
{squares.map(
(square, i) => (
console.log(square),
(
<Square
key={i}
value={square}
onClick={() => onClick("dummy value")}
/>
)
)
)}
</div>
);
}
Button.js
function Square({ value, onClick }) {
return (
<button style={style} onClick={onClick}>
{value}
</button>
);
}
At this point in the code the game is functional and user should be able to play the game.
However, in my current code, when you click on Square nothing appears.
I checked in Game.js, the logic for handleClick() is same as the online documentation. These lines handle the logic for showing "X" and "O"
const boardCopy = [...board];
if (winner || boardCopy[i]) return;
boardCopy[i] = xIsNext ? "X" : "O";
setBoard(boardCopy);
setXIsNext(!xIsNext);
it creates a shallow copy of the original board and updates the original board state with the new one.
in Board.js, I put a console.log(square) to see what is being passed here, this shows NULL.
After the state is updated the console.log() show say X or O right?
What am I doing wrong here?
PS - I also one side question,
1)since I am new to this what is the difference of use between writing functions like below, both work perfectly so which should be used when?
function test({i}){
}
and
const test = (i) =>
{
}
here you are not doing nothing onClick, looks like onClick takes a parameter "i" but your passing "dummy value"
<Square
key={i}
value={square}
onClick={() => onClick("dummy value")}
/>
it should be something like
function Board({ handleClick, squares }) {
<Square
key={i}
index={i}
value={square}
handleClick={handleClick}
/>
function Square({ value, index, handleClick}) {
return (
<button style={style} onClick={() => handleClick(index)}>
{value}
</button>
);
}
Hi I have been using this package react-to-print to print document and it works really well. Passing value to child component works and I can print the dynamic data too. However, I am facing problem to pass dynamic data of array list. It always gets the last item of array. I wrote an example, please take a look at it
import * as React from "react";
import { useRef } from "react";
import ReactToPrint from "react-to-print";
const ComponentToPrint = React.forwardRef((props, ref) => {
const { value } = props;
return (
<div className="print-source" ref={ref}>
Number {value}
</div>
);
});
export default function App() {
const componentRef = useRef();
const numbers = [1, 2, 3, 4, 5];
return (
<>
{numbers.map(function (item, index) {
return (
<div style={{ display: "flex" }}>
<li key={index}>{item}</li>
<ReactToPrint
trigger={() => <button type="primary">Print</button>}
content={() => componentRef.current}
/>
<ComponentToPrint ref={componentRef} value={item} />
</div>
);
})}
</>
);
}
Live Demo
Whenever I click the print button, I expect to send the unique value of number to child component but every time I am getting the last value of array. What am I doing wrong?
Because there's just one componentRef instance, which on the order of rendering will have the last rendered value.
Instead each returned component from App needs to have its own instance of componentRef.
This can be achieved if you
make the returned html from App a component too (say ComponentToPrintWrapper)
have this component its own componentRef.
const ComponentToPrintWrapper = ({ item }) => { // 1.
const componentRef = useRef(); // 2.
return (
<div style={{ display: "flex" }}>
<li>{item}</li>
<ReactToPrint
trigger={() => <button type="primary">Print</button>}
content={() => componentRef.current}
/>
<ComponentToPrint ref={componentRef} value={item} />
</div>
);
};
Use ComponentToPrintWrapper on your App instead
...
export default function App() {
const numbers = [1, 2, 3, 4, 5];
return (
<>
{numbers.map(function (item, index) {
return <ComponentToPrintWrapper key={index} item={item} />;
})}
</>
);
...
}
This will ensure each return element has its own componentRef instead.
CodeSandbox
Please help me out.
I want to add dynamically extra input fileds and save the values into textValues state.
Here is what I tried until now:
const [textValues, setTextValues] = useState([]);
const [numberOfTexts, setNumberOfTexts] = useState(5); <--- this value will be change dynamically
{[...Array(numberOfTexts)].map((x, i) => (
<TextField
style={{ marginLeft: "10px", marginTop: "10px" }}
id="standard-basic"
label="Text value"
value={textValues[i]}
onChange={(e: { target: { value: any } }) =>
setTextValues(e.target.value)
}
/>
))}
So, in this case I want appear 5 input fields and in the textValues array to be 5 empty strings, than fill in step by step. I want to be able to increase and decrease the numberOfTexts, and update everything according to the number of texts. Thank you for your help!
For simple usecase you don't need to have 2 useState to handle the number of fields and their values.
Usually this is a best practice to define the minimal data set model.
You can write abstract function on top of setTextValues, it should do the trick ;)
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [textValues, setTextValues] = useState([]);
const addField = () => {
setTextValues(textValues.concat([""]));
};
const updateField = (index, newValue) => {
const newValues = textValues.map((val, idx) => {
if (idx === index) {
return newValue;
}
return val;
});
setTextValues(newValues);
};
const removeField = () => {
setTextValues(textValues.slice(0, textValues.length - 1));
};
return (
<div>
<button onClick={() => addField()}>Add field</button>
<button onClick={() => removeField()}>Remove (last) field</button>
<br />
<div>
{textValues.map((textValue, idx) => (
<input
style={{ marginLeft: "10px", marginTop: "10px" }}
id="standard-basic"
label="Text value"
value={textValue}
onChange={(e) => {
updateField(idx, e.target.value);
}}
/>
))}
</div>
</div>
);
}
To go further, you may want to create custom hook to abstract this logic, and have reusable chunk of state. The API may look like this:
const { addField, removeField, updateField } = useInputArray(['hello', 'yo'])
One way to do this in a way that doesn't require too many changes to your code so far is to use a useEffect() hook to update the string array as numberOfTexts changes.
You could do this by adding this code:
useEffect(() => {
let dif = numberOfTexts - textValues.length;
if (dif > 0) {
setTextValues(textValues.concat(Array(dif).fill('')));
} else if (dif < 0) {
setTextValues(textValues.slice(0, numberOfTexts));
}
}, [numberOfTexts]);
This will dynamically update the textValues array based on the numberOfTexts array.