React setting specific input forms when mapping through array - reactjs

so I have fetched a bunch of students data from an api and mapped through it, displayed the student info in divs as per common react practices.
I want to add a text input field to be able to add tags/comments to every instance of student. Code looks something like this
<form onSubmit={(e) => handleSubmit(e, i)}>
<input
type='text'
placeholder='Add a tag'
className='tag__input'
value={tag}
onChange={(e) => setTag(e.target.value)}
/>
</form>
const [tag, setTag] is just a useState holding the string value of the comment I want to add.
this is expectedly not working as I need because the value of all my input fields is changing, whereas I need only the one specific one to change based on which student I need to leave a comment for. What would be the next steps into setting this logic up? thanks!

You should create separate component for student info and input, and put inside of that component useState.
Now you have 1 state for all fields.
UPD, something like
function StudentEntry({ data, onSubmit }) {
// one state per entry
const [tag, setTag] = React.useState('')
return (<>
<StudentInfo {...data} />
<form onSubmit={(e) => onSubmit(e, data.id)}>
<input
type='text'
placeholder='Add a tag'
className='tag__input'
value={tag}
onChange={(e) => setTag(e.target.value)}
/>
</form>
</>
}
// and use somewhere
{studentsData.map(data =>
<StudentEntry data={data} key={data.id} onSubmit={handleSubmit} />
)}

Related

React can not use conditionally rendering on a component due to variables being nested inside of my form

I want to conditionally render a component in this case if the user submits a wrong answer, the input is stored as a javascript object called data and gets converted to a string called userInput.
After looking around, I got recommended to create a conditional rendering with a state outside of the form, but the problem I ran across is that since the variables are initialized inside of my form, i can't use ternaries to conditionally render my component in so I'm a bit stuck in what to do.
<main class="gameSection">
<h1>Welcome to League of Wordle!</h1>
<form
onSubmit={handleSubmit((data) => {
let userInput = data.guess;
console.log(userInput);
const championList = Object.keys(champions);
if (userInput.valueOf().toUpperCase() !== correctChampion.valueOf().toUpperCase()) {
<Wrong text="Class" alt="wrong img" img={wrong} />
}
})
}
>
<input
{...register("guess")} class="guess_input" placeholder="Enter Champion Name Here" type="text" />
<input class="guess_input" type="submit" />
</form>
</main>
I suggest you create a state to track if any answers submitted is wrong.
const [isWrong, setIsWrong] = useState(false)
Now, after each submit, update isWrong's value based on input.
onSubmit={handleSubmit((data) => {
let userInput = data.guess;
console.log(userInput);
const championList = Object.keys(champions);
if (userInput.valueOf().toUpperCase() !== correctChampion.valueOf().toUpperCase()) {
setIsWrong(true)
}
else {setIsWrong(false) } // logic in case of correct answer
})
}
Finally, you can implement conditional rendering:
<main class="gameSection">
<h1>Welcome to League of Wordle!</h1>
<form
...
>
<input
{...register("guess")} class="guess_input" placeholder="Enter Champion Name Here" type="text" />
<input class="guess_input" type="submit" />
{isWrong && <Wrong text="Class" alt="wrong img" img={wrong} /> }
</form>
</main>

How to access state from components to the parent component

I have a form in which all input, select tags are separate components and each component but the submit button is in the form it self like-
<form>
<InputComp1 />
<InputComp2 />
<Select1 />
<Select2 />
<button type='submit' value='Register'/>
</form>
So how do I collect all state from various components and when user clicks on the submit the values get submitted.?
Is this approach while dealing with forms right? or should I manage state of all tags in the same component?
Manage the state of all inputs/selects in this component. You can pass values and handler functions to the inputs using props.
There is no "right" approach, the answer depends on the context.
You can have form as a controlled component, where you manage the state of all tags (while passing callbacks down the tree) as you suggested and as mentioned in docs.
Or, you can have it as uncontrolled component, for example:
const Input = ({ name }) => {
return <input name={name} />;
};
const Component = () => {
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.target);
const entries = data.entries();
for (let entry of entries) {
console.log(entry);
}
}}
>
<Input name="username" />
<Input name="email" />
<input type="submit" value="Submit" />
</form>
</>
);
};
See controlled vs uncontrolled components.
Yes you should manage the state in the parent component itself and pass the onchange handler and value of that field as props inside the child components to update the value of the form fields.
Solution #1 would be "manage state react way". With this you should store state you need to share between components somewhere in their common ancestor. In your case it would be component that holds Form
Solution #2 applicable only if you use real form and form controls. Handle 'submit' event from the form and get all you need to submit from form data.
Solution #3 applicable only if you use some sort of "state manager". Follow instructions and best practices of the library you use.
In some cases you can mix and match that solutions. For example I still recommend to handle 'submit' event on form regardless of solution.
there is a concept of state lifting in react:
create a controlled form here and for every child, component pass a function to get the data from child components to parent one. by doing this you can submit all the values once.
here is the example
import React, {useState} from 'react';
const ChildInput = ({onChange, id}) => {
return(
<input
key={id}
type="text"
placeholder="enter name"
onChange={onChange}
/>
)
}
const Parent = () => {
const [name, setName] = useState('');
const onSubmit =(e)=>{
e.preventDefault();
// append your all data here just like child component
data = {name}
}
return(
<form onSubmit={onSubbmit}>
<ChildInput onChange={()=>setName(e.target.value)} id="name" />
<button type="submit" value="submit"/>
</form>
)}
for more information check this one: https://reactjs.org/docs/glossary.html#controlled-vs-uncontrolled-components

Alert user before leaving page after inputting some data (Specially in ReactJS)

I am creating a page in which the user needs to enter the details to add the product to the product list. If the user accidentally moves out of that page after inputting some data in the form, confirmation should be mandatory from the user before moving out. But I am struggling with this requirement. I am using the react-hook-form for storing the data in the JSON server.
I am using a state leave, if it true then alert the user before moving out else nothing.
const [leave, setLeave] = useState(false);
Now, I don't know where and how to use this state for displaying the alert box before leaving.
Here my form which will render.
render (
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row>
<Form.Group as={Col} className="mr-5">
<Form.Label column>Product Name</Form.Label>
<Form.Control
name="productName"
placeholder="Enter product Name"
required
ref={register}
/>
</Form.Group>
</Form.Row>
<Button variant="success" type="submit" className="text-white mt-5">
Add New Product
</Button>
</Form>
<Prompt when={Leave} message="Are you sure you want to leave ?" /> {/*Here I have promted*/}
);
For simplicity, I have given only one input field. Now function definition of onSubmit is given below.
const onSubmit = (formData) => {
setLeave(true); //here I am setting this true
axios.post("http://localhost:4000/products", formData);
navigation.push({
pathname: "/productList",
state: { added: "pass" },
});
};
This will work when I submitting the form but I want to prompt when the user clicks on the back button or any navigation link.
In functional component of reactjs I used below thing. this worked for me. In my case when leaving current page I want to remove some store data. In that case i used below thing.
useEffect(() => {
return () => {
// you can add your functionality here
};
}, []);
when must be true when you want to interrupt navigation.
I would rename your variable to something like isDirty, initialised as false. I've found since I started naming my boolean flags like this my mind has to do a little less work to understand what it might be being used for.
Flip it to true when the first change is made to the form values. Could be simply a useEffect with formData as a dependency? Check its not already flipped to prevent unnecessary renders.
Flip it back to false when you get a 200 from your submission to allow the subsequent navigation.
I don't think React will re-render before the Prompt gets fired in your current onSubmit, so you may need to figure out a way to navigate after verifying the submission.
Another flag like didSubmit would allow you a code path to render a <Redirect> instead of calling navigation.push(), if that suits.
But otherwise, using your current setup, can can allow navigation when you land on a fresh form (your user hasn't committed any effort yet), navigation will be interrupted if the form is 'dirty', and navigation will happen automatically on the next render after setting didSubmit.
const [isDirty, setIsDirty] = React.useState(false)
const [didSubmit, setDidSubmit] = React.useState(false)
const onSubmit = async (formData) => {
try {
await axios.post('url', formData)
setIsDirty(false)
setDidSubmit(true)
} catch (error) {
// handle your errors
}
}
React.useEffect(() => {
if(!isDirty) setIsDirty(true)
}, [formData])
React.useEffect(() => {
if(didSubmit) navigation.push({ ... })
}, [didSubmit]
return (
// the form...
<Prompt when={isDirty} {...} />
)
I have updated the state isDirty or leave to true when there is a change in the input field of the form as shown below
render (
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row>
<Form.Group as={Col} className="mr-5">
<Form.Label column>Product Name</Form.Label>
<Form.Control
name="productName"
placeholder="Enter product Name"
required
onChange={() => setIsDirty(true)} {/* I have updated here*/}
ref={register}
/>
</Form.Group>
</Form.Row>
<Button variant="success" type="submit" className="text-white mt-5">
Add New Product
</Button>
</Form>
<Prompt when={isDirty} message="Are you sure you want to leave ?" />
);
Now inside the onSubmit function, I have updated the isDirty state to false before navigating to the destination.
const onSubmit = (formData) => {
axios.post("http://localhost:4000/products", formData);
setIsDirty(false); //here I am setting this true
navigation.push({
pathname: "/productList",
state: { added: "pass" },
});
};
I'd like to share an important point here that whenever there is a change in input at any point then only isDirty state is true and during submission of the form (all input fields are filled) the state change to false so that it can navigate to another URL.

In this basic react application, why does the custom component `TextInput` allow me to type stuff in even though the regular `input` field doesn't?

I have a basic react form that I am testing out currently. It's still incomplete, but I have discovered unexpected behaviour.
The regular input field doesn't allow me to type in anything, as I am not updating the state yet. However, my custom TextInput component does allow me to type stuff in... Surprising, as I said before, I am not using setValues to update the state yet.
import React, { useState } from 'react';
const App = () => {
const [values, setValues] = useState({
firstName: '',
lastName: ''
});
return (
<div>
<form>
{/* [EXPECTED] this doesn't allow anything to be typed in at the front-end... which is expected...
... as I am not using `setValues` to update the state yet */}
<input
type="text"
id="first-name"
name="firstName"
value={values.firstName}
/>
{/* [NOT EXPECTED] this does allow stuff to be typed in at the front-end... which is strange...
... as I am not using `setValues` to update the state yet */}
<TextInput
id="last-name"
name="lastName"
value={values.lastName}
/>
<button type="submit">
Register
</button>
</form>
</div>
);
};
const TextInput = props => {
return (
<input
type="text"
id={props.id}
name={props.name}
/>
/* <span id={props.id + '-error'}>{props.title}</span> */
);
};
export default App;
Can anybody help me to explain why the difference?
Your first input is controlled - it has a value prop which is used to determine what the value of the element should be when rendered:
<input
type="text"
id="first-name"
name="firstName"
value={values.firstName}
/>
No matter what you type into it, since it's "controlled", the value that exists in it will always be what's currently in state as values.firstName.
In contrast, your second input is uncontrolled. It has no value prop:
<input
type="text"
id={props.id}
name={props.name}
/>
So, since you're not giving React any directives on what its value should be while being rendered, you can type whatever you want into it, and it won't be in conflict with React state.

How to use hooks in multiple rendered inputs?

I'm creating the form to my app. Problem i'm facing is whenever i click add and generate another row of inputs i got an "Rendered more hooks than during the previous render" error.
I have a "benefit" input with checkbox. When user checks it i want to render another 2 inputs (in the same row) with KPI. Hook works perfectly fine if there is only one row. When i add another row the error occurs.
const renderBenefitFields = (benefit, index, fields) => {
const [hidden, setHidden] = useState(false);
return (
<div key={index}>
<InputGroup>
<Input
name={`${benefit}.projectBenefitData`}
type="text"
component={renderField}
label="Benefit of the project"
/>
<InputGroupAddon addonType="append">
<InputGroupText>
<Input
addon
type="checkbox"
aria-label="Mesurable benefit?"
onChange={() => setHidden(!hidden)}
/>
</InputGroupText>
<InputGroupText>Benefit mesurable </InputGroupText>
</InputGroupAddon>
<Trash
className="align-middle"
size={25}
onClick={() => fields.remove(index)}
/>
Show KPI inputs? {hidden === false ? "false" : "true"}
</InputGroup>
</div>
);
};
const renderBenefits = ({ fields }) => (
<div>
{fields.map(renderBenefitFields)}
<PlusCircle
className="align-baseline"
size={24}
onClick={() => fields.push({})}
/>
</div>
);
Perfect solution would be: when user checks the box react will show another two inputs.
As Ross Hunter mentioned,
1. You should capitalize your component, i.e. RenderBenefitFields
2. No mutation on state/props, in onClick={() => fields.push({})}
In addition, you should call your RenderBenefitFields in renderBenefits as following:
{fields.map((props, index) => <RenderBenefitFields key={index} {...props} />)}
P.S. You can use a unique key instead of index, depending on your situation.
The name of user-defined components needs to be capitalized.
The onClick callback in PlusCircle looks like it's directly mutating state. That's a big problem for React, and almost certainly the root of your issue. I can explain further if you need sometime I'm not on mobile 😊

Resources