I am trying to build a small recipe app. One feature of the app is saving user recipes, including ingredient/qty/measurement.
I need to wrap up the inputted ingredients into an array of objects to send to server but right now my setIngredientList only works for the first two ingredients a user inputs.
When a user tries to add a third ingredient it just mirrors the data from the second input (and fills the third input's fields with the same data as the second input). It is like the second inputs and any subsequent input mirror each other.
I believe the problem is init is not clearing properly (it seems to clear after the first ingredient is added allowing the second one to be added, but then it does not clear for the next ingredients.
I'm not sure the proper way to make sure this happens so multiple ingredients can be added.
Here is my code:
const init = {
ingredient_name: '',
quantity: '',
measure: '',
}
export default function Recipe() {
const [name, setName] = useState('')
const [ingredientList, setIngredientList] = useState([
{
ingredient_name: '',
quantity: '',
measure: '',
},
])
const handleChange = (e, i) => {
const { name, value } = e.target
setIngredientList((prevState) => {
const newIngredientList = [...prevState]
newIngredientList[i][name] = value
return [...newIngredientList]
})
}
return (
<div>
<div className="recipe-form-container">
<form className="recipe-form">
[...]
</div>
{ingredientList.map((list, i) => (
<div key={i} className="ingredient-triad">
<input
className="ingredient"
name="ingredient_name"
type="text"
value={list.ingredient_name}
onChange={(e) => handleChange(e, i)}
></input>
<input
className="quantity"
name="quantity"
type="text"
value={list.quantity}
onChange={(e) => handleChange(e, i)}
></input>
<select
className="dropdown"
name="measure"
id="measure"
value={list.measure}
onChange={(e) => handleChange(e, i)}
>
<option value="" disabled>
--none--
</option>
<option value="cup">cup</option>
</select>
<button
onClick={(e) => {
console.log(init)
setIngredientList((prev) => [...prev, init])
e.preventDefault()
}}
>
Add
</button>
</div>
))}
</form>
</div>
</div>
)
}
Classical object reference issue.
Use the below code it will work fine.
Previously, you pass the same init object for multiple rows,
which is why you got that result. Instead of doing that, when the user clicks 'add' button then add a new Object to your state which is derived from your init object. Here I just clone the init object and then set the state.
<button
onClick={(e) => {
console.log(init);
setIngredientList((prev) => [...prev, { ...init }]);
e.preventDefault();
}}
>
Sounds like you wanted some advice and can prob find the solution yourself, but what i would do to this code:
Move e.preventDefault() above setIngredientList.
Create an Ingredient class with the state / logic within and pass state to Ingredient, the list should not not be concerned with an Ingredient, it just lists.
Then what is onClick doing - init - its the initial value isn't it, so I think that with your onClick you are overwriting prev with the value that is outside the component - should it be outside or is it some state - it holds a value - so it should be an initial state of an ingredient? Checkout react tools profiler so see exactly what is happening on your events.
So what is prev we are over writing it, but what is it? Isn't the first arg on onClick the click event? So I think you are overwriting the click event with the init state - which is outside the component - a few things to tweak for you (if you like), hope it helps.
Related
Hey I have an issue where I am collecting input from several inputs.. I tried to write a helper function for just updating the state object for each input, but it seems like the input is only gathering the first letter from my input and nothing else.. and it seems like it stores it once and then I can change it.. any idea what I'm missing her?
`
export const Form = ({addStudent}) => {
const [newStudent, setNewStudent] = useState({school:"university"})
const updateValue = e => {
const { name, value } = e.target;
setNewStudent({[name]: value, ...newStudent});
}
return (
<section>
<p>First Name</p>
<input type="text" name="firstName"
onChange={updateValue}
/>
<p>Last Name</p>
<input type="text" name="lastName"
onChange={updateValue}
/>
<label>Choose a school:</label>
<select name="school"
onChange={updateValue}
>
<option value="university">university</option>
<option value="highSchool">High School</option>
</select>
<button onClick={() => addStudent(newStudent)}>
Add new Student
</button>
</section>
)
}
`
I tried to make the updateValue function dynamic with the values like this and now it seems to not work anymore...
You are overwriting the current existing value with your spread.
setNewStudent({[name]: value, ...newStudent});
// ^^^^^^^^^^ overwriting existing value
After the first keystroke (onChange) the field gets created and the value of that field is just one letter (because if was empty before). And it stays in this state because the new value is consistently overwritten by the previous (first) value.
Note: You should be also using a callback while setting the new state, just to make sure it's up-to-date when updating.
setNewStudent((prev) => ({ ...prev, [name]: value }));
you are setting the values in the wrong place.
first copy the state, and the add it the new field.
setNewStudent({...newStudent, [name]: value});
I'm trying to make a CRUD person function where each person has an array of skills.
I want a function where you're able to add/edit/remove skills on a given person.
Each array consist of a skill element as a string and a star element as an integer. I've made some dynamic inputfields with an add and a remove function for more/less inputfields in a bootstrap modal.
The data is fetched from Firebase with a useEffect and set as setData in EditPerson.jsx. No problem here.
The issue consist of 3 components atm: EditPerson -> ModalEditSkills -> EditSkills. (Please let me know if this is a bad structure).
I'm now able to set the useState of newData in SkillEdit.jsx with the correct data. This makes sure that on EditPerson I'll be able to view the correct data input from given in the EditSkills. Also if I console.log the data in EditSkills I can see that it works like a charm. But when I close the bootstrap modal and open it again the useState in index 0 have been reset to init useState (0).
I can't add images in the text here yet, so here's some links for the images if needed.
The image explains that the console.log tells me that the useState is set correct, but it stills reset the state of index 0 everytime I re-open the modal.
Hope that makes sense otherwise let me know.
ReactStars-choosen
Console.log
EditPerson.jsx
const EditPerson = () => {
const [data, setData] = useState({});
const [skills, setSkills] = useState([]);
const { id } = useParams();
useEffect(() => {
if (id) {
const fetchData = async () => {
const docRef = doc(db, "person", id);
try {
const docSnap = await getDoc(docRef);
setData(docSnap.data());
} catch (error) {
console.log(error);
}
};
fetchData().catch(console.error);
} else {
setData("");
}
}, [id]);
useEffect(() => {
if (data) {
setSkills(data.skills);
}
}, [data]);
const handleSkills = (skill) => {
setSkills(skill);
};
return (
<div>
<ModalEditSkills
handleSkills={handleSkills}
data={skills}
/>
</div>
);
}
ModalEditSkills.jsx
const ModalEditSkills = ({ data, handleSkills }) => {
const [show, setShow] = useState(false);
const [newData, setNewData] = useState({});
useEffect(() => {
if (data) {
setNewData(data);
}
}, [data]);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleSubmitSkills = (e) => {
e.preventDefault();
handleSkills(newData);
setShow(false);
};
return (
<>
<div className="content_header">
<div className="content_header_top">
<div className="header_left">Skills</div>
<div className="header_right">
<Button className="round-btn" onClick={handleShow}>
<i className="fa-solid fa-pencil t-14"></i>
</Button>
</div>
</div>
</div>
<Modal show={show} onHide={handleClose} size="">
<Modal.Header closeButton>
<Modal.Title>Edit Person</Modal.Title>
</Modal.Header>
<Modal.Body>
<SkillEdit data={data} setNewData={setNewData} />
</Modal.Body>
<Modal.Footer>
<Form>
<Button className="btn-skill-complete" onClick={handleSubmitSkills}>
Save
</Button>
</Form>
</Modal.Footer>
</Modal>
</>
);
};
SkillEdit.jsx
const SkillEdit = ({ data, setNewData }) => {
const [inputField, setInputField] = useState([{ skill: "", stars: 0 }]);
const handleAddFields = () => {
setInputField([...inputField, { skill: "", stars: 0 }]);
};
const handleRemoveFields = (index) => {
const values = [...inputField];
values.splice(index, 1);
setInputField(values);
setNewData(values);
};
const handleChangeInput = (index, name, value) => {
const values = [...inputField];
values[index][name] = value;
setInputField(values);
setNewData(values);
};
useEffect(() => {
if (data) {
const setSkills = () => {
setInputField(data);
};
setSkills();
}
}, [data]);
return (
<Form>
<div>
{inputField?.map((inputField, index) => (
<div key={index}>
<Row>
<Col xs={5} md={5}>
<Form.Group as={Col}>
<Form.Control
className="mb-3"
type="text"
id="skill"
name="skill"
value={inputField?.skill}
onChange={(event) =>
handleChangeInput(index, "skill", event.target.value)
}
/>
</Form.Group>
</Col>
<Col xs={4} md={4}>
<div>
<Form.Group>
<ReactStars
type="number"
name="stars"
count={5}
size={24}
id="stars"
onChange={(newValue) =>
handleChangeInput(index, "stars", newValue)
}
emptyIcon={<i className="fa-solid fa-star"></i>}
filledIcon={<i className="fa-solid fa-star"></i>}
value={inputField.stars}
/>
</Form.Group>
</div>
</Col>
<Col xs={3} md={3}>
<div>
<button
type="button"
onClick={() => handleAddFields()}
>
<i className="fa-solid fa-plus"></i>
</button>
<button
type="button"
onClick={() => handleRemoveFields(index)}
>
<i className="fa-solid fa-minus"></i>
</button>
</div>
</Col>
</Row>
</div>
))}
</div>
</Form>
);
};
This took some time for me to work out. I had trouble reproducing and still do, but I noticed a lot of odd behaviour around the stars. In the end, I've figured out its probably this bug in the react-stars package.
Unfortunately, the value prop does not actually control the value after the initial render. So It's like an uncontrolled component. The library therefore, is poor. It hasn't been committed to for 4 years. Usually, if a component is uncontrolled, the developer calls the prop initialValue or defaultValue instead of value, which usually implies the component is controlled. Here, the author has made a mistake. Regardless, in your case, you need controlled mode.
It's possible theres another bug interacting. But I'd start by replacing react-stars as not being able to have controlled mode is extremely poor and it makes it very hard to see the wood through the trees. There is a "solution" in the github thread but its a massive hack -- its using the special key property to remount it every time the value changes.
I went looking for an alternative and much to my surprise a lot of the other libraries are also uncontrolled -- which really sucks. What you could do instead of the hack in the github issue, is make it so the dialog is unmounted when open is false. This would mean each time the dialog opens it resets the value back to that which is held in the parent state. See my bottom code for that solution.
There's good options though here and here but they are part of larger design systems, and its probably overkill to bring in a whole other design system when you have committed to bootstrap. Depending on how early you are in your project though, I'd seriously consider switching to something like MUI. Personal opinion territory, but bootstrap is pretty outdated and the React wrapper and associated community, plus diversity of components, is much smaller. It shows that react-bootstrap is a wrapper on top of old school global CSS files as opposed to material-ui which was built from the ground up in React and has a React based CSS-in-JS solution. When people first start learning react, they often slip into bootstrap because its what they know from non-react dev -- but using a framework like React moves the needle and trade offs.
It's not your problem here, but I feel the need to say it :D. At the same time I'd say don't always take a random internet strangers recommendation and refactor for nothing -- you should research it.
A few other important notes:
If id is not set, you set data to an empty string. If that case ever happened for real, your code would error since accessing data.skills would result in undefined. I would argue that you shouldn't even need to handle this case, since your router setup should be such that there is no mapping between this component and a route without an id. Make sure on your router config the id param is non-optional.
You are copying data.skills into a new state item called skills. It's not a bug per-se but it is a code smell as its not actually necessary. Why not edit the data in the data state directly? Copying state that is fundamentally the same is usually an anti-pattern as you can fall into traps where you are trying to keep 2 bits of state in sync that are supposed to be the same. I think its ok where you do it further down as I believe you are copying it because you want that state not to be committed further up until the user clicks save -- which means its actually fundamentally different state at given times. But that doesnt apply to the top level data vs skills one. Actually, I think the inputField state is also not needed since your "staged" state is held by the parent ModalEditSkills in newData. You could instead get rid of this inputField state and have handleChangeInput, handleAddFields and handleRemoveFields call up into ModalEditSkills to patch newData directly. Then pass that down. This will greatly reduce the surface area for bugs and remove unnecessary effects in SkillEdit.
You are often passing props called set[Something]. Generally, in react, you want to keep to the naming convention of user triggered events beginning with on. The trouble with the former, is it implies that the user action does a certain thing, which makes the component look like its less reusable in other contexts (even though its the same behaviour really, its just incorrect naming).
When setting a state item thats derived partly from its previous value, you should use the callback pattern of setState (which is passed the current value) instead of referencing the state value returned from useState. This avoids bugs to do with accidentally getting a stale value when you update state in a loop or something. React doesn't actually update the value of the state immediately, it gets flushed at the end of the call stack. Using the previous value callback gets around this. Not a problem that will show up in your code, just good practice that you should get in the habit of.
There's a bug where you need to reset the form data back to the data stored in the top component when it opens/closes because currently if you make a change and close dialogue, without clicking save, its still there next time you open it. Its not actually saved to the parent state, its just hanging around in the background (hidden). Its weird UX so I've fixed by adding show to the effect that resets the newData state. If you really wanted it how it was though you can just remove that again from the deps array.
There was also a bug in the way you were patching the state when editing an existing skill. Even though you cloned the skills array by spreading a new one, this only does a shallow clone. The object you spread into the array are the same (as in literally, the same object reference) as the original ones. This meant you were mutating the data prop which is not allowed in react. I've changed to Object.assign to make sure its a proper edited clone. The way this works if everything gets merged from right to left. So by the first param being {}, you start with a brand new object and then you load int the old + new data into it. Libraries like Immer make this easier, you might want to look into it.
I haven't fixed this one as it's kind of up to you but if you were on slow network, there would be a period where you could confuse the user since the data hasn't come back yet but they can open the dialogue and see no skills. You might want to handle that case by showing a loading display instead of the rest of the app whilst its in flight. You could make the default state of the top level data state null and then only populate it when the request comes back like you do now. Then in the top level render you'd check for null and return some loading display instead of ModalEditSkills. Similar stuff would happen if the network errored. You might also want to have some new state that says if an error happened (instead of just logging), and check that as well and display the error page.
Heres the code with my proposed changes (minus the library change, you'd still need to do that if you cared enough).
And heres a code sandbox with it working: https://codesandbox.io/s/wispy-meadow-3ru2nq?file=/src/App.js (I replaced the network call with static data for testing, and I dont have your icons).
I hope this feedback helps you on your react journey!
const EditPerson = () => {
const [data, setData] = useState({skills: []});
const { id } = useParams();
useEffect(() => {
const fetchData = async () => {
const docRef = doc(db, "person", id);
try {
const docSnap = await getDoc(docRef);
setData(docSnap.data());
} catch (error) {
console.log(error);
}
};
fetchData().catch(console.error);
}, []);
const handleSkillsChanged = (skills) => {
setData(data => ({...data, skills}));
}
return (
<div>
<ModalEditSkills
onSkillsChanged={handleSkillsChanged}
data={data.skills}
/>
</div>
);
}
const ModalEditSkills = ({ data, onSkillsChanged}) => {
const [show, setShow] = useState(false);
const [newData, setNewData] = useState([]);
useEffect(() => {
setNewData(data);
}, [data, show]);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleSkillChange = (index, name, value) => {
setNewData(prevValues => {
const newValues = [...prevValues]
newValues[index] = Object.assign({}, newValues[index], { [name]: value });
return newValues
});
}
const handleSkillAdded = () => {
setNewData(prevValues => [...prevValues, { skill: "", stars: 0 }]);
}
const handleSkillRemoved = (index) => {
setNewData(prevValues => {
const newValues = [...prevValues];
newValues.splice(index, 1);
return newValues
});
}
const handleSubmitSkills = (e) => {
e.preventDefault();
onSkillsChanged(newData);
setShow(false);
};
return (
<>
<div className="content_header">
<div className="content_header_top">
<div className="header_left">Skills</div>
<div className="header_right">
<Button className="round-btn" onClick={handleShow}>
<i className="fa-solid fa-pencil t-14"></i>
</Button>
</div>
</div>
</div>
{show && (
<Modal show={show} onHide={handleClose} size="">
<Modal.Header closeButton>
<Modal.Title>Edit Person</Modal.Title>
</Modal.Header>
<Modal.Body>
<SkillEdit
data={newData}
onSkillChanged={handleSkillChange}
onSkillAdded={handleSkillAdded}
onSkillRemoved={handleSkillRemoved}
/>
</Modal.Body>
<Modal.Footer>
<Form>
<Button
className="btn-skill-complete"
onClick={handleSubmitSkills}
>
Save
</Button>
</Form>
</Modal.Footer>
</Modal>
)}
</>
);
};
const SkillEdit = ({ data, onSkillChanged, onSkillRemoved, onSkillAdded}) => {
return (
<Form>
<div>
{data?.map((inputField, index) => (
<div key={index}>
<Row>
<Col xs={5} md={5}>
<Form.Group as={Col}>
<Form.Control
className="mb-3"
type="text"
id="skill"
name="skill"
value={inputField?.skill}
onChange={(event) =>
onSkillChanged(index, "skill", event.target.value)
}
/>
</Form.Group>
</Col>
<Col xs={4} md={4}>
<div>
<Form.Group>
<ReactStars
type="number"
name="stars"
count={5}
size={24}
id="stars"
onChange={(newValue) =>
onSkillChanged(index, "stars", newValue)
}}
emptyIcon={<i className="fa-solid fa-star"></i>}
filledIcon={<i className="fa-solid fa-star"></i>}
value={inputField.stars}
/>
</Form.Group>
</div>
</Col>
<Col xs={3} md={3}>
<div>
<button
type="button"
onClick={onSkillAdded}
>
<i className="fa-solid fa-plus"></i>
</button>
<button
type="button"
onClick={() => onSkillRemoved(index)}
>
<i className="fa-solid fa-minus"></i>
</button>
</div>
</Col>
</Row>
</div>
))}
</div>
</Form>
);
};
My colleague has been working on a small project and needs my help with it, I have barely any experience in react and have been trying to use my knowledge from other languages to handle this problem, however I have come to my limits after searching the web for hours and not finding an efficient way to handle this issue without having to rewrite most of the code.
So the problem is the following: We have customer orders with different variables, (i.e. Loading Country)
These are displayed in a table like this (we map over the data and print this tr/td for every order):
<td><input type="text" onChange={(e) => setLoadingCountry(e.target.value)} className={"lcountry"+data.rowid} value={data.array_options.options_lcountry}></input></td>
<td><input type="text" onChange={(e) => setLoadingZip(e.target.value)} className={"lpostcode"+data.rowid} value={data.array_options.options_lpostcode}></input></td>
setLoadingCountry is used for a useState.
With an update button for each row at the end:
<td><button className={data.id} onClick={(e) => handleCoData(e.target.className)}>Update</button></td>
handleCoData is a function I made, since we get the initial data from a json and I put the json into a useState array, this function handles that array after button click like the following:
const handleCoData = (coID) => {
setCoData(
coData.map((co) =>
co.id == coID
? {...co, array_options: {
options_lcountry: loadingCountry,
options_lpostcode: loadingZip
}}
: {...co}
)
);
console.log(coID);
console.log(coData);
};
Now: One Problem, which is the smaller one, is that we can only update one row at a time, since changing 2 countries from 2 rows without updating inbetween will overwrite the value of the first loading country in the useState.
The bigger problem is that, when you only want to change the loading country, and then click update, the Zip is either empty, because it was never set by the use state, or it has the value from another row which was changed previously. Is there any way to get the value from that specific row for that specific field?
If this question needs changes or additional information please tell me so, as I said I do not have much experience in React, and it is not my project either so I have no idea what might be needed to fully understand the issue.
Thank you.
Edit:
I was not able to make a codepen, since it would show a syntax error that I can't figure out quickly ( there isn't one in the actual code), but here is the code which includes all the things needed.
//import Data from "../../data.json";
const CombinedComponents = (apiKey) => {
const [loadingCountry, setLoadingCountry] = useState([]);
const [loadingZip, setLoadingZip] = useState([]);
const [coData, setCoData] = useState(Data);
console.log(coData);
const handleCoData = (coID) => {
setCoData(
coData.map((co) =>
co.id == coID
? {
...co,
array_options: {
options_lcountry: loadingCountry,
options_lpostcode: loadingZip
}
}
: { ...co }
)
);
console.log(coID);
console.log(coData);
};
return (
<div>
<table>
{visible && <TableHeadView1 />}
<tbody>
{coData.map((data) => (
<tr>
<td>{data.ref}</td>
<td>
<input
type="text"
onChange={(e) => setLoadingCountry(e.target.value)}
className={"lcountry" + data.rowid}
value={data.array_options.options_lcountry}
></input>
</td>
<td>
<input
type="text"
onChange={(e) => setLoadingZip(e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}
></input>
</td>
<td>
<button
className={data.id}
onClick={(e) => handleCoData(e.target.className)}
>
Update
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
This is what the array coData looks like (ofcourse with many more entries):
[{
"id": "11480",
"array_options": {
"options_lcountry": "1",
"options_lpostcode": "80190",
}
}]
Here your problem resides in using a single state value for handling multiple input values. If I was to do it, I'd only use the main array all the time, giving me multiple fields to change from. Currently, your update button only handles the React state, which is purely front end. You could use it to send the update to your backend instead.
My take on this one would be using these functions for inputs 'onChange' props:
const handleCountryUpdate = (id, value) => {
coData.find(it => it.id === id).array_options.options_lcountry = value;
setCoData([...coData]); // Using an array copy to override the state
};
const handlePostCodeUpdate = (id, value) => {
coData.find(it => it.id === id).array_options.options_lpostcode = value;
setCoData([...coData]);
};
...
<input type="text"
onChange={(e) => handlePostCodeUpdate(data.id, e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}>
</input>
Another way around would be to use a child component state to do the job
...
{coData.map((data) => <TableRow key={data.id}
data={data}
handleCoData={handleCoData}/>)}
...
const TableRow = props => {
const {data, handleCoData} = props;
const [loadingCountry, setLoadingCountry] = useState(data.array_options.options_lcountry);
const [loadingZip, setLoadingZip] = useState(data.array_options.options_lpostcode);
return (
<tr>
<td>{data.ref}</td>
<td>
<input
type="text"
onChange={(e) => setLoadingCountry(e.target.value)}
className={"lcountry" + data.rowid}
value={data.array_options.options_lcountry}
></input>
</td>
<td>
<input
type="text"
onChange={(e) => setLoadingZip(e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}
></input>
</td>
<td>
<button
className={data.id}
onClick={(e) => handleCoData(e.target.className)}
>
Update
</button>
</td>
</tr>
);
}
I am trying to default check the first radio button which the following code helps me to do. When loaded the page the first radio button is checked but the problem i am facing is that it doesn't allow me to check the other buttons that also are present in the array.
constructor(props: any) {
super(props);
this.state = {
selectedSort: '',
sort: ['Apple', 'Orange '],
}
}
this.state.sort.map((sortType:string, index:number) => {
return <span key={`${sortType}${index}` onClick={() => this.setSort(sortType)} >
<input type="radio" id={sortType}
value={this.state.selectedSort}
name={sortType} defaultChecked={index===0}
}/>
<span>{sortType}</span>
})
private setSort = (selectedSort: string) => {
this.setState({
selectedSort: selectedSort
});
}
Issue
The defaultChecked value is a boolean but your condition sortType === 0 will always evaluate false since your sortType is only ever one of your sort state values, i.e. ["Apple", "Orange "].
Solution
If you want the first radio button to be default checked then you should compare against the mapped index.
defaultChecked={index === 0}
Other Issues & Suggestions
Radio button group inputs should all have the same name attribute.
Use a semantic label to wrap your inputs so they are more accessible.
Use the radio input's onChange event callback versus an onClick, to update state.
The sortType values alone should be sufficient for a React key.
Code:
{this.state.sort.map((sortType, index) => (
<label key={sortType}>
<input
type="radio"
id={sortType}
value={selectedSort}
name="sortType"
defaultChecked={index === 0}
onChange={(e) => this.setState({ selectedSort: e.target.id })}
/>
{sortType}
</label>
))}
Additionally, I suggest converting this to a fully controlled input since you have already all the parts for it. Remove the value attribute and use the checked prop. Set what you want the initial checked state to be. This will allow you have have already valid checked state.
state = {
selectedSort: 'Apple',
sort: ['Apple', 'Orange '],
}
{this.state.sort.map((sortType, index) => (
<label key={sortType}>
<input
type="radio"
id={sortType}
name="sortType"
checked={sortType === this.state.selectedSort}
onChange={(e) => this.setState({ selectedSort: e.target.id })}
/>
{sortType}
</label>
))}
Demo
I used to use ref for forms but now I always state for forms, I'm facing an issue where I have to clear a field after user submitted something.
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.state.todoText
if(todoText.length > 0){
this.refs.todoTextElem = "" // wont work
this.props.onAddTodo(todoText)
} else {
this.refs.todoTextElem.focus() //worked
}
}
render() {
return(
<div>
<form onSubmit={this.handleSumbit}>
<input ref="todoTextElem" type="text" onChange={e => this.setState({todoText: e.target.value})} name="todoText" placeholder="What do you need to do?" />
<button className="button expanded">Add Todo</button>
</form>
</div>
)
}
Clearing the ref simply don't work because it's a controlled input. I don't want to do something stupid like
passing a flag from parent component telling the form is submitted then use setState to clear the input. Or make onAddTodo to have a callback so that I can do
this.props.onAddTodo(todoText).then(()=>this.state({todoText:""}))
The way you are using the input element is uncontrolled, because you are not using the value property, means not controlling it's value. Simply storing the value in state variable.
You don't need to store the input field value in state variable if you are using ref, ref will have the reference of DOM element, so you need to use this.refName.value to access the value of that element.
Steps:
1- Write the input element like this:
<input
ref= {el => this.todoTextElem = el}
type="text"
placeholder="What do you need to do?" />
To get it's value: this.todoTextElem.value
2- To clear the uncontrolled input field, clear it's value using ref:
this.todoTextElem.value = '';
Write it like this:
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.todoTextElem.value;
if(todoText.length > 0){
this.todoTextElem.value = ''; //here
this.props.onAddTodo(todoText)
} else {
this.todoTextElem.focus()
}
}
Another change is about the string refs, As per DOC:
If you worked with React before, you might be familiar with an older
API where the ref attribute is a string, like "textInput", and the DOM
node is accessed as this.refs.textInput. We advise against it because
string refs have some issues, are considered legacy, and are likely to
be removed in one of the future releases. If you're currently using
this.refs.textInput to access refs, we recommend the callback pattern
instead.
Try and use functional refs instead. Note that the ref is to a DOM element, meaning you still need to address its properties (.value) to modify them as opposed to trying to overwriting the element directly.
The following should work:
handleSumbit = (e) => {
e.preventDefault()
const todoText = this.state.todoText
if(todoText.length > 0){
this.todoTextElem.value = ""
this.props.onAddTodo(todoText)
} else {
this.todoTextElem.focus()
}
}
render() {
return(
<div>
<form onSubmit={this.handleSumbit}>
<input ref={input => this.todoTextElem = input} type="text" onChange={e => this.setState({todoText: e.target.value})} name="todoText" placeholder="What do you need to do?" />
<button className="button expanded">Add Todo</button>
</form>
</div>
)
}