So, are there any other ways, rather than changing value to defaultValue to change the input value manually? (When using defaultValue My program doesn't work properly)
ChildComponent = React.memo(
({ name, index, onChange, onDelete
}) => {
return (
<div>
<input value={name} onChange={onChange(index, name)} />
<button onClick = {() => onDelete(index)}>delete</button>
</div>
);
}
);
function App() {
const [names, setNames] = React.useState(["First", "Second"]);
const newName = React.useRef();
const onNameChange = (index: number, newName: string) => (event: {
target: { value: string };
}) => {
names[index] = event.target.value;
setNames(names);
};
function onNameDelete(index: number) {
const nameArr = [...names];
nameArr.splice(index, 1);
setNames(nameArr);
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={index}
name={name}
index={index}
onChange={onNameChange}
onDelete={onNameDelete}
/>
))}
</div>
);
}
The issue is in your onChange input handler in your ChildComponent. You are not using passed value by user to input at all. You need to write it similarly to your onDelete handler (with use the new value, in my snippet stored in event.target.value):
ChildComponent = React.memo(
({ name, index, onChange, onDelete
}) => {
return (
<div>
<input value={name} onChange={(event) => onChange(index, event.target.value)} />
<button onClick = {() => onDelete(index)}>delete</button>
</div>
);
}
);
Look also on the definition of the input change handler in Html documentation.
EDIT:
Another issue is your handler in your parent controller:
const onNameChange = (index: number, newName: string) => (event: {
target: { value: string };
}) => {
names[index] = event.target.value; //this is BAD, do not change your state value directly, moreover, the event is undefined here
setNames(names);
};
You need to update item in the array immutably (source Updating an Item in an Array):
const onNameChange = (index: number, newName: string) => (event: {
target: { value: string };
}) => {
const newNames = names.map((item, itemIndex) => {
if (itemIndex !== index) {
// This isn't the item we care about - keep it as-is
return item
}
// Otherwise, this is the one we want - return an updated value
return newName;
});
setNames(newNames);
};
Related
I have two children Components, when I onChange in first children, then the second children re render, I don't want to the second children re render. Online code example:
https://codesandbox.io/s/ji-ben-antd-4-24-0-forked-efg56l?file=/demo.tsx
const ChildA = (props: {
name: string;
changeValue: (key: string, value: any) => void;
}) => {
const { name, changeValue } = props;
return (
<Input
value={name}
onChange={(e) => {
changeValue("name", e.target.value);
}}
/>
);
};
const ChildB = (props: {
age: number;
changeValue: (key: string, value: any) => void;
}) => {
const { age, changeValue } = props;
console.log("==when I change name====, this component re-render");
return (
<InputNumber
value={age}
onChange={(e) => {
changeValue("age", e);
}}
/>
);
};
const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [saveValue, setSaveValue] = useState({
name: "wang",
age: 18
});
const showModal = () => {
setIsModalOpen(true);
};
const handleOk = () => {
// send value
console.log("====saveValue==", saveValue);
setIsModalOpen(false);
};
const handleCancel = () => {
setIsModalOpen(false);
};
const changeValue = (key: string, value: any) => {
const newValue = JSON.parse(JSON.stringify(saveValue));
newValue[key] = value;
setSaveValue(newValue);
};
return (
<>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
>
<ChildA name={saveValue?.name} changeValue={changeValue} />
<ChildB age={saveValue?.age} changeValue={changeValue} />
</Modal>
</>
);
};
When I change the name ,I don`t want to Child B re-render
The actual situation is that there are many sub-components in a Modal. When you click OK, the value of the sub-component is obtained, saved and sent to the server. If you have good design component ideas, please share
I don't want to the second children re render.
Wrap ChildB with React.memo for a basic memoization.
const ChildB = memo(...);
Wrap the changeValue function with React.useCallback to persist the instance.
const changeValue = useCallback(...);
Slightly modify the changeValue function so it does not use the saveValue as a dependency.
setSaveValue((prev) => {
const newValue = JSON.parse(JSON.stringify(prev));
newValue[key] = value;
return newValue;
});
Codesandbox demo
I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.
I am developing a list of items using React, Express (axios) and MongoDB.
Items of an array of the list are retrieved from MongoDB, displayed in a component and the user can add new items (and save them in MongoDB).
The problem is that Warning: Each child in a list should have a unique "key" prop" is shown because new element are added to the list using id: Math.random() attribute (although those ids are not submitted to the DB)
App.tsx (HERE the list of items is fetched with ids generated by MongoDB)
export default function App() {
const [ExpenseAndAmountList, setExpenseAndAmountList] = useState<
Array<{
id: number,
expenseTitle: string,
expenseAmount: string,
}>
>([]);
useEffect(() => {
const expensesListResp = async () => {
await axios.get('http://localhost:4000/app/expenseslist')
.then(
response => setExpenseAndAmountList(response.data && response.data.length > 0 ? response.data : []));
}
expensesListResp();
}, []);
return (
<div className="App">
<ExpenseAmountInputContainer
expenseAndAmountList={ExpenseAndAmountList}
setExpenseAndAmountList={setExpenseAndAmountList}
setTotalExpensesAmount={setTotalExpensesAmount}
totalExpenses={TotalExpensesAmount}
/>
<DynamicList
expenseAndAmountList={ExpenseAndAmountList}
currencySymbol={Currency}
setExpenseAndAmountList={setExpenseAndAmountList}
/>
</div>
);
}
ExpenseAmountInputContainer.tsx (HERE items are posted to MongoDB list without any id)
interface Props {
expenseAndAmountList: Array<ExpenseAndAmountObject>;
setExpenseAndAmountList: (value: Array<ExpenseAndAmountObject>) => void;
}
const ExpenseAmountInputContainer: React.FC<Props> = (
{
expenseAndAmountList,
setExpenseAndAmountList,
}: Props
) => {
const [Expense, setExpense] = useState<string>('');
const [Amount, setAmount] = useState<string>('');
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
axios.post('http://localhost:4000/app/expenseslist',
{
expenseTitle: Expense,
expenseAmount: Amount
});
setExpense("");
setAmount("");
const expensesListResp = async () => {
await axios.get('http://localhost:4000/app/expenseslist')
.then(
response => setExpenseAndAmountList(response.data && response.data.length > 0 ? response.data : []));
}
expensesListResp();
}
return (
<div>
<InputItem
onChange={setExpense}
onBlur={setExpense}
title="Expense"
type="text"
placeholder="Item title"
value={Expense}
/>
<InputItem
onChange={setAmount}
onBlur={setAmount}
title="Amount"
type="number"
placeholder="Expense cost"
value={Amount}
/>
<AddButton
onClick={AddItemToList}
content="Add expense"
/>
</div>
);
};
export default ExpenseAmountInputContainer;
ExpenseAndAmountObject.tsx (interface used at ExpenseAmountInputContainer.tsx)
export interface ExpenseAndAmountObject {
id: number,
expenseTitle: string,
expenseAmount: string,
}
DynamicList.tsx
import { ExpenseAndAmountObject } from '../ExpenseAndAmountObject';
interface ListItemsArray {
expenseAndAmountList: Array<ExpenseAndAmountObject>;
currencySymbol: string;
setExpenseAndAmountList: (value: Array<ExpenseAndAmountObject>) => void;
}
const DynamicList: React.FC<ListItemsArray> = (
{
expenseAndAmountList,
currencySymbol,
setExpenseAndAmountList
}: ListItemsArray) => {
return (
<>
<List>
{expenseAndAmountList.map(item => (
<ExpensesListItem
expenseTitle={item.expenseTitle}
expenseAmount={item.expenseAmount}
currencySymbol={currencySymbol}
item={item}
items={expenseAndAmountList}
setExpenseAndAmountList={setExpenseAndAmountList}
/>
))}
</List>
</>
);
}
export default DynamicList;
How could the problem be solved?
You need to pass the prop key when you render lists.
import { ExpenseAndAmountObject } from '../ExpenseAndAmountObject';
interface ListItemsArray {
expenseAndAmountList: Array<ExpenseAndAmountObject>;
currencySymbol: string;
setExpenseAndAmountList: (value: Array<ExpenseAndAmountObject>) => void;
}
const DynamicList: React.FC<ListItemsArray> = (
{
expenseAndAmountList,
currencySymbol,
setExpenseAndAmountList
}: ListItemsArray) => {
return (
<>
<List>
{expenseAndAmountList.map(item => (
<ExpensesListItem
key={} // <----- SOME UNIQUE ID HERE!!!!!!!!
expenseTitle={item.expenseTitle}
expenseAmount={item.expenseAmount}
currencySymbol={currencySymbol}
item={item}
items={expenseAndAmountList}
setExpenseAndAmountList={setExpenseAndAmountList}
/>
))}
</List>
</>
);
}
export default DynamicList;
Please see the documentation that explains this behavior https://reactjs.org/docs/lists-and-keys.html#keys
I have two Question ->
First one is I want to add user input field dynamically when user click "+" button in react.js. And if user click more times then more number of field add to the form. How to do this in react.js?
Second one when user change its value I want to store their corresponding value of each input field into my component state variable. Now here how I add their value, what data structure I use array or object or something else ? and how ?
function App() {
const inputArr = [
{
type: "text",
id: 1,
value: ""
}
];
const [arr, setArr] = useState(inputArr);
const addInput = () => {
setArr(s => {
return [
...s,
{
type: "text",
value: ""
}
];
});
};
const handleChange = e => {
e.preventDefault();
const index = e.target.id;
setArr(s => {
const newArr = s.slice();
newArr[index].value = e.target.value;
return newArr;
});
};
return (
<div>
<button onClick={addInput}>+</button>
{arr.map((item, i) => {
return (
<input
onChange={handleChange}
value={item.value}
id={i}
type={item.type}
size="40"
/>
);
})}
</div>
);
}
I would set a counter and increase it with each click of a button. I would map the counter numbers into an array and loop over creating new input elements.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
const handleClick = () => {
setCounter(counter + 1);
console.log(counter);
};
return (
<div className="App">
<button onClick={handleClick}>Hello</button>
{Array.from(Array(counter)).map((c, index) => {
return <input key={c} type="text"></input>;
})}
</div>
);
}
https://codesandbox.io/s/elastic-wave-36ous?fontsize=14&hidenavigation=1&theme=dark
I need a little help with my project. I think it is almost done, but I don't know how to finish...
So, I want to build app with input, select and button. Into input u can write for example, mettalica and after click on button app renders list with all songs, titles and tabTypes(guitar tabs). The problem is that i want to get info from select and render only that songs which includes for example player tabs.
Sandbox Code: https://codesandbox.io/s/react-example-ys6py?fontsize=14&hidenavigation=1&theme=dark
class Search extends React.Component {
state = {
searchValue: "",
songs: [],
musicTabs: [
'Dowolne',
'Bass',
'Player',
'Chords',
'Guitar'
],
result: ''
};
handleOnChange = event => {
this.setState({ searchValue: event.target.value });
};
handleSelectChange = (event) => {
this.setState({
result: event.target.value
})
console.log(this.state.result)
}
handleSearch = () => {
this.makeApiCall(this.state.searchValue);
};
makeApiCall = async searchInput => {
let api_url = `https://www.songsterr.com/a/ra/songs/byartists.json?artists=${searchInput}`;
const response = await fetch(api_url);
const songs = await response.json();
this.setState({ songs });
};
render() {
return (
<div>
<Header />
<input
name="text"
type="search"
placeholder="Search..."
onChange={event => this.handleOnChange(event)}
value={this.state.SearchValue}
/>
<Select optionValue={ this.state.musicTabs } change={ this.handleSelectChange } value={ this.state.result } />
<br />
<button onClick={this.handleSearch}>Search</button>
{this.state.songs ? (
<div>
{
this.state.songs.map((song, index) => (
<div key={index} className="lists">
<h1>Artist: <span>{song.artist.name}</span></h1>
<h2>Song title: <span>{song.title}</span></h2>
<ol>
<b>Available tabs:</b>
{song.tabTypes.map((tab, index) =>
<li key={index}> {song.tabTypes[index]} </li>
)}
</ol>
</div>
))
}
</div>
) : (
<p>Something</p>
)}
</div>
);
}
}
const Select = (props) => {
const { optionValue, change } = props;
const valueMusicTabs = optionValue.map((musicTab, index) => {
return <option name={ optionValue[index] } key={ index }> { optionValue[index] } </option>
})
return (
<>
<select onChange={ change }>
{ valueMusicTabs }
</select>
</>
)
};
Thanks for help guys!
I think you did everything right, just used the wrong prop
<Select optionValue={ this.state.musicTabs } onChange={ this.handleSelectChange } value={ this.state.result } />
the change prop on the Select component should just be changed to onChange since it's a default event it will be passed with the event to your handleChange method
I checked the codesandbox, everything was working right. this.setState is an asynchronous function. So, if you will console.log after this.setState chances are your will not log updated value. You can do it like this with a callback function.
handleSelectChange = (event) => {
this.setState({
result: event.target.value
}, () => console.log(this.state.result))
}
If you want to filter you can do that by making a function like:
filterSongs = selected => {
return songs.filter(song => song.tabTypes === selected);
}
and After that modify your handleSelectChange as:
handleSelectChange = (event) => {
let songs = filterSongs(event.target.value);
this.setState({
result: event.target.value,
toDisplay: songs
}, () => console.log(this.state.result))
}
and finally in your JSX:
return (
<>
{toDisplay.map((song, index) => {
return <p key={index}>{song.toString()}</p>
})}
</>
);
If I understand correctly. you want to get the results from API based on selected values of input and select.
as I can see you are only passing the param from input nothing from select.
handleSearch = () => {
this.makeApiCall(this.state.searchValue);
};
makeApiCall = async searchInput => {
let api_url = `https://www.songsterr.com/a/ra/songs/byartists.json?artists=${searchInput}`;
const response = await fetch(api_url);
const songs = await response.json();
this.setState({ songs });
};
The new call will be
let api_url = `https://www.songsterr.com/a/ra/songs/byartists.json?artists=${searchInput}&tabTypes=${selectValue}`;
I do not know how what are the parameters this API accepts.
Thanks for answers! I have last question, where I have to use method to filter that rendered list. If I select "Player" I want to render only that songs, which inlcudes "Player" tab in tabTypes. I still can't get it. I can't do it by changing API link.