How to handle parameters between parent component and child component - reactjs

This is working perfectly as a single component. I am trying to remove the search function because I render the component in another component which is embedded in a list. I don't want the search to appear in multiple places. I want to separate it and put it in a parent component.
I don't know how to handle this. I have tried to use props, probably I am doing it the wrong way.
import React, {useState} from 'react'
function TagsInput(props) {
const [tags, setTags] = useState([])
const [search, setSearch] = useState("");
const addTags = event => {
if (event.key === "Enter" && event.target.value !== "") {
setTags([...tags, event.target.value]);
props.selectedTags([...tags, event.target.value]);
event.target.value = "";
}
};
const removeTags = index => {
setTags([...tags.filter(tag => tags.indexOf(tag) !== index)]);
};
const handleFilterChange = e => {
setSearch(e.target.value)
}
function DataSearch(rows) {
return rows.filter((row) => row.toLowerCase().indexOf(search.toLowerCase()) > -1);
}
const searchPosts = DataSearch(tags);
return (
<>
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search"} />
</div>
<div className="tags-input">
<ul>
{searchPosts.map((tag, index) => (
<li key={index}>
<span>{tag}</span>
<i
className="material-icons"
onClick={() => removeTags(index)}
>x</i>
</li>
))}
</ul>
<input
type="text"
onKeyUp={event => addTags(event)}
placeholder="Press enter to add tags" />
</div>
</>
)
}
export default TagsInput
I am using this component in another component. I want to remove the search input but I don't know how to do it as it is component of a function not defined.
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search"} />
</div>
In the parent component
<TagsInput /> is embedded in it map function and I want avoid the search being created in multiple li.
Thank you and I am sorry for dump question as I am new to it.

What exactly do you need from this component? If you just need to remove the search input box you can just remove
<div>
<input
value={search}
onChange={handleFilterChange}
placeholder={'Search'}
/>
</div>
It seems you are calling this component inside a parent component loop. If you just need to call this component once you have to use it outside the loop.

Related

How to pass prop from one component to another component

I am trying to pass the name, bio, experience, ctc props from InputForUserProfile to the userprofile.So that I can display them in my userprofile when ever any changes happen. But I can't pass them and itsays undefined when I logged them in console.Note(userprofile.js is a custom web component I created)These are the codes I tried.
App.js:
import "./App.css";
import Routing from "./Routing";
function App() {
return (
<div className="App">
<Routing/>
</div>
);
}
export default App;
Routing.js:
import InputForUserProfile from "./InputForUserProfile";
import "./userprofile.js";
import { Link, Route, Routes } from "react-router-dom";
const Routing = ({name, bio, experience, ctc}) => {
console.log("1", name);
return (
<>
<nav>
<ul>
<li>
<Link to="*">InputFields</Link>
</li>
<li>
<Link to="/userprofile">userprofile</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="*" element={<InputForUserProfile />} />
<Route
path="/userprofile"
element={
<user-profile
name={name}
bio={bio}
exp={experience}
ctc={ctc}
/>
}
/>
</Routes>
</>
);
};
export default Routing;
InputForUserProfile.js:
import { useState } from "react";
import { Route, Routes } from "react-router-dom";
import "./userprofile.js";
const InputForUserProfile = () => {
const [name, setName] = useState(localStorage.getItem("name") || "");
const [bio, setBio] = useState(localStorage.getItem("bio") || "");
const [experience, setExperience] = useState(
localStorage.getItem("experience") || ""
);
const [ctc, setCtc] = useState(localStorage.getItem("ctc") || "");
const handleSubmit = (event) => {
event.preventDefault();
localStorage.setItem("name", name);
localStorage.setItem("bio", bio);
localStorage.setItem("experience", experience);
localStorage.setItem("ctc", ctc);
};
return (
<>
<form onSubmit={handleSubmit}>
<label>
Change User Name:
<input
type="text"
placeholder="Change User Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<br />
<label>
Change User Bio:
<textarea
placeholder="Change User Bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
</label>
<br />
<label>
Change User Experience:
<input
type="text"
placeholder="Change User Experience"
value={experience}
onChange={(e) => setExperience(e.target.value)}
/>
</label>
<br />
<label>
Change User CTC:
<input
type="text"
placeholder="Change User CTC"
value={ctc}
onChange={(e) => setCtc(e.target.value)}
/>
</label>
<br />
<button type="submit">Save Changes</button>
</form>
<Routes>
<Route
path="/userprofile"
element={
<user-profile name={name} bio={bio} exp={experience} ctc={ctc} />
}
/>
</Routes>
</>
);
};
export default InputForUserProfile;
userprofile.js(custom web component):
class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<div id="profile">
<br /><br />
<img
src=""
alt="Profile Picture"
/>
<h1>
Name:
<p id="name"></p>
</h1>
<h1>
BIO:
<p id="bio"></p>
</h1>
<h1>
Experiance:
<p id="exp"></p>
</h1>
<h1>
CTC:
<p id="CTC"></p>
</h1>
<input type="text" id="user-name" class="hide-input" placeholder="changeusername">
<input type="text" id="user-bio" class="hide-input" placeholder="changeuserbio">
<input type="text" id="user-experience" class="hide-input" placeholder="changeuserexperience">
<input type="text" id="user-CTC" class="hide-input" placeholder="changeuserCTC">
<button id="save-button" class="hide-input" >save</button>
<button id="edit-button" >Edit Profile</button
><br /><br />
</div>`;
}
connectedCallback() {
const userVaule = this.shadowRoot.querySelector("div");
this.shadowRoot.querySelector('#name').textContent = this.getAttribute("name");
this.shadowRoot.querySelector('#bio').textContent = this.getAttribute("bio");
this.shadowRoot.querySelector('#exp').textContent = this.getAttribute("exp");
this.shadowRoot.querySelector('#CTC').textContent = this.getAttribute("ctc");
userVaule
.querySelector("#save-button")
.addEventListener("click", this.saveProfile.bind(this));
userVaule
.querySelector("#edit-button")
.addEventListener("click", this.editProfile.bind(this));
userVaule.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.add("hide-input");
});
}
editProfile() {
this.shadowRoot.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.remove("hide-input");
});
this.shadowRoot.querySelector("#user-name").value =
localStorage.getItem("name") || "";
this.shadowRoot.querySelector("#user-bio").value =
localStorage.getItem("bio") || "";
this.shadowRoot.querySelector("#user-experience").value =
localStorage.getItem("experience") || "";
this.shadowRoot.querySelector("#user-CTC").value =
localStorage.getItem("ctc") || "";
}
saveProfile() {
this.shadowRoot.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.add("hide-input");
});
let name = this.shadowRoot.querySelector("#name");
let bio = this.shadowRoot.querySelector("#bio");
let exp = this.shadowRoot.querySelector("#exp");
let CTC = this.shadowRoot.querySelector("#CTC");
const userName = this.shadowRoot.querySelector("#user-name").value;
localStorage.setItem("name", userName);
const userBio = this.shadowRoot.querySelector("#user-bio").value;
localStorage.setItem("bio", userBio);
const userExperience =
this.shadowRoot.querySelector("#user-experience").value;
localStorage.setItem("exp", userExperience);
const userCTC = this.shadowRoot.querySelector("#user-CTC").value;
localStorage.setItem("CTC", userCTC);
name.textContent = userName;
bio.textContent = userBio;
exp.textContent = userExperience;
CTC.textContent = userCTC;
}
}
customElements.define("user-profile", UserProfile);
There are two problems with your code - one for now and another in future.
First is properties vs attributes. React doesn't support passing props (non-primitive types like objects, array, etc.) to the custom elements. Only values like strings, numbers are supported and they are passed as attributes instead of DOM properties. (But this doesn't look to be the issue right now as all the data is just strings).
The way to get around is to wrap your web component in some react component and use the hook useRef to get instance of actual web component and when the props changes, you change the prop of the web component like:
function MyWrapperComp(props) {
const ref = useRef(null);
// Observe
useEffect(() => {
// Set the name as prop as opposed to attribute
ref.current?.name = props.name;
}, props.name);
return (
<user-profile ref={ref}></user-profile>
);
}
If you are fine with using just attribute (as opposed to props), then the second issue is the design of your web component. It may happen that your component is initialized but react has not yet passed the props to the component (async behavior or something similar - maybe localstorage empty initially). To get around that, you should listen for attributeChangedCallback lifecycle event or use MutationObserver to observe attribute changes done by React. For example:
class UserProfile extends HTMLElement {
// Rest of the code.
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'name' && oldValue !== newValue) {
// Write code here when attribute `name` is changed.
}
}
}
Finally to answer your original question - How to pass prop from one component to another component - unless you are using any declaration abstraction on top of web components, the only way is to grab the instance of the component and pass props to it using that instance.
On a side note, writing web components without any abstraction is cumbersome and extremely error prone. I would recommend something like lit-element to author web components it provide many utilities to ease the pain.

Use array of strings in React Hook Form

In a form that I am making the material that is being created in the form should have multiple width options that can be added. This means that I will have a text input where the user can add an option, and when this option is added, it should be added to the React Hook Form widthOptions array, without using the regular react state. How would one do this? How do you add an item to the total React Hook Form state, I only see options for just one input field corresponding to a property.
This is how i would do it using the regular React state
import { TrashIcon } from "#heroicons/react/24/outline";
import React, { useRef, useState } from "react";
const Test = () => {
const [widthOptions, setWidthOptions] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const removeWidthOption = (widthOption: string) => {
setWidthOptions(widthOptions.filter((option) => option !== widthOption));
};
const addWidthOption = (widthOption: string) => {
setWidthOptions([...widthOptions, widthOption]);
};
const editWidthOptions = (widthOption: string, index: number) => {
const newWidthOptions = [...widthOptions];
newWidthOptions[index] = widthOption;
setWidthOptions(newWidthOptions);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => addWidthOption(inputRef?.current?.value)}>
Add Width Option
</button>
{widthOptions.map((option, index) => (
<div className="flex">
<input
type="text"
value={option}
onChange={() => editWidthOptions(option, index)}
/>
<button type="button" onClick={() => removeWidthOption(option)}>
<TrashIcon className="w-5 h-5 mb-3 text-gray-500" />
</button>
</div>
))}
</div>
);
};
export default Test;
You can just the controller component for this as for all other fields.
Since you have not shared any of you code here is a generic multi-select
<Controller
name={name}
render={({ field: { value, onChange, ref } }) => {
return (
// You can use whatever component you want here, the you get the value from the form and use onChange to update the value as you would with a regular state
<Test
widthOptions={value}
setWidthOptions={onChange}
/>
);
}}
/>;
https://react-hook-form.com/api/usecontroller/controller/
And in you Test component remove the state and get the props instead
const Test = ({widthOptions, setWidthOptions}) => {
const inputRef = useRef<HTMLInputElement>(null);
.
.
.

Deleting items from an array React (infinite re-render loop error)

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)
};

Why Todo List in React erases always the last item?

I've been trying to make a Todo List App Work with React Hooks.
Everything works just fine when I use <span>{todo}</span>. It just delete the element that I click. But when I change <span>{todo}</span> for <input></input>, every 'X' that I click to delete always delete the last element. I just don't know what's happening, as the keys aren't changed.
Todo.js Component:
import React, { useState } from 'react';
const TodoForm = ({ saveTodo }) => {
const[value, setValue] = useState('');
return (
<form
onSubmit={event => {
event.preventDefault();
saveTodo(value);
setValue('');
}}
>
<input onChange={event => setValue(event.target.value)} value={value} />
</form>
)
}
const TodoList =({ todos, deleteTodo }) => (
<ul>
{
todos.map((todo, index) => (
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
))
}
</ul>
);
const Todo = () => {
const [todos, setTodos] = useState([]);
return (
<div className="App">
<h1>Todos</h1>
<TodoForm saveTodo={todoText => {
const trimmedText = todoText.trim();
if(trimmedText.length > 0) {
setTodos([...todos, trimmedText]);
}
}}
/>
<TodoList
todos={todos}
deleteTodo={todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}}
/>
</div>
);
};
export default Todo;
It changes the deletion behavior when I change:
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
to:
<li key={index.toString()}>
<input></input>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
Are you sure that this is the case? Or it just seems to behave that way, because you render the input without any values? If I paste your code (and adjust the input to actually include the value of the todo) into a CodeSandbox it deletes the correct element. Please also consider that using indexes as list keys should be seen as the "last resort" (See React docs).
The problem is that you are using index as the key. Therefore the last item (key that stops existing) is removed and all the other items are just updated.
Instead, create some kind of unique id for your todos and use that id as key.

custom method in nested, dynamic child component doesnt fire React

I have a component inside my App that processes data from an API and uses it to dynamically create children (which are defined in their own components) based on one of the parameter values from the API call. I am able to get the data through to render the components, but the onClick methods don't work. Here's some code:
The parent Component(within the return):
{this.state.customInputs.map((input, i) => {
if(input.type === 'radio'){
return(
<div className="preference-section" key={i}>
<h2>{input.name}</h2>
<RadioCustomInput
radioActiveState={this.state.radioActiveState}
handleRadioClick={() => this.handleRadioClick()}
options={input.options} />
</div>
)
} else if ( input.type === 'checkbox'){
return(
<div className="preference-section" key={i}>
<h2>{input.name}</h2>
<CheckboxCustomInput
selectedBoxes={this.state.selectedCheckboxes}
handleOnClick={() => this.handleCheckboxClick()}
options={input.options} />
</div>
)
}
})
}
The children components:
class CheckboxCustomInput extends Component {
handleCheckboxOnClick = (e) => {
e.preventDefault();
let checkboxState = e.target.value;
console.log(checkboxState);
return this.props.handleCheckboxClick(checkboxState);
}
render(){
return(
<div className="preference-options">
{this.props.options.map((item, i) => {
return(
<div key={i} className="checkbox-group">
<label
className={this.props.selectedBoxes.includes(item) ? "active-box checkbox-label" : "checkbox-label"}
htmlFor={item}>
<input
className="checkbox-input"
type="checkbox"
value={item}
id={item + '_checkbox'}
onClick={() => this.handleCheckboxOnClick()} />
{item}
</label>
</div>
);
})}
</div>
)
}
}
export default CheckboxCustomInput;
and the other:
class RadioCustomInput extends Component{
handleRadioOnClick = (e) => {
e.preventDefault();
let radioState = e.target.value;
let radioName = e.target.name;
console.log(radioState);
return this.props.handleRadioClick(radioState, radioName);
}
render(){
return(
<div className="radio-group">
{this.props.options.map((item, i) => {
return(
<label
className={this.props.radioActiveState === item ? "active-box radio-label" : "radio-label"}
htmlFor={item + '-card'} key={i}>
<input
className="radio-input"
type="radio"
name={item}
value={item}
id={item + '_radio'}
onClick={() => this.handleRadioOnClick()} />
{item}
</label>
)
})}
</div>
)
}
}
export default RadioCustomInput;
Is the radio working but the checkbox isn't? It looks like you're passing a prop called handleOnClick to your checkbox component, but expecting a handleCheckboxClick prop.
After editing the method prop invocation in the Checkbox component, I was able to pass the event through using the following tweak to my onClick method for each child: onClick={(e) => this.handleCheckboxOnClick(e)}
I also, as suggested by #larz, removed the function return in the parent and passe dthe method by reference only there.
The rest of my issue was CSS, in that the <input/> had a css rule of visibility:hidden and the <label/> element was handling the CSS visual interaction withthe onClick. To fix this, I moved the onClick method to the <label/> and added a native JS method to the chain for the property definitions within the child-local onClick methods, like so:
let radioState = e.target.childNodes[0].value;

Resources