In a small React app, I'm trying to add delete functionality via a button for a list. Presently, I'm attempting this through the deleteItem function, which makes use of array.splice prototype method.
However, I'm encountering the error, Too many re-renders. React limits the number of renders to prevent an infinite loop.. What is the cause of this error? Shouldn't this function only be invoked once, when the button is clicked?
And how can I resolve this error?
import "./styles.css";
import React, { useState, Fragment } from "react";
export default function App() {
const [items, setItems] = useState(["first item"]);
const [newItem, setNewItem] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
setItems([newItem, ...items]);
};
const handleChange = (event) => {
setNewItem(event.target.value);
};
const deleteItem = (i) => {
setItems(items.splice(i,1))
}
return (
<div>
<form>
<input type="text" value={newItem} onChange={handleChange} />
<input type="button" value="submit" onClick={handleSubmit} />
</form>
<ul>
{items.map((i) => {
return (
<Fragment>
<li>{i}</li>
<button
onClick= {() => deleteItem(i)}> // Amr recommendation
delete
</button>
</Fragment>
);
})}
</ul>
</div>
);
}
Edit: I've taken user, Amr's, recommendation and added a anonymous arrow function to the button. However, a new issue has arisen. I can delete any item up until there exists only one item in the array. The final item cannot be deleted. Why is this?
you are passing function reference on the onClick handler, change it to an arrow function that triggers the delete method onClick= {()=>deleteItem(i)}>
second thing is that you should add keys to your the parent component when you Map over components to prevent unnecessary behavior.
and the last thing is that in your delete method, you are using Array.prototype.splice(), which returns the item that will be removed, from the items, your requested/ required behavior can be achieved through the Array.prototype.filter() method
const deleteItem = (i) => {
setItems(items.filter((item) => item !== i));
};
This is the final result, it should work fine.
import React, { useState, Fragment } from "react";
export default function App() {
const [items, setItems] = useState(["first item"]);
const [newItem, setNewItem] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
setItems([...items, newItem]);
};
const handleChange = (event) => {
setNewItem(event.target.value);
};
const deleteItem = (i) => {
setItems(items.filter((item) => item !== i));
};
console.log(items);
return (
<div>
<form>
<input type="text" value={newItem} onChange={handleChange} />
<input type="button" value="submit" onClick={handleSubmit} />
</form>
<ul>
{items.map((i, idx) => {
return (
<div key={idx}>
<li>{i}</li>
<button onClick={() => deleteItem(i)}>delete</button>
</div>
);
})}
</ul>
</div>
);
}
you can use following code for deleting from an array. it copies 'items' array and delete one item and after that setstate new array.
it prevent re-render whole component,do operations on copy of state and setstate final result.
const deleteItem = (i) => {
let newItems=[...items]
newItems.splice(i,1)
setItems(newItems)
};
Related
I've just started learning about react js and this is my first react js app. I'm using api to fetch the data. so far it works, but now I want to add a search keyword to the function that is acquired from a search bar component.
here's my code:
SearchBar.js
const SearchBar = ({ getUsers }) => {
return (
<div className="is-flex flex-align-items-center mb-3">
<input type="text" id="query" className="input search-input" placeholder="search keyword"/>
<Button className="search-btn ps-3 pe-3"
onClick={() => getUsers(document.querySelector('#query').value)}>
<FontAwesomeIcon icon={faMagnifyingGlass} />
</Button>
</div>
);
};
MasterUser.js
import { useState, useEffect } from "react";
import SearchBar from "./SearchBar";
const MasterUser = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
getUsers();
}, []);
const getUsers = async (query='') => {
console.log('get users', query);
try {
let myurl = 'http://localhost:8080/users';
const response = await fetch(myurl);
const data = await response.json();
setUsers(data);
setIsLoading(false);
} catch (e) {
console.log(e.getMessage());
}
};
return (
<div>
<SearchBar onClick={getUsers}/>
</div>
);
};
when the app loads, the console log says get users <empty string> and it returns all the users as expected, but when I clicked on the search button (magnifyingGlass) it gives an error Uncaught TypeError: getUsers is not a function.
any help is appreciated..
<SearchBar onClick={getUsers}/>
You have named the prop onClick not getUsers. That's why you get that error.
Yeah, accessing dom element value using selectors (e.g. document.querySelector('#query').value) is also not typical react. Read about controlled form elements (save form element value in state).
Make your searchBar component more reactive like so
const SearchBar = ({ getUsers }) => {
const [searchValue,setSearchValue]=useState('');
return (
<div className="is-flex flex-align-items-center mb-3">
<input type="text" id="query" className="input search-input" placeholder="search keyword" value={searchValue} onChange={(e)=>setSearchValue(e.target.value)}/>
<Button className="search-btn ps-3 pe-3"
onClick={() => getUsers(searchValue)}>
<FontAwesomeIcon icon={faMagnifyingGlass} />
</Button>
</div>
);
};
I'm super new to React and building my first ever app which is a url shortening app. Each shortened url has a button next to it whose text is set to 'copy' initially and once the user click on it the link is copied to the clipboard and the button text changes to 'copied'. Everything is working fine except if I have multiple shortened url's and I click on one of the buttons next to any particular url, it still only copies that url to clipboard but the button text changes to copied on all of them.
If anyone can please enlighten me how to single out those buttons individually that'll be of great help. I've tried using the id but maybe I'm not doing that correctly?
P.S - this is first time I'm posting on here so apologies upfront if I missed any crucial bits.
import {useState} from 'react'
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid';
function Main() {
const [name, setName] = useState('')
const [list, setList] = useState(initialList);
const handleSubmit = (e) => {
e.preventDefault();
}
const handleAdd = async () => {
const res = await axios.get(`https://api.shrtco.de/v2/shorten?url=${name}`)
const {data: {result: {full_short_link: shortLink}}} = res
const newList = list.concat({name:shortLink, id: uuidv4()});
setList(newList);
setName('');
}
const [buttonText, setButtonText] = useState("Copy");
return (
<form onSubmit={handleSubmit}>
<input type="text"
value= {name}
onChange={(e) => setName(e.target.value)}
placeholder='Shorten a link here'
onClick = {()=> setButtonText('copy')}
/>
<button onClick = {handleAdd}>Shorten it!</button>
</form>
<ul>
{list.map((item, index) => (
<li key={item.id}>{item.name}<button
onClick = {() => { navigator.clipboard.writeText(item.name); setButtonText("Copied")}} >
{buttonText}
</button></li>))}
</ul>
export default Main``
It’s because you are using one state variable for all of your buttons, you need a variable to keep track of state for each individual button. You should refactor the code within your map function into its own component, and declare the buttonText state within that component. That way each button has its’ own state.
Eg (sorry for the capitalisations in my code):
MyButton.js
Const MyButton = ({item}) => {
const [buttonText, setButtonText] = useState(‘Copy’)
Return (
<li key={item.id}>{item.name}
<button
onClick = {() => {
navigator.clipboard.writeText(item.name);
setButtonText("Copied")}
}
>
{buttonText}
</button>
</li>
)
Export default MyButton
Form:
// ……
<ul>
{list.map((item, index) => <MyButton key={item.id} item={item} />)}
</ul>
I'd like to react rerender component after every state edit.
App component:
let [cur1, setCur1] = useState('USD')
let [cur2, setCur2] = useState('EUR')
let [result, setResult] = useState(0)
let currenciesArr = [cur1, cur2]
async function getRate(e) {
e.preventDefault()
setCur1(cur1 = e.target.cur1.value)
setCur2(cur2 = e.target.cur2.value)
let amount = e.target.amount.value
const api_url = await fetch(`https://free.currconv.com/api/v7/convert?q=${cur1}_${cur2}&compact=ultra&apiKey=${API_KEY}`)
const data = await api_url.json()
await setResult(convert(amount, data))
}
I have used Context.Provider for rerender, but it doesn't work.
return (
<Context.Provider value={{currenciesArr}}>
<div>
<Choose getRate={getRate} chooseCur={chooseCur} chooseCur2={chooseCur2}/>
<ShowRate currencies={currenciesArr} result={result}/>
</div>
</Context.Provider>
)
Component that need to rerender
function Choose(props) {
const cProps = useContext(Context)
console.log(cProps.currenciesArr);
return(
<div>
<div>
<button onClick={ props.chooseCur } name='RUB'>RUB</button>
<button onClick={ props.chooseCur } name='AUD'>AUD</button>
</div>
<div>
<button onClick={ props.chooseCur2 } name='EUR'>EUR</button>
<button onClick={ props.chooseCur2 } name='GBP'>GBP</button>
</div>
<form onSubmit={props.getRate}>
{cProps.currenciesArr.map((item,i) => {
return(
<input type='text' key={i} name={'cur'+(i+1)} defaultValue={item}></input>
)
})
}
<input type='text' name='amount' defaultValue='1'></input>
<button onClick={(e)=>{console.log(e.target)}} ></button>
</form>
</div>
)
}
Button with prop props.chooseCur setting state in App component
function chooseCur(e) {
e.preventDefault()
setCur1(e.target.name)
}
function chooseCur2(e) {
e.preventDefault()
setCur2(e.target.name)
}
and i'd like to "choose" component will rerender after setState.
First currenciesArr should be part of the state as const [currenciesArr, setCurrenciesArr] = useState([cur1, cur2])
Next, you need to call setCurrenciesArr in your chooseCur2 functions. I used a restructuring assignment to get the value of name inside the function. Hooks are called when the event loop is complete. See Capbase Medium post for more information on hooks and the event loop.
In choose.js
You need to use value in your input instead of defaultValue and set it as readonly to prevent receiving a warning about setting the value.
Default value provides the value if none is present.
See the following codesandbox for a working version.
https://codesandbox.io/s/long-rain-8vyuh
I tried to put a form in a separate reusable component but when used that way I can't type anything into the input. I observed, that after entering one letter (it does not appear in the input box) it seems that React rerender the whole component and the name is updated with the inserted letter.
in the version 2 the same code works correctly.
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
// VERSION 1. doesn't work
const FormEdit = () => (
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
return (
<Layout>
<div>
{name} //<-it shows only one letter
<FormEdit />
</div>
</Layout>
);
// VERSION 2 -> works properly
return (
<Layout>
<div>
{name} //<-the updated name is shown immediately
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
</div>
</Layout>
);
};
export default User;
The issue is directly related to declaring the FormEdit component within the other component. Here's why:
In a functional component, everything declared inside gets destroyed and re-created each render. It's no different than a normal function call. This is what makes React's hooks so special. They keep track of values in between renders and make sure they are re-created with the correct values.
You're declaring the FormEdit component inside a function, which means not only is it re-declared every render, but as a side-effect it also un-mounts and remounts each render as well.
This has a few different effects:
The component's input loses focus every render.
It's impossible for it to maintain its own state.
It's not very performant.
Below is a working example to demonstrate.
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As for why you only see the first character; You are not giving the input a value, only an onChange. If the component does not unmount, this just makes it an "uncontrolled" component. The input still gets it's value updated, you just can't programatically control it. But, since it is unmounting and re-mounting every render, it loses its last value every time the user types.
Making it a controlled input would fix this:
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={name} onChange={handleChange("name")} type="text"/>
// ^ Add this
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is a little better, but still not ideal. Now it keeps the value each update, but it still loses focus. Not a very good user experience.
This final solution is to never declare a component within another component.
const {useState, useEffect} = React;
const FormEdit = (props) => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={props.name} onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
}
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
return (
<div>
{name}
<FormEdit name={name} handleChange={handleChange} submitEdit={submitEdit} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Now it only mounts once, keeps focus, and updates as expected.
You would have to pass your form handlers to the child component as props so that the lifted state can be manipulated from the child.
// Parent Component
...
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
...
};
const submitEdit = event => {
...
};
return (
<Layout>
<div>
{name}
<FormEdit handleChange={handleChange} submitEdit={submitEdit}/>
</div>
</Layout>
);
and then in the child:
// Child Component
const FormEdit = (props) => (
<form>
<div className="form-group">
<input onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
Your FormEdit component which is inside the App component is causing the entire App component to re-render when the state gets updated onChange and hence you can only enter only one character at a time. It is generally not a great idea to declare a component within a component. Refer this link for more info. All you have to do is pull the FormEdit component out of the App component in its own separate function and pass the change handlers as props to the FormEdit component. Have a look at the working code below.
import React, { useState } from 'react';
const FormEdit = ({ handleChange, submitEdit, name }) => {
return (
<form>
<div className='form-group'>
<input onChange={handleChange('name')} type='text' value={name || ''} />
</div>
<button onClick={submitEdit} type='submit'>
Submit
</button>
</form>
);
};
export default function App() {
const [userdata, setUser] = useState();
const { name } = userdata || {};
const handleChange = key => event => {
setUser(prevState => {
return { ...prevState, [key]: event.target.value };
});
event.persist();
event.preventDefault();
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
return (
<div>
<div>
{name || ''}
<FormEdit
handleChange={handleChange}
submitEdit={submitEdit}
name={name}
/>
</div>
</div>
);
}
I want to grab the value of input inside the array when the button is clicked. How do i pass the input value to the function of button.
Any help would be appreciated. Thanks
import React, { useState, useEffect } from 'react'
export default function Todo(props) {
const [todo,settodo] = useState([]);
function getdata(){
//fetch data
settodo(data);
}
function SaveInput(id){
}
useEffect(() => {
getdata();
},[]);
return (
<React.Fragment>
<div>
{todo.map(function(item, key){
return <div>
<div>{item.name}</div>
<div>
<input type="text" name="inputval" onChange={() => handleChange(e)}>
<button onClick={()=> SaveInput(item.id)}></button>
</div>
</div>
})}
</div>
</React.Fragment>
)
}
You need to send item.id to your handleChange function,
<input type="text" name="inputval" onChange={(e) => handleChange(e,item.id)} />
You handleChange function should,
const handleChange = (e,id) => {
let val = e.target.value;
setInputVal(prevState =>({
...prevState,
[id]:val
}))
}
You must define a state to store input values,
const [inputVal,setInputVal] = useState({});
On the click of button you can access input state,
function SaveInput(id){
console.log(inputVal[id]);
}
Demo
You can save the inputs in a separate useState when the input is being changed, which can be later retrieved easily during the button click event.
Code below is an example and is not tested, but should give you some idea how to proceed.
import React, { useState, useEffect } from 'react'
export default function Todo(props) {
const [todo,settodo] = useState([]);
const [inputVal, setInputVal] = useState({});
function getdata(){
//fetch data
settodo(data);
}
function SaveInput(id){
let inputVal = inputVal[id];
// do other stuff.
}
useEffect(() => {
getdata();
},[]);
return (
<React.Fragment>
<div>
{todo.map(function(item, key){
return <div>
<div>{item.name}</div>
<div>
<input type="text" name="inputval" onChange={(e) => setInputVal({...inputVal, [item.id]: e.target.value })}>
<button onClick={()=> SaveInput(item.id)}></button>
</div>
</div>
})}
</div>
</React.Fragment>
)
}
One common pattern is to use the handleChange(event) function on input to set a state with the current value.
const [input,setInupt] = useState("");
function handleChange(event) {
setInput(event.target.value)
}
and when the button is clicked, you can use the value of the input state to pass on
<button onClick={()=> console.log(input))}>
First of all, If you are having an onChange method then you must have a value for that input as well or else it will display a warning for "uncontrolled input" and that input box is of no use to you unless you provide a value to it.
Secondly, you should use a state for the values of those input boxes and then you can access the values of input in the save button click function. Here is the example of how you can do it.
import React from 'react'
export default class Todo extends React.Component {
constructor(props) {
super(props);
this.state = {
inputIDs: {}
}
}
SaveInput = id => {
console.log("input value:", this.state[id]);
};
handleChange = (e, id) => {
this.setState({[id]: e.target.value});
};
render() {
const {inputIDs} = this.state;
const todo = [
{id: 1, val: "abc", name: "lorem"},
{id: 2, val: "xyz", name: "Ipsum"}
];
let todos = todo.map((item, key) => {
return <div key={key}>
<div>{item.name}</div>
<div>
<input type="text" value={this.state[item.id]} onChange={(e) => this.handleChange(e, item.id)}/>
<button onClick={() => this.SaveInput(item.id)}>Click Me!</button>
</div>
</div>
});
return (
<React.Fragment>
{todos}
</React.Fragment>
)
}
}