How to implement 2-way bindings in React? - javascript

I understand that we can pass props from a parent component to the child component. In my case, I am passing from App.js ( parent component) to child component - UserInput.js. However, I also want to pass the props back from UserInput.js to App.js so that I can reset the label values.
However, I am unable to do so. When I do props.reset(), it says reset is not a function. I get exception - typeError- "prop.reset is not a function". My ultimate goal is to reset the userName and age field to blank.
Here is my App.js and UserInput.js
import "./App.css";
import UserInput from "./Components/UserInput/UserInput";
import Validation from "./Components/Validation/Validation";
import { useState } from "react";
import Output from "./Components/Output/Output";
const App = (props) =>
{
const errorMessage = [
"",
"Please enter a valid name and age (non-empty values).",
"Please enter a valid age (>0)."
];
let usrList = [
];
const [errNumber, setErrNumber] = useState(0);
const [userList, setUsrList] = useState(usrList);
const validationOkHandler = () => {
//setting error back to 0.
setErrNumber(0);
}
const userInputHandler = (userData) => {
//console.log(userData);
if (userData.user.trim() === "" || userData.age === 0) {
console.log(
"Invalid Input - Please enter a valid name and age (non-empty values)."
);
setErrNumber(1);
}
else if (userData.age < 0) {
console.log("Invalid input Please enter a valid age (>0).");
setErrNumber(2);
}
else {
props.reset();
setErrNumber(0);
userData.id = Math.random();
setUsrList( (previousUSer) => {
return [userData, ...previousUSer];
});
}
};
return (
<div>
<UserInput
onUserInput={userInputHandler}
valdiationOk={validationOkHandler}
/>
{errNumber > 0 && (
<Validation valdiationOk={validationOkHandler}>
{errorMessage[errNumber]}
</Validation>
)}
{errNumber === 0 && <Output userList={userList} />}
</div>
);
}
export default App;
Here is my UserInput.js
import { useState } from "react";
import styles from "./UserInput.module.css";
import App from "../../App";
const UserInput = (props) => {
const [usrName, setUsrName] = useState("");
const [age, setAge] = useState("");
const userNameHandler = (event) => {
console.log(event.target.value);
setUsrName(event.target.value);
};
const ageHandler = (event) => {
console.log(event.target.value);
setAge(event.target.value);
};
const resetForm = () => {
setUsrName('');
};
const addUserHandler = (event) => {
const userInfo = {
user: usrName,
age: +age,
};
console.log(userInfo);
event.stopPropagation();
props.onUserInput(userInfo);
<App reset={resetForm} />
};
const ModalHandler = () => {
console.log("Modal handler clicked");
props.valdiationOk();
};
return (
<div className={`${styles["container"]}`} onClick={ModalHandler}>
<div className={`${styles["inputWrapper"]}`}>
<label htmlFor="username">Username</label>
<input
type="text"
name="username"
id="username"
onChange={userNameHandler}
></input>
</div>
<div className={`${styles["inputWrapper"]}`}>
<label htmlFor="age">Age</label>
<input type="text" name="age" id="age" onChange={ageHandler}></input>
</div>
<div>
<button type="submit" onClick={addUserHandler}>
Add User
</button>
</div>
</div>
);
};
export default UserInput;

There's no need for props to invoke a function from another functional component in this case. You can plainly pass the resetForm as a callback argument to the onUserInput prop function along with userInfo, this argument function can then be directly invoked based on your checks and conditions. If you implement this you might still not see the form fields reset to empty strings "" when the form is valid, because there's no two-way binding established between the state variable and the input fields value. To get things work as expected, you should establish this binding by setting the fields value to the hook's state variable, so that the value in the field is the same as the value of the state variable.
Your updated code should be something like this:
App.js
import "./App.css";
import UserInput from "./Components/UserInput/UserInput";
import Validation from "./Components/Validation/Validation";
import { useState } from "react";
import Output from "./Components/Output/Output";
const App = (props) =>
{
const errorMessage = [
"",
"Please enter a valid name and age (non-empty values).",
"Please enter a valid age (>0)."
];
let usrList = [
];
const [errNumber, setErrNumber] = useState(0);
const [userList, setUsrList] = useState(usrList);
const validationOkHandler = () => {
//setting error back to 0.
setErrNumber(0);
}
const userInputHandler = (userData,reset) => { //reset is the callback function
//console.log(userData);
if (userData.user.trim() === "" || userData.age === 0) {
console.log(
"Invalid Input - Please enter a valid name and age (non-empty values)."
);
setErrNumber(1);
}
else if (userData.age < 0) {
console.log("Invalid input Please enter a valid age (>0).");
setErrNumber(2);
}
else {
reset(); //Call reset function from the function argument as a callback function
setErrNumber(0);
userData.id = Math.random();
setUsrList( (previousUSer) => {
return [userData, ...previousUSer];
});
}
};
return (
<div>
<UserInput
onUserInput={userInputHandler}
valdiationOk={validationOkHandler}
/>
{errNumber > 0 && (
<Validation valdiationOk={validationOkHandler}>
{errorMessage[errNumber]}
</Validation>
)}
{errNumber === 0 && <Output userList={userList} />}
</div>
);
}
export default App;
UserInput.js
import { useState } from "react";
import styles from "./UserInput.module.css";
const UserInput = (props) => {
const [usrName, setUsrName] = useState("");
const [age, setAge] = useState("");
const userNameHandler = (event) => {
console.log(event.target.value);
setUsrName(event.target.value);
};
const ageHandler = (event) => {
console.log(event.target.value);
setAge(event.target.value);
};
const resetForm = () => {
setUsrName('');
};
const addUserHandler = (event) => {
const userInfo = {
user: usrName,
age: +age,
};
console.log(userInfo);
event.stopPropagation();
props.onUserInput(userInfo,resetForm); //Pass resetfrom function as the callback function.
};
const ModalHandler = () => {
console.log("Modal handler clicked");
props.valdiationOk();
};
return (
<div className={`${styles["container"]}`} onClick={ModalHandler}>
<div className={`${styles["inputWrapper"]}`}>
<label htmlFor="username">Username</label>
<input
type="text"
name="username"
id="username"
value = {usrName} //Two way binding using the value attribute.
onChange={userNameHandler}
></input>
</div>
<div className={`${styles["inputWrapper"]}`}>
<label htmlFor="age">Age</label>
<input type="text" value={age} name="age" id="age" onChange={ageHandler}></input>
</div>
<div>
<button type="submit" onClick={addUserHandler}>
Add User
</button>
</div>
</div>
);
};
export default UserInput;

Related

How do I re-render a component when an object is changed in React

I'm trying to build a simple expense log app in React and would like to display the latest data every time the expensesLog object is updated. On clicking the button, the clickHandler function is called and the object is updated but nothing seems to change on the UI inspite of me mapping the expenseLog object. Here's my code :
import './App.css';
import { useState } from 'react';
function App() {
var expenseLog = [
{
name: "Expense1",
amount: 1000
},
{
name: "Expense2",
amount: 100
}
];
var [count, setCount] = useState(0);
var [name, setName] = useState("");
const inputChangeHandler = (e) => {
setName(e.target.value);
}
const inputChangeHandler2 = (e) => {
setCount(e.target.value);
}
const clickHandler = () => {
expenseLog = [...expenseLog, {name: name, amount: count}];
document.querySelector('.inputField').value = "";
document.querySelector('.inputField2').value = 0;
}
return (
<div className="App">
<input onChange={inputChangeHandler} className='inputField'/>
<input type={"number"} onChange={inputChangeHandler2} className='inputField2'/>
<button onClick={clickHandler}></button>
{expenseLog.map(expense => <p>{expense.name} {expense.amount}</p>)}
</div>
);
}
export default App;
It's not updating UI as expenseLog is just object, and it's immutable when you don't change the reference.
So, you need to use expenseLog as State variable. And update it inside callback function using setExpenseLog for updating the state.
import "./App.css";
import { useState } from "react";
function App() {
const [expenseLog, setExpenseLog] = useState([
{
name: "Expense1",
amount: 1000,
},
{
name: "Expense2",
amount: 100,
},
];)
var [count, setCount] = useState(0);
var [name, setName] = useState("");
const inputChangeHandler = (e) => {
setName(e.target.value);
};
const inputChangeHandler2 = (e) => {
setCount(e.target.value);
};
const clickHandler = () => {
setExpenseLog(prev => [...prev,{ name: name, amount: count } ])
document.querySelector(".inputField").value = "";
document.querySelector(".inputField2").value = 0;
};
return (
<div className="App">
<input onChange={inputChangeHandler} className="inputField" />
<input
type={"number"}
onChange={inputChangeHandler2}
className="inputField2"
/>
<button onClick={clickHandler}></button>
{expenseLog.map((expense) => (
<p>
{expense.name} {expense.amount}
</p>
))}
</div>
);
}
export default App;
Your variable expenseLog is defined in the function body of your react component. That means react will initialize the variable on every render and thus forget all state that you mutated.
As you did with count and name, you need to wrap expensesLog into a React useState hook in order to maintain state inside your component.
Add the data to a new expenses state.
Remove the count and name states, and add a new one input that deals with both input changes.
Add a data attribute to each input. We can use that to update the input state when it changes.
Remove the native DOM methods. When you click the button reinitialise the input state. The change in state will be reflected in the input values.
const { useState } = React;
function Example({ data }) {
const inputInit = { name: '', amount: 0 };
const [expenses, setExpenses] = useState(data);
const [input, setInput] = useState(inputInit);
function handleChange(e) {
const { dataset: { type }, value } = e.target;
setInput({ ...input, [type]: value });
}
function handleClick() {
const { name, amount } = input;
setExpenses([ ...expenses, { name, amount } ]);
setInput(inputInit);
}
return (
<div onChange={handleChange}>
Name: <input data-type="name" type="text" value={input.text}/>
Amount: <input data-type="amount" type="number" value={input.number} />
<button onClick={handleClick}>Add entry</button>
<div className="list">
{expenses.map(obj => {
return <p>{obj.name}: {obj.amount}</p>;
})}
</div>
</div>
);
}
const data=[{name:'Expense1',amount:13},{name:'Expense2',amount:100}];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
* { font-size: 0.95em; }
input { display: block; }
button { margin-top: 1em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Send form information to another component

I designed a flash card and I want to change the text of the card when I click on the button, but I do not know how to send the form information to the App component to update the state.
App component
function App() {
const [flashCard, setFlashCard] = useState({
word: "test",
persianEquivalent: "test",
});
const setFlashCardHandler = (e) => {
setFlashCard({ flashCard: e });
};
return (
<div className="container">
<Form setFlashCard={setFlashCardHandler} />
<FlashCard flashCard={flashCard} />
</div>
);
}
export default App;
Form component:
function Form({ setFlashCard }) {
const [valueEn, setValueEn] = useState();
const [valuePer, setValuePer] = useState();
const setValueHandlerEn = (e) => {
setValueEn(e.target.value);
};
const setValueHandlerPer = (e) => {
setValuePer(e.target.value);
};
const setFlashCardHandler = (e) => {
e.preventDefault();
setFlashCard((e)=>{valueEn=e.target[0].value});
};
return (
<form onSubmit={setFlashCardHandler}>
<input
id="word-input"
placeholder="world"
value={valueEn}
onChange={setValueHandlerEn}
/>
<input
id="persian-equivalent-input"
placeholder="Equivalent"
value={valuePer}
onChange={setValueHandlerPer}
/>
<button id="submit-btn">send</button>
</form>
);
}
export default Form;
There is an issue with how you set values.
Pass setFlashCard as the prop to Form
<Form setFlashCard={setFlashCard} />
In Form change the setFlashCardHandler as below.
const setFlashCardHandler = (e) => {
e.preventDefault();
setFlashCard({ word: valueEn, persianEquivalent: valuePer });
};
Set empty string ("") as default state to avoid sending undefined.
const [valueEn, setValueEn] = useState("");
const [valuePer, setValuePer] = useState("");
Please add new updateFlashCard props to to component.
Like:
<Form updateFlashCard={(e) => setFlashCard(e)}/>
And change value in updateFlashCard state from form component
Like:
function Form({ updateFlashCard }) {
const [valueEn, setValueEn] = useState();
const [valuePer, setValuePer] = useState();
const setValueHandlerEn = (e) => {
setValueEn(e.target.value);
};
const setValueHandlerPer = (e) => {
setValuePer(e.target.value);
};
const setFlashCardHandler = (e) => {
e.preventDefault();
updateFlashCard(e.target[0].value) // update from here
};
return (
<form onSubmit={setFlashCardHandler}>
<input
id="word-input"
placeholder="world"
value={valueEn}
onChange={setValueHandlerEn}
/>
<input
id="persian-equivalent-input"
placeholder="Equivalent"
value={valuePer}
onChange={setValueHandlerPer}
/>
<button id="submit-btn">send</button>
</form>
);
}
export default Form;

Need help filtering an autocomplete array with react v17

Most tutorials I found on this subject were outdated. So here I am.
What I expect to do with this app is, input text into the field and filter the results based on what you input. Currently I'm stuck and I've been through so many array methods such as filter, indexOf etc. I'm sure I am overthinking the issue so I need help. Here's the code I have currently:
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then((res) => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue)
setCountryArr(filteredSuggestions)
}
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{countryArr.map((country) => {
return (
<ul key={country.numericCode}>
{country.name}
</ul>
)
})}
</div>
);
}
export default Autocomplete;
You should not change to actual data source (countryArr) otherwise it reset and store last filtered on that. so I create state variable filteredCountryArr for filter and setting up filtered valued on that.
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
const [filteredCountryArr, setFilteredCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then(res => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value;
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue);
setFilteredCountryArr(filteredSuggestions);
};
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{filteredCountryArr && filteredCountryArr.length > 0 && filteredCountryArr.map(country => {
return <ul key={country.numericCode}>{country.name}</ul>;
})}
</div>
);
}
export default Autocomplete;

InvalidValueError: not an instance of HTMLInputElement in React project

I'm trying to add in my React project a google autocomplete feature to my location input but I get this error: InvalidValueError: not an instance of HTMLInputElement.
I guess when the event fires I get the error but I cannot figure out where it comes from.
Here's my code
Search.js
import React, { useState, useContext } from "react";
import DisplaySearchBar from "../layout/DisplaySearchBar";
import RestContext from "../context/restaurant/restContext";
import AlertContext from "../context/alert/alertContext";
const Search = () => {
const restContext = useContext(RestContext);
const alertContext = useContext(AlertContext);
const [where, setWhere] = useState("");
const [what, setWhat] = useState("");
const [sortBy, setSortBy] = useState("best_match");
const [city, setCity] = useState("");
const sortByOptions = {
"Best Match": "best_match",
"Highest Rated": "rating",
"Most Reviewed": "review_count",
};
// give active class to option selected
const getSortByClass = (sortByOption) => {
if (sortBy === sortByOption) {
return "active";
} else {
return "";
}
};
// set the state of a sorting option
const handleSortByChange = (sortByOption) => {
setSortBy(sortByOption);
};
//handle input changes
const handleChange = (e) => {
if (e.target.name === "what") {
setWhat(e.target.value);
} else if (e.target.name === "where") {
setWhere(e.target.value);
}
};
const onSubmit = (e) => {
e.preventDefault();
if (where && what) {
restContext.getRestaurants({ where, what, sortBy });
setWhere("");
setWhat("");
setSortBy("best_match");
} else {
alertContext.setAlert("Please insert somethiing", "error");
}
};
// displays sort options
const renderSortByOptions = () => {
return Object.keys(sortByOptions).map((sortByOption) => {
let sortByOptionValue = sortByOptions[sortByOption];
return (
<li
className={getSortByClass(sortByOptionValue)}
key={sortByOptionValue}
onClick={() => handleSortByChange(sortByOptionValue)}
>
{sortByOption}
</li>
);
});
};
// google suggestion
const handleScriptLoad = () => {
const handlePlaceSelect = () => {
// Extract City From Address Object
const addressObject = autocomplete.getPlace();
const address = addressObject.address_components;
// Check if address is valid
if (address) {
// Set State
setCity(address[0].long_name);
}
};
const options = {
types: ["(cities)"],
}; // To disable any eslint 'google not defined' errors
// Initialize Google Autocomplete
/*global google*/ let autocomplete = new google.maps.places.Autocomplete(
document.getElementById("autocomplete"),
options
);
// address.
autocomplete.setFields(["address_components", "formatted_address"]);
// Fire Event when a suggested name is selected
autocomplete.addListener("place_changed", handlePlaceSelect);
};
return (
<DisplaySearchBar
onSubmit={onSubmit}
handleChange={handleChange}
renderSortByOptions={renderSortByOptions}
where={where}
what={what}
handleScriptLoad={handleScriptLoad}
/>
);
};
export default Search;
DisplaySearch.js
import React, { useContext } from "react";
import PropTypes from "prop-types";
import RestContext from "../context/restaurant/restContext";
//Import React Script Libraray to load Google object
import Script from "react-load-script";
const DisplaySearchBar = ({
renderSortByOptions,
onSubmit,
where,
handleChange,
what,
handleScriptLoad,
}) => {
const restContext = useContext(RestContext);
const { restaurants, clearSearch } = restContext;
const googleUrl = `https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_API_KEY}&libraries=places`;
return (
<div className="searchBar">
<h1>Where are you going to eat tonigth?</h1>
<div className="searchBar-sort-options">
<ul>{renderSortByOptions()}</ul>
</div>
<form onSubmit={onSubmit} className="searchBar-form">
<div className="searchBar-input">
<Script url={googleUrl} onLoad={handleScriptLoad} />
<input
type="text"
name="where"
placeholder="Where do you want to eat?"
value={where}
onChange={handleChange}
/>
<input
type="text"
name="what"
placeholder="What do you want to eat?"
onChange={handleChange}
value={what}
/>
</div>
<div className="searchBar-submit">
<input
className="myButton button"
type="submit"
name="submit"
value="Search"
></input>
{restaurants.length > 0 && (
<button className="clearButton button" onClick={clearSearch}>
Clear
</button>
)}
</div>
</form>
</div>
);
};
DisplaySearchBar.propTypes = {
renderSortByOptions: PropTypes.func.isRequired,
where: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
what: PropTypes.string.isRequired,
handleScriptLoad: PropTypes.func.isRequired,
};
export default DisplaySearchBar;
Thanks for your help
Your code is quiet complex. You might be able to save some lines and make your component leaner with one of the npm packages out there. I would suggest you to give https://github.com/hibiken/react-places-autocomplete a shot.
For performance improvements also consider using React's useMemo and useCallback in functional Components https://reactjs.org/docs/hooks-reference.html#usememo
I found the issue.
I didn't give the id "autocomplete" to my input therefore when the event was firing it couldn't reach it.

How to display the state on the same page when clicked Submit button in react

I have made a form in react which takes input from the user and stores it in the state. Now, I want to display the state values when the user clicks Submit button in an input field just below the submit button in React.
Im new to react.
You have to make an object (E.g. Credentials) and when someone clicks the button, credential takes the props of the state like this:
App.js
//import code....
import Form from './Form.js'
//class app code.....
//in the render method:
render() {
return (
<Form />
)
}
Form.js
// import code ....
state = {
firstName: '', // or what you want
lastName: '', // or what you want
email: '', // or what you want
send: false,
}
//handleChange function
const handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
}
//handleClick function
const handleClick = () => {
this.setState({send: true})
}
In the Render method
render() {
return (
<div>
<input name='firstName' onChange={handleChange} />
<input name='lastName' onChange={handleChange} />
<input name='email' onChange={handleChange}/>
<button onClick={handleClick}>Send</button>
{send &&
<Credentials
firstName={this.state.firstName}
lastName={this.state.lastName}
email={this.state.email}
/>
}
</div>
)
}
export default Form // or your file's name
In the Credential.js
//import code...
const Credentials = ({firstName, lastName, email}) => {
return (
<h2>firstName is: {firstName}</h2>
<h4>lastName is: {lastName}</h4>
<p>email is: {email}</p>
)
}
export default Credentials
In React you can use 'useState' to initiate a number or any kind of input. Then set that number when the user clicks on a button.
import React, { useState } from "react";
function App() {
const [number, setNumber] = useState();
let typedNumber = 0;
const btnHandler = (e) => {
e.preventDefault();
setNumber(typedNumber);
};
return (
<div>
<form onSubmit={btnHandler}>
<input type="text" onChange={(e) => (typedNumber = e.target.value)} />
<input type="submit" />
</form>
<p>{number}</p>
</div>
);
}
export default App;

Categories

Resources