I'm working on a project ( Airbnb clone - for personal learning), and on this project I am trying to understand concepts and conventions thoroughly.
Here's the problem:
On this page I use a date input custom component like this:
-- parent component -
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { testData } from "../utils/mockData";
import Header from "../components/header/Header";
import DateSearchBar from "../components/header/DateSearchBar";
import Guests from "../components/cards/Guests";
const Property = () => {
const propertyId = useParams();
const convertedId = parseInt(propertyId.id);
const [noOfGuests, setNoOfGuests] = useState(0);
const [fromDate, setFromDate] = useState();
const [toDate, setToDate] = useState();
const selectedProperty = testData.filter(item => item.id === propertyId.id)
const handleGuests = (val) => {
if (noOfGuests === 0) {
setNoOfGuests(0);
}
setNoOfGuests(noOfGuests + val)
}
const handleDate = (val) => {
setFromDate(val);
}
return (
<div>
<Header />
<div className="flex justify-center space-x-24 mt-6">
<div className=" max-w-sm rounded-xl overflow-hidden shadow-sm w-9/12">
<img
className=" text-centerw w-96 rounded-md h-64"
src={selectedProperty[0].image}
/>
<p className="h-16">{selectedProperty[0].title}</p>
</div>
<div className="ml-96">
<h4 className="text-center italic font-extrabold">From</h4>
<DateSearchBar name="fromDate" value={fromDate} handleDate={handleDate } />
<h4 className="text-center italic font-extrabold">To</h4>
<DateSearchBar name="toDate" value={fromDate} handleDate={handleDate } />
<Guests handleGuests={handleGuests} noOfGuests={noOfGuests} />
</div>
</div>
</div>
);
};
export default Property;
--- Child Component ---
import React from "react";
const DateSearchBar = ({ handleDate }) => {
return (
<div>
{/* fromDate */}
<div className="text-center">
<input
className="bg-slate-50 hover:bg-red-200 rounded-md h-12 w-80 text-center mb-16 "
type="date"
onChange={(e) => handleDate(e.target.value)}
/>
</div>
</div>
);
};
export default DateSearchBar;
The Property.js component owns the local state, I am using a callback function to set a local state in the parent component.
The problem is that I need to differ between the fromDate state and the toDate state in the parent component, but I'm not sure how to write this logic.
Obviously I can set up another date component and target it, however it beats the purpose of creating and using reusable components and keeping your code DRY.
Also, Redux/Context seem too much for this issue ( but I might be wrong)
Any ideas on how I can solve this ?
First thing is, that you are passing fromDate in both of the components. I believe you should have a value of fromDate in the first and toDate in the second.
Next, to be able to reuse handleDate for both inputs, you need to pass the element's name back to the parent along with the value and then use that name argument to differentiate between the two components.
In parent:
const handleDate = (name, value) => {
if (name === "fromDate") {
setFromDate(value)
} else if (name === "toDate") {
setToDate(value)
}
}
In child:
onChange={e => handleDate(props.name, e.target.value)}
Another approach would be to return a method from handleDate():
In parent:
const handleDate = (name) => {
if (name === "fromDate") {
return (value) => setFromDate(value)
} else if (name === "toDate") {
return (value) => setToDate(value)
}
}
...
<DateSearchBar name="fromDate" value={fromDate} handleDate={handleDate("fromDate")} />
<DateSearchBar name="toDate" value={toDate} handleDate={handleDate("toDate")} />
In this case, you don't have to change child component.
However IMO this still isn't the simplest approach. Yes, we should try to follow these clean code recommendations but only to the point where they don't lead to further complexity. For example, in the above case we are over-complicating handleDate(), it would be a lot simpler to have separate inline change handlers for each component:
<DateSearchBar name="fromDate" value={fromDate} handleDate={val => setFromDate(val)} />
<DateSearchBar name="toDate" value={toDate} handleDate={val => setToDate(val)} />
If our form grows bigger, we can use dedicated form handling React libraries such as Formik and React-hook-form to keep our component logic simpler.
Related
whenever i try. to change an input it's show the 2 component changes of input at the same time how i show only the changed one
input.tsx
export const Input: React.FC<InputProps> = ({ type, placeholder, classNameProp, value, onChange, forwardRef, isReadOnly = false, isRequired = true, errorText }) => {
console.log(forwardRef?.current);
return (
<>
<input type={type} placeholder={placeholder} className={`p-2 mb-4 rounded-sm border-b-2 border-gray-300 focus:outline-none focus:border-orange-500 ${classNameProp}`} value={value} onChange={onChange} disabled={isReadOnly} required={isRequired} ref={forwardRef} />
</>
);
};
formSurvey.tsx
export const SurveyForm: React.FC<SurveyFormProps> = () => {
const titleRef = React.useRef<HTMLInputElement>(null);
const subjectRef = React.useRef<HTMLInputElement>(null);
const [surveyTitle, setSurveyTitle] = React.useState("");
const [surveySubject, setSurveySubject] = React.useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(surveyTitle);
let state: any = {
title: surveyTitle,
subject: surveySubject,
};
};
return (
<div className="relative">
<form className="max-w-3xl mx-auto bg-white rounded-xl p-5 mb-6" onSubmit={handleSubmit}>
<Input type="text" placeholder="Survey Title" classNameProp={`w-full ${titleRef.current?.value.length === 0 ? "border-red-500" : ""}`} value={surveyTitle} onChange={React.useCallback(setter(setSurveyTitle), [])} forwardRef={titleRef} errorText="Please fill the title field" />
<Input type="text" placeholder="Subject Line" classNameProp={`w-full ${subjectRef.current?.value.length === 0 ? "border-red-500" : ""}`} value={surveySubject} onChange={React.useCallback(setter(setSurveySubject), [])} forwardRef={subjectRef} errorText="Please fill the subject field" />
<div className="form-footer flex justify-center space-x-10">
<button className="bg-orange-500 text-white px-4 py-2 rounded-3xl font-semibold" type="submit">
Submit
</button>
</div>
</form>
</div>
);
};
setter.ts
type Set<T> = React.Dispatch<React.SetStateAction<T>>;
type ChangeEvent<E> = React.ChangeEvent<E>;
type Input = HTMLInputElement | HTMLTextAreaElement;
export function setter<T extends number | string | Date, E extends Input = HTMLInputElement>(setX: Set<T>) {
return (e: ChangeEvent<E>) => {
setX(e.target.value as T);
};
}
try to find the best way to implement reusable component in react js
you can see the code here also here
Since you use state, whenever you call setState, each children re-renders. If the components are not so big, most of the time re-renders don't matter and everything is fast enough (optimized)
But if you want to optimize this, there are techniques you can use to avoid unnecessary renders.
The quickest solution for you would be to use React.memo to wrap your inputs.
const Input = React.memo(...your component body)
Or you can use uncontrolled inputs.
Or some library that does this for you, like react-hook-form
let me explain my situation.
I am building a MERN project to my portfolio and I am trying to make a button toggle between the name of an item and a inputfield. So when the user click the pen (edit), it will add a class with the displain:none; in the div with the text coming from the MongoDB data base to hide it and will remove it from the div with the input. I could manage to do it. BUT since the amount of items can inscrease, clicking in one of them cause the toggle in all of them.
It was ok until I send some useState as props to the component.
This is my code from the App.jsx
import React, {useState, useEffect} from "react";
import Axios from "axios";
import "./App.css";
import ListItem from "./components/ListItem";
function App() {
//here are the use states
const [foodName, setFoodName] = useState("");
const [days, setDays] = useState(0);
const [newFoodName, setNewFoodName] = useState("");
const [foodList, setFoodList] = useState([]);
//here is just the compunication with the DB of a form that I have above those components
useEffect(() => {
Axios.get("http://localhost:3001/read").then((response) => {
setFoodList(response.data);
});
}, []);
const addToList = () => {
Axios.post("http://localhost:3001/insert", {
foodName: foodName,
days: days,
});
};
const updateFood = (id) => {
Axios.put("http://localhost:3001/update", {
id: id,
newFoodName: newFoodName,
});
};
return (
<div className="App">
//Here it starts the app with the form and everything
<h1>CRUD app with MERN</h1>
<div className="container">
<h3 className="container__title">Favorite Food Database</h3>
<label>Food name:</label>
<input
type="text"
onChange={(event) => {
setFoodName(event.target.value);
}}
/>
<label>Days since you ate it:</label>
<input
type="number"
onChange={(event) => {
setDays(event.target.value);
}}
/>
<button onClick={addToList}>Add to list</button>
</div>
//Here the form finishes and now it starts the components I showed in the images.
<div className="listContainer">
<hr />
<h3 className="listContainer__title">Food List</h3>
{foodList.map((val, key) => {
return (
//This is the component and its props
<ListItem
val={val}
key={key}
functionUpdateFood={updateFood(val._id)}
newFoodName={newFoodName}
setNewFoodName={setNewFoodName}
/>
);
})}
</div>
</div>
);
}
export default App;
Now the component code:
import React from "react";
//Material UI Icon imports
import CancelIcon from "#mui/icons-material/Cancel";
import EditIcon from "#mui/icons-material/Edit";
//import CheckIcon from "#mui/icons-material/Check";
import CheckCircleIcon from "#mui/icons-material/CheckCircle";
//App starts here, I destructured the props
function ListItem({val, key, functionUpdateFood, newFoodName, setNewFoodName}) {
//const [foodList, setFoodList] = useState([]);
//Here I have the handleToggle function that will be used ahead.
const handleToggle = () => {
setNewFoodName(!newFoodName);
};
return (
<div
className="foodList__item"
key={key}>
<div className="foodList__item-group">
<h3
//As you can see, I toggle the classes with this conditional statement
//I use the same classes for all items I want to toggle with one click
//Here it will toggle the Food Name
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__name"
}>
{val.foodName}
</h3>
<div
className={
newFoodName
? "foodList__item-newName-group"
: "foodList__item-newName-delete"
}>
//Here is the input that will replace the FoodName
<input
type="text"
placeholder="The new food name..."
className="foodList__item-newName"
onChange={(event) => {
setNewFoodName(event.target.value);
}}
/>
//Here it will confirm the update and toggle back
//Didn't implement this yet
<div className="foodList__icons-confirm-group">
<CheckCircleIcon
className="foodList__icons-confirm"
onClick={functionUpdateFood}
/>
<small>Update?</small>
</div>
</div>
</div>
//here it will also desappear on the same toggle
<p
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__day"
}>
{val.daysSinceIAte} day(s) ago
</p>
<div
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__icons"
}>
//Here it will update, and it's the button that toggles
<EditIcon
className="foodList__icons-edit"
onClick={handleToggle}
/>
<CancelIcon className="foodList__icons-delete" />
</div>
</div>
);
}
export default ListItem;
I saw a solution that used different id's for each component. But this is dynamic, so if I have 1000 items on the data base, it would display all of them, so I can't add all this id's.
I am sorry for the very long explanation. It seems simple, but since I am starting, I spent the day on it + searched and tested several ways.
:|
Why the input only taking inputs from second input only?
import React, { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
let toDoIs = document.getElementById("toDoInput");
const [ToDo, setToDoIs] = useState("d");
const [ToDoArray, setToDoArray] = useState([]);
return (
<div>
<h1>ToDo</h1>
<input
id="toDoInput"
onChange={() => {
setToDoIs(toDoIs.value);
}}
type="text"
/>
<button
onClick={() => {
setToDoArray([...ToDoArray, { text: ToDo }]);
toDoIs.value = "";
}}
>
Add
</button>
<Item push={ToDoArray} />
</div>
);
}
export default ToDo;
Why the second input only works, which means whenever I use submit the value from second input only stored and displayed. I don't know why this happens.
There's a few problems here...
Don't use DOM methods in React. Use state to drive the way your component renders
Your text input should be a controlled component
When updating state based on the current value, make sure you use functional updates
import { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
// naming conventions for state typically use camel-case, not Pascal
const [toDo, setToDo] = useState("d");
const [toDoArray, setToDoArray] = useState([]);
const handleClick = () => {
// use functional update
setToDoArray((prev) => [...prev, { text: toDo }]);
// clear the `toDo` state via its setter
setToDo("");
};
return (
<div>
<h1>ToDo</h1>
{/* this is a controlled component */}
<input value={toDo} onChange={(e) => setToDo(e.target.value)} />
<button type="button" onClick={handleClick}>
Add
</button>
<Item push={toDoArray} />
</div>
);
}
export default ToDo;
i have an array, called reportsData, then i need to filter it, generating some checkboxes with each of them having a label based on each name that comes from another array (emittersData), so basically i set it like this:
const [searchUser, setSearchUser] = useState<string[]>([])
const mappedAndFiltered = reportsData
.filter((value: any) =>
searchUser.length > 0 ? searchUser.includes(value.user.name) : true
)
Then i render my checkboxes like this:
function EmittersCheckboxes () {
const [checkedState, setCheckedState] = useState(
new Array(emittersData.length).fill(false)
)
const handleOnChange = (position: any, label: any) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
)
setSearchUser((prev) =>
prev.some((item) => item === label)
? prev.filter((item) => item !== label)
: [...prev, label]
)
setCheckedState(updatedCheckedState)
};
return (
<div className="App">
{emittersData.map((value: any, index: any) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
className="h-4 w-4 focus:bg-indigo border-2 border-gray-300 rounded"
type="checkbox"
id={`custom-checkbox-${index}`}
name={value.Attributes[2].Value}
value={value.Attributes[2].Value}
checked={checkedState[index]}
onChange={() => handleOnChange(index, value.Attributes[2].Value)}
/>
<label className="ml-3 font-medium text-sm text-gray-700 dark:text-primary" htmlFor={`custom-checkbox-${index}`}>{value.Attributes[2].Value}</label>
</div>
</div>
</li>
);
})}
</div>
)
}
And on the react component i am rendering each checkbox, that is a li, like:
<ul><EmittersCheckboxes /></ul>
And i render the mappedAndFiltered on the end.
Then it is fine, when i click each generated checkbox, it filters the array setting the state in setSearch user and the array is filtered.
You can check it here: streamable. com /v6bpk6
See that the filter is working, the total number of items in the array is changing based on the checkbox selected (one or more).
But the thing is that each checkbox does not become 'checked', it remains blank (untoggled).
What am i doing wrong, why doesnt it check itself?
You've defined your EmittersCheckboxes component inside another component. and every time that the parent component renders (by state change) your internal component is redefined, again and again causing it to lose it's internal state that React holds for you.
Here's a simplified example:
import React, { useState } from "react";
function CheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>CheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
export default function App() {
const [counter, setCounter] = useState(1);
function InternalCheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>InternalCheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
return (
<>
<InternalCheckboxeComponent />
<CheckboxeComponent />
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
</>
);
}
There's the App (parent component) with its own state (counter), with a button to change this state, clicking this button will increase the counter, causing a re-render of App. This re-render redefines a new Component named InternalCheckboxeComponent every render.
The InternalCheckboxeComponent also has an internal state (checkedState).
And there's an externally defined functional component named CheckboxeComponent, with this component React is able to hold its own state, because it's not redefined (It's the same function)
If you set the state of each to be "checked" and click the button, this will cause a re-render of App, this will redefine the InternalCheckboxeComponent function, causing React to lose its state. and the CheckboxeComponent state remains in React as it's the same function.
I am pretty new to react and got really stuck on something. I am working on a sort of ordering application. People can order a product and can select all ingredients they want. I was thinking to do this with a checkbox for each ingredient. Unfort. I just don't know how to get this fixed. Also, I am wondering if I have to use a state in my component or just a variable.
So I am mapping through the array of ingredients and for each ingredient I am displaying a checkbox to turn on/off an ingredient. So my main question, How can I adjust my object with these checkboxes, and if I need to have a state in my component to keep up to date with the checkboxes, How will I set the product to my state? Because It's coming from props.
I've tried all sort of things, for instance this from the docs:
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
But then again, How can I add the product to my state? Also, this will be different since the ingredients are in a object, and the example from the docs are just values and not in a specific object.
My component
import React from 'react';
import { Link } from 'react-router-dom';
class Order extends React.Component {
constructor(props) {
super(props);
}
handleToggle(e) {
//Handle the toggle, set the value of the ingredient to 0 or 1
}
getData(e, product) {
e.preventDefault();
console.log(product)
}
render() {
const product = this.props.products.find((product) => {
return product.id == this.props.match.params.id;
});
return (
<form className="container mx-auto px-4 pt-6" onSubmit={(e) => this.getData(e, product) }>
<Link to={`/${this.props.match.params.category}`} className="mb-4 relative block text-brand hover:text-brand-dark">← Terug naar categorie</Link>
<div className="flex flex-row items-center justify-between bg-white rounded px-8 py-8 shadow-sm mb-4">
<div className="">
<h2 className="text-brand uppercase">{product && product.name}</h2>
<div className="ingre">
<p>
{product && product.ingredients.map((item) => {
return <span className="ing text-grey-dark text-sm" key={item.name}>{item.name}</span>
})}
</p>
</div>
</div>
<div className="">
<h3 className="text-brand text-4xl">€{product && product.price}</h3>
</div>
</div>
<div className="flex flex-wrap mb-4 -mx-2">
{product && product.ingredients.map((item) => {
return (
<div className="w-1/2 mb-4 px-2" key={item.name}>
<div className="flex flex-row items-center justify-between bg-white rounded px-8 py-8 shadow-sm">
<div>
<h3 className="text-grey-dark font-normal text-sm">{item.name}</h3>
</div>
<div>
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e)}/>
</div>
</div>
</div>
);
})}
</div>
<button type="submit" className="bg-brand hover:bg-brand-dark text-white font-bold py-4 px-4 rounded">
Order this product
</button>
</form>
);
}
}
export default Order;
An example of a product
So actually I need to keep track of the product and bind the value's of the ingredients to the checkbox. If it's not checked the value must become 0 (or false).
Edit:
Parent component passing props
// React deps
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
// Custom components
import Navigation from './components/General/Navigation'
// Pages
import Index from './components/Index'
import Category from './components/Category'
import Order from './components/Order'
// Data
import products from './Data'
class App extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
}
componentWillMount() {
setTimeout(() => {
this.setState({products});
}, 100);
}
render() {
return (
<main className="App font-sans">
<Router>
<div>
<Navigation logo="Jackies" />
<Switch>
<Route exact path="/" component={Index} />
<Route exact path="/:category" render={(props) => <Category {...props} products={this.state.products} />}/>
<Route exact path="/:category/:id" render={(props) => <Order {...props} products={this.state.products} />}/>
</Switch>
</div>
</Router>
</main>
);
}
}
export default App;
In the parent, you will pass a handler function in a onIngredientToggle prop:
<Route exact path="/:category/:id" render={(props) => <Order {...props} products={this.state.products} onIngredientToggle={this.handleIngredientToggle} />}/>
Then define the handleIngredientToggle function:
function handleIngredientToggle(productId, ingredientIndex, newIngredientValue) {
// basically this goes shallow cloning the objects and arrays up to
// the point it changes the ingredient value property
let products = [...this.state.products];
let modifiedProductIndex = products.findIndex(p => p.id === productId);
let product = {...products[modifiedProductIndex]};
products[modifiedProductIndex] = product;
product.ingredients = [...products[modifiedProductIndex].ingredients];
product.ingredients[ingredientIndex] = {...product.ingredients[ingredientIndex], value: newIngredientValue};
this.setState({products});
}
// If you prefer, the above function can be replaced with:
function handleIngredientToggle(productId, ingredientIndex, newIngredientValue) {
// deep clone products
let products = JSON.parse(JSON.stringify(this.state.products));
// modify just what changed
products.find(p => p.id === productId).ingredients[ingredientIndex].value = newIngredientValue;
this.setState({products});
}
In the Order child, you will add the index argument to the map (you have two of these, just add to the second):
{product && product.ingredients.map((item, index) => {
In the checkbox pass the product and index to the handleToggle function as argument:
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e, product, index)}/>
And then in the function implementation, you will call the function received as prop from the parent:
handleToggle(e, product, index) {
this.props.onIngredientToggle(product.id, index, e.target.checked);
}
Thanks to #acdcjunior who opened my (tired) eyes, I've found a solution
Checkbox html
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e, item, product)}/>
Function on the child component
handleToggle(e, item, product) {
//Get value from checkbox and set it the opposite
let value = e.target.checked ? 1 : 0;
//Pass down the item (ingredient) and parent product and also the value
this.props.toggleIngredient(item, product, value);
}
Function on parent component to change the state
toggleIngredient(i, p, v) {
// Get the product from the state
var product = this.state.products.find((product) => {
return product.id == p.id
});
// Filter down the state array to remove the product and get new products array
let products = this.state.products.filter((product) => {
return product != product;
});
// Get the ingredient object
var object = product.ingredients.find((product) => {
return product == i;
});
// Set the value for the ingredient, either true or false (depends on checkbox state)
object.value = v;
// Push the edited product the array of products
products.push(product);
// Set the state with the new products array
this.setState({
products: products
});
}