When dynamically adding element, state stays at default value inside this element - reactjs

I'm trying to dynamically add button to my component when selecting a value in a select. Adding the button to my page works fine, but the state of the component inside the callback function of the button stays at its default value.
I have the following implementation:
const Component = () => {
const [value, setValue] = useState('');
const [buttons, setButtons] = useState([]);
const onChangeValue = (e) => {
setValue(e.target.value);
setButtons([...buttons,
<button
onClick={() => console.log(value)} // displays '' which is the default state
>
Try me
</button>
]);
}
return (
<div>
<select onChange={onChangeValue}>
<option value={1}>Option #1</option>
<option value={2}>Option #2</option>
<option value={3}>Option #3</option>
</select>
{buttons}
</div>
);
}
I would guess that the state of my component is not "connected" to the buttons, but I'm not sure how I can do it.

I think the onClick event is trying to console log the wrong variable. You should put event.target.value instead of just value.

You can create custom components and pass the dynamic value from useState as a prop:
const Component = () => {
const [value, setValue] = useState('');
const [buttons, setButtons] = useState([]);
const onChangeValue = (e) => {
setValue(e.target.value);
setButtons([...buttons,
(props) => <button
onClick={() => console.log(props.value)}
>
Try me
</button>
]);
}
return (
<div>
<select onChange={onChangeValue}>
<option value={1}>Option #1</option>
<option value={2}>Option #2</option>
<option value={3}>Option #3</option>
</select>
{buttons.map((B, i) => <B key={i} value={value}/>)}
</div>
);
}

The issue in your code is because of useState since useState is asynchronous. So at the time component is creating so the value is "empty" as its not updated because of asynchronous. The better way is to kind of closure which remember its state. You can simply do this by saving variable state:
Here is the code:
const Component = () => {
const [value, setValue] = useState("");
const [buttons, setButtons] = useState([]);
const handleClick = (val) => console.log(val);
const onChangeValue = (e) => {
e.persist();
const val = e.target.value;
setValue(e.target.value);
if (buttons.length < 3) {
//avoid unnecessary addition
setButtons([
...buttons,
<button
key={value}
onClick={() => handleClick(val)} // displays '' which is the default state
>
Try me
</button>
]);
}
}
Here is the demo: https://codesandbox.io/s/sharp-oskar-qnm51?file=/src/App.js:65-633

Related

React select box function

I am trying to make a custom select box component with parent and shild components, with autocomplete and also fetching from api. The problem is that i am trying to fire onchange function from parent to child to select an item from the select box but it is not working, can someone tell me where is the problem?
export function SelectComponent() {
const [results, setResults] = useState([]);
const [selectedValue, setSelectedValue] = useState<ComboBoxOption>();
const handleOnChange = (e: any) => {
if (!e.target.value.trim()) return setResults([]);
const filteredValue = results.filter((item: any) =>
item.value.toString().toLowerCase().startsWith(item.toLowerCase())
);
setResults(filteredValue);
};
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(...);
setResults(response.data);
};
fetchData();
}, []);
return (
<div>
<SelectField
options={results}
value={selectedValue?.value}
onChange={handleOnChange}
onSelect={item => setSelectedValue(item)}
/>
</div>
);
}
export function SelectField({
...
}: SelectFieldProps) {
const [isOpen, setIsOpen] = useState(false);
const [isActive, setIsActive] = useState(false);
const [defaultValue, setDefaultValue] = useState("");
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = event => {
setIsOpen(true);
setDefaultValue(event.target.value);
onChange && onChange(event);
};
return (
<div>
<input
placeholder={placeholder}
value={defaultValue}
onChange={handleOnChange}
/>
<button onClick={() => {setIsOpen(!isOpen);}}></button>
<ul>
{options.map((option: any, index: any) => {
return (
<li
key={index}
onClick={() => {setIsOpen(false);}
>
<span>{option.value}</span>
</li>
);
})}
</ul>
)
</div>
);
}
It looks like the problem may be in the handleOnChange function in the SelectComponent. The function is trying to filter the results state based on the value of the input element, but it should be filtering based on the e.target.value instead. Also, it's using item.toLowerCase() which doesn't make sense, instead it should use e.target.value.toLowerCase():
const handleOnChange = (e: any) => {
if (!e.target.value.trim()) return setResults([]);
const filteredValue = results.filter((item: any) =>
item.value.toString().toLowerCase().startsWith(e.target.value.toLowerCase())
);
setResults(filteredValue);
};
Also, in the SelectField component, it seems that you are not calling the onSelect prop when an option is selected. You should call the onSelect prop and pass the selected option as a parameter when an option is clicked, like so:
<li
key={index}
onClick={() => {
setIsOpen(false);
onSelect(option);
}}
>
<span>{option.value}</span>
</li>
I would also recommend using onBlur instead of onClick for the input field, this way it can be closed when the user clicks outside of the component.

How to add items to array in react

Code:
export default function App() {
const [name,setName] = useState("");
var myArray = [];
const handleAdd = () => {
myArray = [...myArray,name]
setName("")
}
return (
<div className="App">
<input placeholder="type a name" onChange={(e) => setName(e.target.value)}/>
<button onClick={handleAdd}>add</button>
<button onClick={() => console.log(myArray)}>test</button>
{myArray.map((n) => {
return <h2>{n}</h2>
})}
</div>
);
}
OnClick it isn't adding the name to the array.
this is how you "push" to an array with useState
const [array, setArray] = useState([])
setArray(previous => [...previuous, newItem])
You should use a state for your array and set that state to see the changes reflected:
export default function App() {
const [name, setName] = useState('');
const [myArray, setMyArray] = useState([]);
const handleAdd = () => {
setMyArray([...myArray, name]);
setName('');
};
return (
<div className="App">
<input
placeholder="type a name"
onChange={(e) => setName(e.target.value)}
/>
<button onClick={handleAdd}>add</button>
<button onClick={() => console.log(myArray)}>test</button>
{myArray.map((n) => {
return <h2>{n}</h2>;
})}
</div>
);
}
We can also set the state of myArr to be an empty array initially, making it easier to manipulate the subsequent state of that array. The onClick event handler does not fire the handleAdd function, for some reason, it only resets the form and does not provide any state. To submit the form and materialize the state, we can also use the onSubmit event handler instead of onClick. In the same way, we can use the name state as a value/prop for the name input, which will be used by the onChange handler.
import React, { useState } from 'react'
const App = () => {
const [name, setName] = useState('')
const [myArr, setMyArr] = useState([])
const submit = (event) => {
event.preventDefault()
setMyArr(myArr.concat(name))
setName('')
}
//console.log(myArr)
return (
<div className="App">
<form onSubmit={submit}>
<div>
<label htmlFor="name">Name</label>
<input
placeholder="type a name"
type="text"
value={name}
onChange={({ target }) => setName(target.value)}
/>
</div>
<div>
<button type="submit">Add</button>
</div>
</form>
<div>
{myArr.map((arr, index) => (
<div key={index}>
<p>{arr}</p>
</div>
))}
</div>
</div>
)
}
export default App
I have a proclivity of inserting items on an array using concat.
import React, { useState } from 'react'
// ...
const App = () => {
// ...
const [myArr, setMyArr] = useState([])
// somewhere on your event handler e.g. Submit handler
setMyArr(myArr.concat(name))
// ...
}

todos array at index 1 always get changed and the array doesn't move further - React-Typescript

I am Creating a Todo-List using typescript-react and my code doesn't seem to work.
App.tsx:
import { useState } from "react";
import "./App.css";
export default function App() {
let id = 0;
const [todoInput, setTodoInput] = useState("");
let todos: string[] = ["Arbit"];
const handleSubmit = (e: any) => {
e.preventDefault();
todos.push(todoInput);
console.log(todos)
setTodoInput("");
id += 1;
// console.log(todos);
// console.log(todos[id])
// console.log(e.target[0].value);
// todos.push(e.target[0].value);
// console.log(e)
};
return (
<div className='App'>
<form onSubmit={handleSubmit}>
<input
type='text'
value={todoInput}
onChange={(e) => setTodoInput(e.target.value)}
/>
<button>Submit</button>
</form>
<ul>
{todos.map((todo) => (
<li key={id}>{todo}</li>
))}
</ul>
</div>
);
}
The todos array doesn't push the todoInput. Instead, it omits todos[1].
I am creating my app in a single file.
In react functions you have to use hooks in order to retain the state during re-renders. In this case, you need to turn your todos array into a const [todos, setTodos] = React.useState() and update it using the setTodos function in a functional way.
const [todos, setTodos] = React.useState([]);
// also use useState for ids
const [id, setId] = React.useState(0);
const handleSubmit = (e: any) => {
e.preventDefault();
setTodos([...todos, todoInput]);
console.log(todos)
setTodoInput("");
setId(id + 1);
};
I am not sure, but I think you need to make the array a state too:
import { useState } from "react";
import "./App.css";
export default function App() {
let id = 0;
const [todoInput, setTodoInput] = useState("");
const [todos, setTodos] = useState(["Arbit"]);
const handleSubmit = (e: any) => {
e.preventDefault();
todos.push(todoInput);
console.log(todos)
setTodoInput("");
setTodos(todos);
id += 1;
// console.log(todos);
// console.log(todos[id])
// console.log(e.target[0].value);
// todos.push(e.target[0].value);
// console.log(e)
};
return (
<div className='App'>
<form onSubmit={handleSubmit}>
<input
type='text'
value={todoInput}
onChange={(e) => setTodoInput(e.target.value)}
/>
<button>Submit</button>
</form>
<ul>
{todos.map((todo) => (
<li key={id}>{todo}</li>
))}
</ul>
</div>
);
}
I'm sorry if it doesn't work.

Reusable container component with state and dynamic child component

I am trying to have a reusable component that can instantiate a dynamic component:
function EditableElement({endpoint, data, ElementFormClass}) {
const [edit, setEdit] = useState(false);
const [formData, setFormData] = useState(data);
const [deleted, setDeleted] = useState(false);
if (deleted) return (<p>X_X</p>);
if (edit) return ElementFormClass(
endpoint,
data,
setFormData,
pk => {
setDeleted(true);
},
e => setEdit(false)
)
return (
<p onClick={e => setEdit(true)}>{formData.name}</p>
)
}
function ElementFormDisplayOne({endpoint, data, cancel, updateData, deleteData}) {
const [formData, setFormData] = useState(data);
// const [deleted, setDeleted] = useState(false);
// if (deleted) return (<p style={{color: 'red'}}>DELETED</p>);
return (
<form onSubmit={e => {
e.preventDefault();
e.stopPropagation();
updateData(formData);
cancel();
}}>
<label htmlFor={formData.pk}>{formData.name}</label>
<input type="text" id={formData.pk} value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}/>
<button type="button" onClick={cancel}>cancel</button>
<button type="submit">ok</button>
<button type="button" onClick={e => deleteData(formData.pk)}>delete</button>
</form>
)
}
It's used like this:
<div>
<h3>Gallery Form ONE</h3>
{data.map(el => <EditableElement endpoint={'update'} key={el.pk} data={el}
ElementFormClass={ElementFormDisplayOne}/>)}
</div>
<div>
<h3>Gallery Form TWO</h3>
{data2.map(el => <EditableElement endpoint={'update'} key={el.pk} data={el}
ElementFormClass={ElementFormDisplayTwo}/>)}
</div>
But it throws
Error: Rendered more hooks than during the previous render.
Is what I want possible in ReactJs?
Note: I'm not using Redux and don't intend to
Codesandbox example
Fixed your error in line 104 of your codesandbox by converting your function into an actual JSX element.
if (edit)
return (<ElementFormClass
endpoint={endpoint}
data={data}
setFormData={setFormData}
cb1 = {(pk) => setDeleted(true)}
cb2 = {(e) => setEdit(false)}
/>);
This is a common error people face in react, where they use a react component as a function rather than JSX.

How to update state based on checkbox status of being checked

I have a bunch of checkboxes with the following markup
<input type='checkbox' data-id='123' data-label='abc' ref={checkboxRef} />
<input type='checkbox' data-id='456' data-label='xyz' ref={checkboxRef} />
And a state which is initially set as an empty array
const [contacts, setContacts] = useState([])
What I want to do is update the state with an object of a checkbox's data based on whether it's checked or not. If checked, it's data is to be added to the state and if unchecked, remove it.
Expected state after a checkbox is checked
[
{ id: '123', label: 'abc' }
]
I've used a ref for now to the input and getting the data of it but can't figure out how to go about updating the state.
const handleToggle = () => {
setIsChecked(prevState => !isChecked)
const id = checkboxRef.current.getAttribute('data-id')
const label = checkboxRef.current.getAttribute('data-label')
}
I have solved it. Check it here.
https://codesandbox.io/s/affectionate-fermi-f6bct
Full code hereby is
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [contacts, setContacts] = useState([]);
const ref1 = React.createRef();
const ref2 = React.createRef();
const handleClick = (ref) => {
const id = ref.current.getAttribute("data-id");
const label = ref.current.getAttribute("data-label");
if (contacts.map((e) => e.id).includes(id)) {
setContacts(contacts.filter((e) => e.id !== id));
} else {
setContacts([...contacts, { id, label }]);
}
console.log(contacts);
};
return (
<div className="App">
<input
type="checkbox"
data-id="123"
data-label="abc"
ref={ref1}
onClick={() => {
console.log("hi");
handleClick(ref1);
}}
/>
<input
type="checkbox"
data-id="456"
data-label="xyz"
ref={ref2}
onClick={() => handleClick(ref2)}
/>
</div>
);
}

Resources