React Hooks: set component state without re-rendering twice - javascript

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.

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

How to update an index inside an array with hooks without hitting "Error: Too many re-renders."

I have an array of cells that I need to update based on some user input from a socket server. But whenever I try to update an index using useState(), react complains about "Error: Too many re-renders.". I tried adding a state to indicate that the grid has changed, but that still produces the same error. Please help me :<
const GameUI = ({ height, width }) => {
const [grid, setGrid] = React.useState([]);
const [didUpdate, setDidUpdate] = React.useState(0);
React.useEffect(() => {
for (let index = 0; index < 64; index++) {
const gridItem = <GridSquare key={`${index}`} color="" />;
setGrid((oldArray) => [...oldArray, gridItem]);
}
}, []);
//not re-rendering the modified component
const handleChange = () => {
let board = [...grid]
board[1] = <GridSquare key={`${0}${1}`} color="1" />;
setGrid(board)
// let count = didUpdate;
// count += 1
// setDidUpdate(count)
};
// handleChange();
return (
<div className="App">
<GridBoard grid={grid} />
<ScoreBoard />
<Players />
{handleChange()}
{/* <MessagePopup /> */}
</div>
);
};
Every time you change a state provided from the useState hook, it re-renders your component. You are calling handleChange on every render, which is calling your setGrid state hook. Therefore, you are rendering your component infinitely.
When do you actually need to call handleChange? Every animation frame? Every action event of some kind? Create an appropriate useEffect or useCallback hook (that includes used dependencies [array] as the second parameter), apply the appropriate event to your method and trigger it accordingly. If you go with a useEffect, don't forget to return a function that disables the event handling when the component is eventually unmounted or else you will have duplicate events triggering every time it gets remounted in the same application.

React state: unable to get updated value inside onChange

I am using an array of components that are interested depending on various conditions i.e the order and number of elements in the array are dynamic as shown below:
useEffect(() => {
const comp = [];
// if(condition1===true){
comp.push(<MyComp onChange={onValueChange} />);
// }
// if(condition2===true){
comp.push(<YourComp onChange={onValueChange} />);
// }
// if(condition3===true){
comp.push(<HisComp onChange={onValueChange} />);
// }
setComponents(comp);
}, []);
To each of the components in the array, there could be some sort of input control like input-text, input-number, text-area, chips, radio, checkbox, etc.
So there is an onChange event linked to each of these components.
I am using a common onValueChange function which is passed as a callback to these components. In the onValueChange I need 2 things:
changed value (from child component)
activeIndex (from same component)
const onValueChange = (val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
};
But here I am not able to fetch the updated value on activeIndex, it always gives zero no matter in what active step I am in.
Sandbox DEMO
useEffect(() => {
setComponents((previousValues)=>{
// if you want to add on previous state
const comp = [...previousValues];
// if you want to overwrite previous state
const comp = [];
if(condition1===true){
comp.push();
}
if(condition2===true){
comp.push();
}
if(condition3===true){
comp.push();
}
return comp;
});
}, []);
Try using useCallback with dependency array. Also try to avoid storing components in state - the office advice - what shouldn’t go in state?
const onValueChange = useCallback((val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
},[activeIndex];
For rendering try something like below.
condition1===true && <MyComp onChange={onValueChange} />
or create a function which returns the component eg: getComponent(condition) and use this in render/return. Make sure you wrap getComponent in useCallback with empty dependency array []

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.

React global variable not being updated inside function that passing to children component

I have a parent component that having some props passing from grandparent component and I am using one prop (object) and pass the value of that object to children component as props. I also pass a function to child component in order to get the updated value back from child component.
ParentComponent.js
const ParentComponent = props => {
const { record, saveRecord } = props;
const editedRecord = {...record}
const handleRecordValues = (name, value) => {
editedRecord[name] = value;
};
...
const content = <div>
<ChildComponent name={record.name} value={record.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}
ChildrenComponent.js
const ChildComponent = props => {
const { name, value, setValue } = props;
const [input, setInput] = useState(value);
const handleChange = (e, text) => {
setInput(text);
setValue(name, value);
}
return <TextField value={input} onChange={handleChange}/>
}
Above are the sample components I have. The issue is when I pass the editedRecord to saveRecord func to grandparent component the editedRecord is always the same as record as it is copied from record and value is not updated for that variable. I expect the editedRecord being updated by the handleRecordValues func.
For example, the record that I get is {}. And I create a new const editedRecord which is also {}.
After I input some value from ChildComponent the editedRecord should be updated to {name: value}. However when I click on Button in ParentComponent the editedRecord parameter is still {}.
Updated
Instead of using const I use
const [editedRecord, setEditedRecord] = useState(record);
const handleRecordValues = (name, value) => {
const newRecord = {
...editedRecord
};
newRecord[name] = value;
setEditedRecord(newRecord);
};
Now the editedRecord value got updated but another issue came up:
when I have multiple components as child components it only update the last one entry I have entered.
Your setValue/handleRecordValues function changes a variable ... but React has no way of knowing when that variable changes.
To let React know, you have to call saveRecord(editedRecord) after you make the change, or in other words you have to invoke a state-setting function, so that React knows about the change.
In general in React, if you don't change context/state/props (and for context/state, that means doing so using the appropriate React functions), React can't know to re-render your components in response. This means that any data that your components depend on to render needs to be changed via one of those three mechanisms, not just via ordinary Javascript, ie. a.b = c.
EDIT: To clarify a point in the comments. When you make a state variable:
const [myState, myStateSetter] = useState('');
there is nothing "magic" about myState; it's just another JS variable. Javascript doesn't give React any way to know when that variable changes, so if you just do:
myState = 4;
React has no idea that you did so. It only knows that it changed if you tell it that it changed ... ie. if you call:
myStateSetter(4);
Here's how I would alter the parent component to make everything work with react. The main issue you were having is that react needs to know that a change has occurred, so we need to set up the values as state/set state.
const ParentComponent = props => {
const { record, saveRecord } = props;
const [editedRecord,setEditedRecord] = useState(record);
useEffect(()=>{
//This sets the record straight whenever the parent passes a new record.
//You'd need to make sure the record is referentially stable when it isn't being updated, though
setEditedRecord(record);
},[record])
const handleRecordValues = (name, value) => {
setEditedRecord(record=>{...record,[name]:value});
};
...
const content = <div>
<ChildComponent name={editedRecord.name} value={editedRecord.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}

Categories