I am building a small application for adopting pets that has a form to add and delete pets from the adoption list.
API
[
{category: 'dog', pets: [{breed: 'chihuahua', name: 'bar' }, ....]},
{category: 'cat', pets: [{breed: 'tobby', name: 'foo'}, ....]},
....
]
Function that creates the form
export default function petFieldset({
petGroups, addPet, deletePet, deleteCategory,
nameInputChange, breedInputChange, categoryInputChange
}) {
const categoryField = Object.keys(petGroups).map((keys) => (
<Fieldset
title={'Pet Category'}
button={<Delete onClick={deleteCategory.bind(this, { keys })} />}
key={keys}
>
<CategoryComponent
petGroup={petGroups[keys]}
deletePet={deletePet}
categoryKey={keys}
nameInputChange={nameInputChange}
breedInputChange={breedInputChange}
categoryInputChange={categoryInputChange}
/>
<br />
<AddPet onClick={addPet.bind(this, { keys })} />
</Fieldset>
));
return (<div>{ categoryField }</div>);
}
My reducer addPet looks like this
[ADD_PET]: (state, { payload }) => {
state[payload.keys].pets.push({ breed: '', name '' });
return { ...state };
},
The AddPet component is a button that dispatches the action ADD_PET that creates a new form field with the inputs name and breed at the bottom of the form. This is done by pushing an empty { breed: '', name: '' } to the pets array for the selected category.
The problem is when I try to add a pet more than once.
When a pet is added the first time, it creates an empty form with the breed and name with empty fields. Now if new pets are added, the form will contain the same name and breed fields of the field added previously.
example of the unwanted behaviour:
1) form gets loaded with the data from the API as place holder. [expected]
2) user clicks on AddPet button which creates a new empty form with name and breed without placeholders. [expected]
3) user writes the pet name and breed on the newly created form and clicks the AddPet component. [expected]
4) a new form with same name and breed as in step 3). [unexpected]
Something is happening with the state[payload.keys].faqs.push({ breed: '', name '' }) from the reducer.
push is not an immutable operation. you are changing the array itself. So it has the same state and previous state.
You have to create a copy of previous array, and push into this new copy the new item.
For example:
[ADD_PET]: (state, { payload }) => {
let pets = [...state[payload.keys].pets];
pets.push({ breed: '', name '' });
return { ...state, payload.keys: pets };
},
Related
I am working on creating form inputs, where the IDs of the inputs match an item id from the backend data. I need to keep track of the dynamic IDs, so I want the name of the fields to match also.
How can I create a form with an object of dynamic field names? It seems that RHF creates an array instead, so if my itemID is 7841 the array is 7841 in length?
interface DynamicCharges {
...other fields with static names
additionalCharges: {
[id: number]: { // where the `id` comes from the backend so i need the input to follow this
qty: number;
amt: string;
};
}
}
and then implementing the UI:
const chargesWithDynamicIds = (additionalCharges || []).reduce(
(acc, additionalCharge) => {
const keyName = `_${additionalCharge.id}`;
return {
...acc,
additionalCharges: {
...acc.additionalCharges,
[keyName]: {
qty: 0,
amt: '$0.00',
},
},
};
},
otherStaticChargesToInitObj
);
const methods = useForm<ReservationChargesShape>({
defaultValues: chargesWithDynamicIds,
});
<Controller
name={`additionalCharges.${additionalCharge?.id}.qty`}
control={control}
render={({ value, onChange }) => (
<Input
value={value}
onChange={e => {
onChange(e.target.value); // sets the actual QTY input
setValue( // sets the AMT field that is dependent on this QTY change
`additionalCharges.${additionalCharge?.id}.amt`,
formatMoney(
Number(e.target.value) *
centsToDollars(
additionalCharge?.maximum_unit_amount
)
)
);
}}
/>
)}
/>
the original /default form state looks OK
But it turns into an array once the form re-renders
What am I doing wrong with this?
EDIT: I think this is related to the small snippet in their docs:
can not start with a number or use number as key name
What I have done is to prefix the field with _ so the key is no longer numeric, but _7841. Is there any other solution to numeric keys?
The problem come from that you're calling onChange inside your onChange event.
To set the actual QTY input you should use a useState.
I am building a simple application using React, Apollo and React Router. This application allows you to create recipes, as well as edit and delete them (your standard CRUD website).
I thought about how I would present my problem, and I figured the best way was visually.
Here is the home page (localhost:3000):
When you click on the title of a recipe, this is what you see (localhost:3000/recipe/15):
If you click the 'create recipe' button on the home page, this is what you see (localhost:3000/create-recipe):
If you click on the delete button on a recipe on the home page, this is what you see (localhost:3000):
If you click on the edit button on a recipe on the home page, this is what you see (localhost:3000/recipe/15/update):
This update form is where the problem begins. As you can see, the form has been filled with the old values of the recipe. Everything is going to plan. But, when I refresh the page, this is what you see:
It's all blank. I am 67% sure this is something to do with the way React renders components or the way I am querying my apollo server. I don't fully understand the process React goes through to render a component.
Here is the code for the UpdateRecipe page (what you've probably been waiting for):
import React, { useState } from "react";
import { Button } from "#chakra-ui/react";
import {
useUpdateRecipeMutation,
useRecipeQuery,
useIngredientsQuery,
useStepsQuery,
} from "../../types/graphql";
import { useNavigate, useParams } from "react-router-dom";
import { SimpleFormControl } from "../../shared/SimpleFormControl";
import { MultiFormControl } from "../../shared/MultiFormControl";
interface UpdateRecipeProps {}
export const UpdateRecipe: React.FC<UpdateRecipeProps> = ({}) => {
let { id: recipeId } = useParams() as { id: string };
const intRecipeId = parseInt(recipeId);
const { data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const { data: ingredientsData } = useIngredientsQuery({
variables: { recipeId: intRecipeId },
});
const { data: stepsData } = useStepsQuery({
variables: { recipeId: intRecipeId },
});
const originalTitle = recipeData?.recipe.recipe?.title || "";
const originalDescription = recipeData?.recipe.recipe?.description || "";
const originalIngredients =
ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text) || [];
const originalSteps = stepsData?.steps?.steps?.map((stp) => stp.text) || [];
const [updateRecipe] = useUpdateRecipeMutation();
const navigate = useNavigate();
const [formValues, setFormValues] = useState({
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<SimpleFormControl
label="Title"
name="title"
type="text"
placeholder="Triple Chocolate Cake"
value={formValues.title}
onChange={(e) => {
setFormValues({ ...formValues, title: e.target.value });
}}
/>
<SimpleFormControl
label="Description"
name="description"
type="text"
placeholder="A delicious combination of cake and chocolate that's bound to mesmerize your tastebuds!"
value={formValues.description}
onChange={(e) => {
setFormValues({ ...formValues, description: e.target.value });
}}
/>
<MultiFormControl
label="Ingredients"
name="ingredients"
type="text"
placeholder="Eggs"
values={formValues.ingredients}
onAdd={(newValue) => {
setFormValues({
...formValues,
ingredients: [...formValues.ingredients, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
ingredients: formValues.ingredients.filter(
(__, idx) => idx !== index
),
});
}}
/>
<MultiFormControl
ordered
label="Steps"
name="steps"
type="text"
placeholder="Pour batter into cake tray"
color="orange.100"
values={formValues.steps}
onAdd={(newValue) => {
setFormValues({
...formValues,
steps: [...formValues.steps, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
steps: formValues.steps.filter((__, idx) => idx !== index),
});
}}
/>
<Button type="submit">Update Recipe</Button>
</form>
);
};
I'll try to explain it as best as I can.
First I get the id parameter from the url. With this id, I grab the corresponding recipe, its ingredients and its steps.
Next I put the title of the recipe, the description of the recipe, the ingredients of the recipe and the steps into four variables: originalTitle, originalDescription, originalIngredients and originalSteps, respectively.
Next I set up some state with useState(), called formValues. It looks like this:
{
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
}
Finally, I return a form which contains 4 component:
The first component is a SimpleFormControl and it is for the title. Notice how I set the value prop of this component to formValues.title.
The second component is also a SimpleFormControl and it is for the description, which has a value prop set to formValues.description.
The third component is a MultiFormControl and it's for the ingredients. This component has its value props set to formValues.ingredients.
The fourth component is also aMultiFormControl and it's for the steps. This component has its value props set to formValues.steps.
Let me know if you need to see the code for these two components.
Note:
When I come to the UpdateRecipe page via the home page, it works perfectly. As soon as I refresh the UpdateRecipe page, the originalTitle, originalDescripion, originalIngredients and originalSteps are either empty strings or empty arrays. This is due to the || operator attached to each variable.
Thanks in advance for any feedback and help.
Let me know if you need anything.
The problem is that you are using one hook useRecipeQuery that will return data at some point in the future and you have a second hook useState for your form that relies on this data. This means that when React will render this component the useRecipeQuery will return no data (since it's still fetching) so the useState hook used for your form is initialized with empty data. Once useRecipeQuery is done fetching it will reevaluate this code, but that doesn't have any effect on the useState hook for your form, since it's already initialized and has internally cached its state. The reason why it's working for you in one scenario, but not in the other, is that in one scenario your useRecipeQuery immediately returns the data available from cache, whereas in the other it needs to do the actual fetch to get it.
What is the solution?
Assume you don't have the data available for your form to properly render when you first load this component. So initialize your form with some acceptable empty state.
Use useEffect to wire your hooks, so that when useRecipeQuery finishes loading its data, it'll update your form state accordingly.
const { loading, data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const [formValues, setFormValues] = useState({
title: "",
description: "",
ingredients: [],
steps: [],
});
useEffect(() => {
if (!loading && recipeData ) {
setFormValues({
title: recipeData?.recipe.recipe?.title,
description: recipeData?.recipe.recipe?.description,
ingredients: ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text),
steps: stepsData?.steps?.steps?.map((stp) => stp.text),
});
}
}, [loading, recipeData ]);
I am using React Table 7 to create a table with the first cell of each row being a ChecOut/CheckIn Button. My goal is to create a tool where people can CheckOut and CheckIn equipment. My React Table CodeSandbox
The idea is that when a user clicks the button, it will change from "CheckOut" to "CheckIn" and vise versa. It will also change the color of the row when Checked out. I was able to accomplish this with JQuery (code is in the sandbox under the App() functional component) but want to avoid that and am sure it's easily doable in react.
My issue is changing the state of an individual button and which functional component to define it in. The <Button> elements are created dynamically by the React-Table, defined in the columns object under the App functional component.
{
Header: "CheckIn/Out",
Cell: ({ row }) => (
<Button
key={row.id}
id={row.id}
onClick={e => checkClick(e)}
value={checkButton.value}
>
{checkButton.value}
</Button>
)
}
I tried passing a function to onChnage attribute on the <Button> element. In that function I updated the state of the <Button> to change the string value from "CheckOut" to "CheckIn" but that does not work. At first, that changed the name (state) of All the <Button> elements. Not sure if the state will live in the App() component or the TableRe() functional component. Or do I need to use state at all?
I basically built this on top of the editable table code available on the React-Table website React-Table Editable Data CodeSandbox , examples section React-Table Examples .
Any help is much appreciated!
data.js
import React from "react";
// export default random => {
// let arr = [];
// for (let i = 0; i < random; i++) {
// arr.push({
// first: chance.name(),
// last: chance.last(),
// birthday: chance.birthday({ string: true }),
// zip: chance.zip()
// });
// }
// return arr;
// };
const temp_data = [
{
firstName: "johny",
lastName: "Bravo",
age: "23",
visits: "45",
progress: "complete",
status: "all done"
},
{
firstName: "johny",
lastName: "Bravo",
age: "23",
visits: "45",
progress: "complete",
status: "all done"
}
];
const ArrayData = () => {
const data = temp_data.map(item => {
return {
firstName: item.firstName,
lastName: item.lastName,
age: item.age,
visits: item.visits,
progress: item.progress,
status: item.status
};
});
return data;
};
// Do not think there is any need to memoize this data since the headers
// function TempData() {
// const data = React.useMemo(ArrayData, []);
// return data;
// }
export default ArrayData;
I have a dropdown list which is populated from a database. The first option is 'none' (actual record in database with objectId) which should be the default option and only needs to be changed if the user wants to, otherwise it should just use that initial value when submitting the form. However, even though it is selected and has a valid objectId, I still get a validation error saying the field is empty. The validation error only goes away if I select something else from the select menu or select something else and then select 'none' again. I am using Joi-browser for validation.
schema = {
subcategoryId: Joi.string()
.required()
.label("Subcategory"),
}
This is the select menu:
<Form onSubmit={this.handleSubmit}>
<Form.Group controlId="subcategoryId">
<Form.Label>Sub-category</Form.Label>
<Form.Control
as="select"
name="subcategoryId"
value={this.state.data.subcategoryId}
onChange={this.handleChange}
error={this.state.errors.subcategory}
>
{this.state.subcategories.map(subcategory => (
<option key={subcategory._id} value={subcategory._id}>
{subcategory.name}
</option>
))}
</Form.Control>
{this.state.errors.subcategoryId && (
<Alert variant="danger">
{this.state.errors.subcategoryId}
</Alert>
)}
</Form.Group>
And here is my state:
state = {
data: {
name: "",
description: "",
categoryId: "",
subcategoryId: "",
price: ""
},
categories: [],
subcategories: [],
errors: {}
};
const { data: subcategories } = await getSubcategories();
this.setState({ subcategories });
And this is the html output of the dropdown's first field which I want selected by default:
<option value="5d4b42d47b454712f4db7c67">None</option>
The error I get back is that the category Id cannot be empty, yet each option in the select menu has a value. I am new to react but perhaps the value is only actually assigned upon change?
You need to edit componentDidMount. After you get your subcategories, you'll need to set the state to of this.state.data.subcategoryId to one of the categories. This is because you're using a controlled component. Otherwise, it'll still be set to "", which isn't one of the valid values for the <select> component, and likely why it's failing validation.
async componentDidMount() {
// getting a copy of this.state.data so as not to mutate state directly
const data = { ...this.state.data };
const { data: subcategories } = await getSubcategories();
// filter the array to a new array of subcategories that have the name === 'none'
const arrayOfSubcategoriesWhereNameIsNone = subcategories.filter(i => i.name === 'none');
const getIdOfFirstElementOfArray = arrayOfSubcategoriesWhereNameIsNone [0]._id;
//set getIdOfFirstElementOfArray equal to the function's local copy of this.state.data.subcategoryId
data.subcategoryId = getIdOfFirstElementOfArray;
// update the state with the mutated object (the function's local copy of this.state.data)
this.setState({ subcategories, data });
}
I have this state:
state = {
formdata:{
name: null,
about: null,
price: null,
offerPrice:null,
playStoreUrl:null,
appStoreUrl:null ,
photo:null,
}
}
what I want: update form inside modal i used it to update products. I used new props inside componentWillReceiveProps
I did:
componentWillReceiveProps(nextProps){
let Updateproduct = nextProps.productlist.productlist.Products;
Updateproduct.map((item,i) => {
let formdata = Object.assign({}, this.state.formdata);
formdata.name = item.name
formdata.about = item.about
formdata.price = item.price
formdata.offerPrice = item.offerPrice
formdata.playStoreUrl = item.playStoreUrl
formdata.appStoreUrl = item.appStoreUrl
formdata.photo = item.photo
console.log(formdata)
this.setState({formdata})
})
}
MyProblem: this filled the objects but in the form inside modal only I saw the last product not all in modal when click to update any product it. Note:Updateproduct contains:
{
about: "about product1"
appStoreUrl: "https://itunes.apple.com/us/app/snapchat/id447188370?mt=8"
name: "p1"
offerPrice: 99.99
photo: "images/products/"
playStoreUrl: "images/products/"
price: 1000
}
{
about: "about product2"
appStoreUrl: "https://itunes.apple.com/us/app/snapchat/id447188370?mt=8"
name: "p2"
offerPrice: 99.99
photo: "images/products/"
playStoreUrl: "images/products/"
price: 2000
}
Issue is, you want to store single specific product item clicked by user in state variable, but with current code you are always storing the last product item. Also setState in loop is not a good way.
To solve the issue, store the clicked product detail in state inside onClick handler function only, instead of componentWillReceiveProps method. For that you need to bind the product id with onClick function.
Like this:
onClick={this.getProductId.bind(this, item.id)}
// click handler function
getProductId = (id) => {
let productObj = {};
let Updateproduct = this.props.productlist.productlist.Products;
Updateproduct.forEach((item,i) => {
if(item.id == id) {
productObj = {
name: item.name,
about: item.about,
price: item.price,
offerPrice: item.offerPrice,
playStoreUrl: item.playStoreUrl,
appStoreUrl: item.appStoreUrl,
photo: item.photo
};
this.setState({ formdata: productObj, id: id })
}
})
}
You’re setting your state inside the map. If you think of the map as a loop that means you’re overriding it each iteration. So you will only ever have the last one displaying.
I think that's because your setState() call is in the wrong place (and only one object at a time):
let data =[];
UpdateProduct.map((item, i) => {
let formdata = null;
// do your formdata stuff but append it as part of an array
data.push(formdata);
});
this.setState({ formdata: data });
Then I think it should get all the products.