React - which function is closest to the solution - i want duplicate components - javascript

I would like to do something like this to start the function of creating a new component after clicking on the button. Moreover I want the div element with its content to appear every time I click a button. (in my case my own component)
For example when i want to create questionnaire and add questions i need a button who let me add input for this question. You know, duplicate components.
I created 3 functions, but I don't know which one is closest to the solution. Can you help me :c ? 3 days i trying do this. Thanks in advance for your time.
const Adding = () => {
const components = {
button : Button
}
components.map((question, index) => (
<Button key={index}/>
))
}
//customElementsRegistry.define('input-text', Button , { extends: 'div' });
const AddQ = () => {
let counter = 0;
const newEl = document.createElement("input-text"); // ta funkcja tworzy tyle komponentów ile chce ale tylko np divy
newEl.innerText = `przykładowy tekst ${++counter}`;
const div = document.querySelector("#ankieta");
div.appendChild(newEl);
}
function AddQuestion(){
const el = React.createElement("Button", {}, 'My First React Code'); // ta funkcja renderuje mi tylko jeden w tym wypadku button
ReactDOM.render(
el,
document.getElementById('ankieta'))
}

While using React, whenever possible, avoid using original DOM functions. Instead, use React's internal mechanisms to generate and modify the DOM:
// React functional component
const App = () => {
// Maintain a clicked state using `useState`
const [counter, setCounter] = React.useState(0);
// On click, increment counter
const onClick = () => setCounter(counter + 1);
// Generate the array of texts
// Use `useEffect` to only recalculate when `counter` changes
const inputTexts = React.useEffect( () =>
(new Array(counter))
.map( (_, index) => `przykładowy tekst ${index}` ),
[counter]);
return (
<>
<Button onClick={ onClick }>
{/* The DIV where you append the generated inputs */}
<div id="ankieta">
{
// Generate the inputs from the texts generated earlier
inputTexts
.map( (value, index) => <input key={index} value={value} />)
}
</div>
</>
)
}
Note: The generated inputs are not being updated and has a fixed value. This means that their value cannot be modified. If you need to be able to modify them, you'll need to create the corresponding onChange function and save the value to some state: instead of using const inputTexts = useEffect(...) you'll need to do some more sophisticated state management, or manage it using a self-managed sub-component.

Related

ReactJS list of components

I am new in JS and ReactJS.
I tried to implement a list of boxes as follows:
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
function GenreBox() {
let [input, setInput] = useState('')
let [repr, setRepr] = useState('')
let getClick = () => {
fetch(`/genre/${input}`).then(
(res) => res.json().then(
(data) => { console.log(data); setRepr(data.repr) }
)
)
}
return (<div>
<input type="text" value={input} onChange={(e) => setInput(e.target.value)}></input>
<button onClick={getClick}>Get</button>
<p>repr: {repr}</p>
</div>
)
}
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox />);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li>{x}</li>)}
</ol>
</div>)
}
export { GenreBox, GenreBoxList }
When I click the + and - button with index == 0,
I expect the front of the list to be modified.
However, it appears that no matter what number I set the index to,
it is always operating on the tail of the list...
What am I doing wrong or is this a bad design practice?
EDIT 1:
OK, it seems to be the problem with the key. React seems to treat objects with the same type and key to be equal and hence does not update the page.
EDIT 2:
Now I have added keys to both and and it seems to be functioning correctly. So is this how react is proposed to be used?
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let [counter, setCounter] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox key={counter} ></GenreBox>);
setCounter(counter + 1);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li key={x.key}>{x}</li>)}
</ol>
</div>)
}
As you realized in your edit, the key here is key.
Your list is an array of items where each item is created by the expression <GenreBox {...props} /> (which is in fact translated into React.createElement(GenreBox, props) ). When React sees an array of such, say, 10 items - it has no way to know which of them was added first. All it knows is that there are 10 of them.
For a moment, let's ignore the fact that the code later wraps each of them inside it's own <li> element, and assume we are rendering the array as-is into the <ol> container.
React sees there are 10 items of the component that should be rendered, and it invokes the rendering function for each. That function also uses state via useState() so React has to pass the correct state to each render. React looks in the state data remained from the previous render, and sees that there are 9 sets of state data since there were only 9 items in the previous render. How would it associate each set of state data to a component in the list? The only way would be to provide the first set of state data to the first item, the second set to the second item, etc. and leave the last item to initialize it's own new state.
By providing a unique key attribute, on the other hand, you are giving the item an identity. React would now be able to associate the item with the correct set of state data (and other hooks data as well) regardless of it's position in the list.
(In fact, even if you don't provide a key React would provide one, but this key would simply be the index of the item so everything said above still apply).
Lastly, since the code later maps the original array to a new array where each item is wrapped inside a <li> element, the actual relevant list is this list of <li> items, so the key should be provided there - as you indeed did.
Reference:
https://reactjs.org/docs/reconciliation.html#recursing-on-children

React setState in If Statement

I'm making a chess game using React and I'm trying to write a function that computes the squares that should be highlighted when a certain chess piece is selected. I have the game as one component and each chess piece as a second component, and the button in the chess component calls a function from the game component when clicked.
I want to highlight the appropriate squares if and only if the selected piece is the correct color, so I had a setState call in an if statement in my handleClick() function. After reading Issue with setState() in if statement I moved the conditional so that the handleClick function is only linked to pieces of the correct color.
The issue now is that the state gets changed as desired, but for some reason the components don't rerender to reflect that change. Can someone please let me know how I could fix this? Here's my code for handling clicks:
handleClick(num){
this.setState(prevState => {
return {
gameBoard: prevState.gameBoard,
turn: prevState.turn,
selected: num
}
})
}
and here's my code for creating the board:
<div>
{
this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
}
</div>
try:
handleClick(num){
this.setState(prevState => {
return {
...prevState,
selected: num
};
})
}
Ths issue here is mostly a question of dependencies. Your rendering is based on the gameboard field of the state, which isn't being modified. The component therefore doesn't rerender since it doesn't know that there are additional dependencies (selected) for it.
The easiest and cleanest approach I would suggest is to simply move your map function inside a useCallback (or useMemo, both should work) with the proper dependencies, then simply use it in your rendering method.
const foobar = useCallback( () => this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
, [this.state] )
render (
<div>
{foobar()}
</div>
)

TextField loses focus when State changes

I am dynamically creating a form according to the button clicks, so when the user presses the button, the form generates with a new id. SO, i created a state for the createform counter like this,
const [formmakersno, setFormmakersno] = useState([0]);
and inside my render, I have something like this :
{formmakersno.map((no) => {
return formDetails;
})}
My "formDetails" is a constant with jsx, it just returns the form elements like a textfield and a button. Everything works fine, Everytime i click the button the form generates as expected, But, I want to pass the counter number every-time it creates a new form, as a prop, since i cannot do this when it is just a const which returns a jsx, I converted it into an arrow function so that i can pass props and passed it like this :
{formmakersno.map((no) => {
return <formDetails value={no} />;
})}
And i could access the props too, but the problem is that , everytime I type inside the textfield for each letter the textfield goes out of focus, because its a component it renders everytime because I am calling an Onchange method with a state change inside it. Is there a workaround for this ?
EDIT : I created this simple example to further illustrate the problem I am facing :
export const CreatePush2 = () => {
const [titles, setTitles] = useState(["", ""]);
const [numbers, setNumbers] = useState([0, 1, 2]);
const handleChange = (e, id) => {
const { value } = e.target;
let titles_1 = [...titles];
let titles_11 = titles_1[id];
titles_11 = value;
setTitles(titles_1);
console.log(titles);
};
const InputCreator = ({ id }) => {
return (
<form>
<input type="text" onChange={(e) => handleChange(e, id)} value={titles[id]} />
<br />
</form>
);
};
return (
<div>
{numbers.map((number) => {
return <InputCreator key={number.toString()} id={number} />;
})}
</div>
);
};
export default CreatePush2;
The textfield loses focus with every letter typed.

ReactJS: Component doesn't rerender on state change

I am trying to update state on click event using react hooks. State changes, but component doesn't rerender. Here is my code snippet:
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([])
const DeleteIngredient = (ingredient) => {
let selectedIngredientsContainer = selectedIngredients;
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
setSelectedIngredients(selectedIngredientsContainer);
console.log(selectedIngredients);
}
const selectedIngredientsDiv = selectedIngredients.map(ingredient =>
(
<div className={styles.selectedIngredientsDiv}>{ingredient}
<div className={styles.DeleteIngredient}
onClick={() => {
DeleteIngredient(ingredient)}}>x</div></div>
))
return (
...
What am I doing wrong? Thanks in advance!
Issue with you splice as its not being saved to selectedIngredientsContainer. I would do following:
selectedIngredientsContainer = selectedIngredientsContainer.filter(value => value !== ingredient);
or
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1 );
setSelectedIngredients([...selectedIngredientsContainer]);
Hope it helps.
normally I would leave an explanation on what's going on but tldr is that you should check first to make sure that you're array isn't empty, then you you can filter out the currentIngredients. Also you don't need curly brackets to call that function in the jsx but that can be personal flavor for personal code. I apologize if this doesn't help but I have to head out to work. Good luck!
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([]);
const DeleteIngredient = ingredient => {
// let selectedIngredientsContainer = selectedIngredients;
// selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
// setSelectedIngredients(selectedIngredientsContainer);
// console.log(selectedIngredients);
if (selectedIngredients.length > 0) {
// this assumes that there is an id property but you could compare whatever you want in the Array.filter() methods
const filteredIngredients = setSelectedIngredients.filter(selectedIngredient => selectedIngredient.id !== ingredient.id);
setSelectedIngredients(filteredIngredients);
}
// nothing in ingredients - default logic so whatever you want
// log something for your sanity so you know the array is empty
return;
};
const selectedIngredientsDiv = selectedIngredients.map(ingredient => (
<div className={styles.selectedIngredientsDiv}>
{ingredient}
<div className={styles.DeleteIngredient} onClick={() => DeleteIngredient(ingredient)}>
x
</div>
</div>
));
}
The answer is very Simple, your state array selectedIngredients is initialized with an empty array, so when you call map on the empty array, it will not even run once and thus DeleteIngredient is never called and your state does not change, thus no re-render happens

React Hooks: set component state without re-rendering twice

So I have two components: an Input component which is basically just a button that sets the current status of the input value to active and then sends a value object to its parent component Question:
import React, { useState, useEffect } from 'react';
import './Input.css';
const Input = (props) => {
// input state:
const title = props.title;
const index = props.index;
const [active, setActive] = useState(false);
const [inputValue, setInputValue] = useState({index, title, active});
// sets active status based on what status is in Question component
// the logic there would only allow 1 radio input to be active as opposed to checkboxes where we have multiple active
useEffect(() => {
setActive(props.active);
}, [props.active]);
// stores activity status of single input and re-runs only when 'active' changes (when clicking the button)
useEffect(() => {
setInputValue({index, title, active});
}, [active]);
// returns updated input value to Question component
useEffect(() => {
return props.selected(inputValue);
}, [inputValue]);
return (
<div className='input'>
<button
data-key={title}
className={props.active ? 'highlight' : ''}
onClick={() => setActive(active => !active)}
>
{title}
</button>
</div>
);
}
export default Input;
And Question checks if the current question type (which it receives from another parent component) is a 'radio' button type in which case you can only have one option. So currently I set it up like this:
import React, { useState, useEffect } from 'react';
import s from './Question.css';
import Input from './Input/Input';
const Question = (props) => {
// create intitial state of options
let initialState = [];
for (let i=0; i < props.options.length; i++) {
initialState.push(
{
index: i,
option: props.options[i],
active: false,
}
)
}
// question state:
let questionIndex = props.index;
let questionActive = props.active;
let questionTitle = props.question;
let questionType = props.type;
let [questionValue, setQuestionValue] = useState(initialState);
let [isAnswered, setIsAnswered] = useState(false);
useEffect(() => {
console.log(questionValue);
}, [questionValue]);
// stores currently selected input value for question and handles logic according to type
const storeInputValue = (inputValue) => {
let questionInputs = [...questionValue];
let index = inputValue.index;
// first set every input value to false when type is radio, with the radio-type you can only choose one option
if (questionType === 'radio') {
for (let i=0; i < questionInputs.length; i++) {
questionInputs[i].active = false;
}
}
questionInputs[index].active = inputValue.active;
setQuestionValue([...questionInputs]);
// set state that checks if question has been answered
questionValue.filter(x => x.active).length > 0 ? setIsAnswered(true) : setIsAnswered(false);
}
// creates the correct input type choices for the question
let inputs = [];
for (const [index, input] of props.options.entries()) {
inputs.push(
<Input
key={index}
index={index}
title={input}
active={questionValue[index].active}
selected={storeInputValue}
/>
);
}
// passes current state (selected value) and the index of question to parent (App.js) component
const saveQuestionValue = (e) => {
e.preventDefault();
props.selection(questionValue, questionIndex, questionTitle);
}
return (
<div className={`question ${!questionActive ? 'hide' : ''}`}>
<h1>{props.question}</h1>
<div className="inputs">
{inputs}
</div>
<a className={`selectionButton ${isAnswered ? 'highlight' : ''}`} href="" onClick={e => saveQuestionValue(e)}>
<div>Save and continue -></div>
</a>
</div>
);
}
export default Question;
With this setup when I click on an input it sends that to the Question component and that one returns a prop.active to Input so it highlights the input value. But when I click a new input it re-renders twice since it listens to the active state changing in Input, and sets all inputs to false.
My question is: how can I set up the logic in this code to act like a radio input so it only sets the currently selected input to active instead of first setting every input to active = false?
You should not be duplicating the value of state in two different components. There should always only be a single source of truth for what a value is in state. This is one of the most important patterns of React and is even mentioned in the official documentation.
Instead, you should lift shared state up so that it lives at the "closest common ancestor". Your <Input> components should not have any internal state at all - they should be pure functions that do nothing except render the current value and provide a callback to update said value. But, they do not store that value themselves - it is passed to them as a prop.
All of the logic for which input is active and what value it has should live in the parent component and be passed down from there. Whenever you are setting state in a child component and then somehow passing that state back up to a parent is a warning flag, because in React the data should flow down.

Categories