I have a form I am mapping a list of checkbox inputs to using React. The mapping function receives a list of JSON objects and maps them to inputs as follows:
const listCustomBenefits = props.customBenefits.map(benefit => {
return(
<div className="flex">
<input onChange={props.handleCustomBenefitsChange} id={benefit.id} key={benefit.id}
className="my-auto" type="checkbox" checked={benefit.included}></input>
<p className="my-auto px-2">{benefit.benefit}</p>
</div>
)
})
props.customBenefit is formatted as followed:
const customBenefit = useState([
{
benefit: "first benefit description",
id: 1,
included: false,
name: "First Benefit",
},
{
benefit: "second benefit description",
id: 2,
included: false,
name: "Second Benefit",
},
]);
listCustomBenefits is then returned in the component's return JSX. When a checkbox is clicked, handleCustomBenefitsChange updates the state of custom benefits as follows:
const handleCustomBenefitsChange = (event) =>{
event.preventDefault()
const newCustomBenefits = customBenefits.map(benefit =>{
if(benefit.id == event.target.id){
const newCheck = [event.target.checked][0]
return({...benefit, included: newCheck})
}
else{
return benefit
}
})
setCustomBenefits(newCustomBenefits)
}
The function properly updates the state of customBenefits, but the checkbox display is always one event behind (i.e. you have to click the checkbox twice to see the desired change).
I have tried setting state with props on the local component as well (instead of using props directly) but can't seem to get the component to update on the initial click. Can anyone help me resolve this?
I didn't dig into exactly why, but I think it's something to do with the newCheck being out of date. Instead of relying on the dom state we can just flip benefit.included and pass around the id instead of grabbing it from the event target.
So our checkbox would look like this
<input
onChange={() => handleCustomBenefitsChange(benefit.id)}
...
/>
and our handler changes to accept the ID instead of the event, and makes the change based off of it's own state rather than the dom
const handleCustomBenefitsChange = id => {
const newCustomBenefits = customBenefits.map(benefit => {
if(benefit.id == id){
return({...benefit, included: !benefit.included})
} else {
return benefit;
}
});
setCustomBenefits(newCustomBenefits);
};
Related
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 ]);
Oky, so wasted like 12 hours already on this. And I really need help.
I created a filter using an array via Map that returns some checkbox components
Component:
import { useEffect, useState } from "react"
interface Checkbox {
id: string | undefined,
name: string,
reg: any,
value: any,
label: string,
required?: boolean,
allChecked: boolean
}
const Checkbox = ({ id, name, reg, value, label, allChecked }: Checkbox) => {
const [checked, setChecked] = useState(false);
useEffect(() => {
setChecked(allChecked);
}, [allChecked])
return (
<>
<label key={id}>
<input type="checkbox"
{...reg}
id={id}
value={value}
name={name}
checked={checked}
onClick={() => {
setChecked(!checked)
}}
onChange={
() => { }
}
/>
{label}
</label>
</>
)
}
export default Checkbox;
Map:
dataValues.sort()
?
dataValues.map((e: any, i: number) => {
let slug = e.replace(' ', '-').toLowerCase();
return (
<div key={JSON.stringify(e)} id={JSON.stringify(e)}
>
<Checkbox id={slug}
name={title as string}
label={e as string}
value={e}
reg={{ ...register(title as string) }}
allChecked={allChecked}
/>
</div>
)
})
:
null
}
State that is just above the Map:
const [allChecked, setAllChecked] = useState<boolean>(false);
When I try to change the state on the parent and check or uncheck all of the checkboxes, nothing happens.
(the form works without a problem if I manually click on the checkboxes, but I cannot do this as I have some sections with over 40 values)
Sample of array:
dataValues = [
"Blugi",
"Bluza",
"Body",
"Bratara",
"Camasa",
"Cardigan",
"Ceas",
"Cercel",
"Colier",
"Fusta",
"Geanta Cross-body",
"Hanorac",
"Jacheta",
"Palton",
"Pantaloni",
"Pulover",
"Rochie",
"Rucsac",
"Sacou",
"Salopeta",
"Set-Accesorii",
"Top",
"Trench",
"Tricou",
"Vesta"
]
allChecked never changes (at least in the code shown here).
Here's the timeline:
The parent passes down a boolean prop allChecked. That's supposed to tell us if all the checkboxes are checked or not.
useEffect in Checkbox runs on mount, and allChecked is false because that's its default. useEffect then sets checked to allChecked's value, false. Which it already is, because its default is also false.
useEffect then listens for a change in allChecked via [allChecked] that never comes.
Clicking on any checkbox just toggles the state of that checkbox. allChecked's setter, setAllChecked, is never passed to the child or called from the parent.
What's the solution?
Somewhere, setAllChecked(true) needs to happen. Maybe a single checkbox with a label "Check all"?
Then, allChecked in Checkbox needs to be able to control the checkbox inputs. One implementation could be:
checked={checked || allChecked}
I managed to solve this.
There were 2 main problems:
Checkboxes were not rendering again so react-hook-form would see the old value, no matter what
I couldn't press clear all multiple times, because I was sending the allChecked = = false info multiple times, and the state wasn't exactly changing.
What I did was force a render by integrating the allChecked state as an object
interface checkedState {
checked: boolean,
render: boolean
}
So, whenever I send the state, I send it something like:
{
checked: true,
render: !allChecked.render
}
meaning that the new object is always new, no matter if I send the same information.
I have a React web app that is effectively a ton of Questions. These questions need to be validated/laid-out based on their own state values (ie: must be a number in a number field), as well as on the values of each other. A few examples of the more complex 'validation':
Questions A, B, and C might be required to have non-empty values before allowing a 'save' button.
Question B's allowable range of values might be dependent on the value of question A.
Question C might only show if question A is set to 'true'.
You can imagine many other interactions. The app has hundreds of questions - as such, I have their configuration in a JSON object like this:
{ id: 'version', required: true, label: 'Software Version', xs: 3 },
{
id: 'licenseType', label: 'License Type', xs: 2,
select: {
[DICTIONARY.FREEWARE]: DICTIONARY.FREEWARE,
[DICTIONARY.CENTER_LICENSE]: DICTIONARY.CENTER_LICENSE,
[DICTIONARY.ENTERPRISE_LICENSE]: DICTIONARY.ENTERPRISE_LICENSE
}
},
... etc.
I would then turn this object into actual questions using a map in the FormPage component, the parent of all the questions. Given the need to store these interaction in the closest common parent, I store all of the Question values in a formData state variable object and the FormPage looks like so:
function FormPage(props) {
const [formData, setFormData] = useState(BLANK_REQUEST.asSubmitted);
const handleValueChange = (evt, id) => {
setFormData({ ...formData, [id]: evt.target.value})
}
return <div>
{QUESTIONS_CONFIG.map(qConfig => <Question qConfig={qConfig} value={formData[qConfig.id]} handleValueChange={handleValueChange}/>)}
// other stuff too
</div>
}
The Question component is basically just a glorified material UI textField that has it's value set to props.value and it's onChange set to props.handleValueChange. The rest of the qConfig object and Question component is about layout and irrelevant to the question.
The problem with this approach was that every keypress results in the formData object changing... which results in a re-render of the FormPage component... which then results in a complete re-render/rebuild of all my hundreds of Question components. It technically works, but results performance so slow you could watch your characters show up as you type.
To attempt solve this, I modified Question to hold it's own value in it's own state and we no longer pass formData to it... the Question component looking something like this:
function Question(props) {
const { qConfig, valueChangedListener, defaultValue } = props;
const [value, setValue] = useState(props);
useEffect(() => {
if (qConfig.value && typeof defaultValue !== 'undefined') {
setValue(qConfig.value);
}
}, [qConfig.value])
const handleValueChange = (evt, id) => {
setValue(evt.target.value);
valueChangedListener(evt.target.value, id)
}
return <div style={{ maxWidth: '100%' }}>
<TextField
// various other params unrelated...
value={value ? value : ''}
onChange={(evt) => handleValueChange(evt, qConfig.id)}
>
// code to handle 'select' questions.
</TextField>
</div>
}
Notably, now, when it's value changes, it stores it's own value only lets FormPage know it's value was updated so that FormPage can do some multi-question validation.
To finish this off, on the FormPage I added a callback function:
const processValueChange = (value, id) => {
setFormData({ ...formData, [id]: value })
};
and then kept my useEffect that does cross-question validation based on the formData:
useEffect(() => { // validation is actually bigger than this, but this is a good example
let missingArr = requiredFields.filter(requiredID => !formData[requiredID]);
setDisabledReason(missingArr.length ? "Required fields (" + missingArr.join(", ") + ") must be filled out" : '');
}, [formData, requiredFields]);
the return from FormPage had a minor change to this:
return <div>
{questionConfiguration.map(qConfig =>
<Question
qConfig={qConfig}
valueChangedListener={processValueChange}
/>
</ div>
)
}
Now, my problem is -- ALL of the questions still re-render on every keypress...
I thought that perhaps the function I was passing to the Question component was being re-generated so I tried wrapping processValueChange in a useCallback:
const processValueChange = React.useCallback((value, id) => {
setFormData({ ...formData, [id]: value })
}
},[]);
but that didn't help.
My guess is that even though formData (a state object on the FormPage) is not used in the return... its modification is still triggering a full re-render every time.
But I need to store the value of the children so I can do some stuff with those values.
... but if I store the value of the children in the parent state, it re-renders everything and is unacceptbaly slow.
I do not know how to solve this? Help?
How would a functional component store all the values of its children (for validation, layout, etc)... without triggering a re-render on every modification of said data? (I'd only want a re-render if the validation/layout function found something that needed changing)
EDIT:
Minimal sandbox: https://codesandbox.io/s/inspiring-ritchie-b0yki
I have a console.log in the Question component so we can see when they render.
I have created my custom Autocomplete (Autosuggestions) component. Everything works fine when I pass a hardcoded array of string to autocomplete component, but when I try to pass data from API as a prop, nothing is showing for the first time I search. Results are showing each time exactly after the first time
I have tried different options but seems like when a user is searching for the first time data is not there and autocomplete is rendered with an empty array. I have tested same API endpoint and it's returning data as it should every time you search.
Home component which holds Autocomplete
const filteredUsers = this.props.searchUsers.map((item) => item.firstName).filter((item) => item !== null);
const autocomplete = (
<AutoComplete
items={filteredUsers}
placeholder="Search..."
label="Search"
onTextChanged={this.searchUsers}
fieldName="Search"
formName="autocomplete"
/>
);
AutoComplete component which filters inserted data and shows a list of suggestions, the problem is maybe inside of onTextChange:
export class AutoComplete extends Component {
constructor(props) {
super(props);
this.state = {
suggestions: [],
text: '',
};
}
// Matching and filtering suggestions fetched from the backend and text that user has entered
onTextChanged = (e) => {
const value = e.target.value;
let suggestions = [];
if (value.length > 0) {
this.props.onTextChanged(value);
const regex = new RegExp(`^${value}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
}
this.setState({ suggestions, text: value });
};
// Update state each time user press suggestion
suggestionSelected = (value) => {
this.setState(() => ({
text: value,
suggestions: []
}));
};
// User pressed the enter key
onPressEnter = (e) => {
if (e.keyCode === 13) {
this.props.onPressEnter(this.state.text);
}
};
render() {
const { text } = this.state;
return (
<div style={styles.autocompleteContainerStyles}>
<Field
label={this.props.placeholder}
onKeyDown={this.onPressEnter}
onFocus={this.props.onFocus}
name={this.props.fieldName}
formValue={text}
onChange={this.onTextChanged}
component={RenderAutocompleteField}
type="text"
/>
<Suggestions
suggestions={this.state.suggestions}
suggestionSelected={this.suggestionSelected}
theme="default"
/>
</div>
);
}
}
const styles = {
autocompleteContainerStyles: {
position: 'relative',
display: 'inline',
width: '100%'
}
};
AutoComplete.propTypes = {
items: PropTypes.array.isRequired,
placeholder: PropTypes.string.isRequired,
onTextChanged: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onPressEnter: PropTypes.func.isRequired,
onFocus: PropTypes.func
};
export default reduxForm({
form: 'Autocomplete'
})(AutoComplete);
Expected results: Every time user use textinput to search, he should get results of suggestions
Actual results: First-time user use textinput to search, he doesn't get data. Only after first-time data is there
It works when it is hardcoded but not when using your API because your filtering happens in onTextChanged. When it is hardcoded your AutoComplete has a value to work with the first time onTextChanged (this.props.items.sort().filter(...) is called but with the API your items prop will be empty until you API returns - after this function is done.
In order to handle results from your API you will need do the filtering when the props change. The react docs actually cover a very similar case here (see the second example as the first is showing how using getDerivedStateFromProps is unnecessarily complicated), the important part being they use a PureComponent to avoid unnecessary re-renders and then do the filtering in the render, e.g. in your case:
render() {
// Derive your filtered suggestions from your props in render - this way when your API updates your items prop, it will re-render with the new data
const { text } = this.state;
const regex = new RegExp(`^${text}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
...
<Suggestions
suggestions={suggestions}
...
/>
...
}
Lets imagine we want an input for a "product" (stored in redux) price value.
I'm struggle to come up with the best way to handle input constraints. For simplicity, lets just focus on the constraint that product.price cannot be empty.
It seems like the 2 options are:
1: Controlled
Implementation: The input value is bound to product.price. On change dispatches the changePrice() action.
The main issue here is that if we want to prevent an empty price from entering the product store, we essentially block the user from clearing the input field. This isn't ideal as it makes it very hard to change the first digit of the number (you have to select it and replace it)!
2: Using defaultValue
Implementation: We set the price initially using input defaultValue, that allows us to control when we want to actually dispatch changePrice() actions and we can do validation handling in the onChange handler.
This works well, unless the product.price is ever updated from somewhere other than the input change event (for example, an applyDiscount action). Since defaultValue doesn't cause rerenders, the product.price and the input are now out of sync!
So what am I missing?
There must be a simple & elegant solution to this problem but I just can't seem to find it!
What I have done in the past is to use redux-thunk and joi to solve input constraints/validation using controlled inputs.
In general I like to have one update action that will handle all the field updating. So for example if you have two inputs for a form, it would looks something like this:
render() {
const { product, updateProduct } = this.props;
return (
<div>
<input
value={product.name}
onChange={() => updateProduct({...product, name: e.target.value})}
/>
<input
value={product.price}
onChange={() => updateProduct({...product, price: e.target.value})}
/>
</div>
)
}
Having one function/action here simplifies my forms a great deal. The updateProject action would then be a thunk action that handles side effects. Here is our Joi Schema(based off your one requirement) and updateProduct Action mentioned above. As a side note, I also tend to just let the user make the mistake. So if they don't enter anything for price I would just make the submit button inactive or something, but still store away null/empty string in the redux store.
const projectSchema = Joi.object().keys({
name: Joi.number().string(),
price: Joi.integer().required(), // price is a required integer. so null, "", and undefined would throw an error.
});
const updateProduct = (product) => {
return (dispatch, getState) {
Joi.validate(product, productSchema, {}, (err, product) => {
if (err) {
// flip/dispatch some view state related flag and pass error message to view and disable form submission;
}
});
dispatch(update(product)); // go ahead and let the user make the mistake, but disable submission
}
}
I stopped using uncontrolled inputs, simply because I like to capture the entire state of an application. I have very little local component state in my projects. Keep in mind this is sudo code and probably won't work if directly copy pasted. Hope it helps.
So I think I've figure out a decent solution. Basically I needed to:
Create separate component that can control the input with local state.
Pass an onChange handler into the props that I can use to dispatch my changePrice action conditionally
Use componentWillReceiveProps to keep the local value state in sync with the redux store
Code (simplified and in typescript):
interface INumberInputProps {
value: number;
onChange: (val: number) => void;
}
interface INumberInputState {
value: number;
}
export class NumberInput extends React.Component<INumberInputProps, INumberInputState> {
constructor(props) {
super(props);
this.state = {value: props.value};
}
public handleChange = (value: number) => {
this.setState({value});
this.props.onChange(value);
}
//keeps local state in sync with redux store
public componentWillReceiveProps(props: INumberInputProps){
if (props.value !== this.state.value) {
this.setState({value: props.value});
}
}
public render() {
return <input value={this.state.value} onChange={this.handleChange} />
}
}
In my Product Component:
...
//conditionally dispatch action if meets valadations
public handlePriceChange = (price: number) => {
if (price < this.props.product.standardPrice &&
price > this.props.product.preferredPrice &&
!isNaN(price) &&
lineItem.price !== price){
this.props.dispatch(updatePrice(this.props.product, price));
}
}
public render() {
return <NumberInput value={this.props.product.price} onChange={this.handlePriceChange} />
}
...
What i would do in this case is to validate the input onBlur instead of onChange.
For example consider these validations in the flowing snippet:
The input can't be empty.
The input should not contain "foo".
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
myVal: '',
error: ''
}
}
setError = error => {
this.setState({ error });
}
onChange = ({ target: { value } }) => {
this.setState({ myVal: value })
}
validateInput = ({ target: { value } }) => {
let nextError = '';
if (!value.trim() || value.length < 1) {
nextError = ("Input cannot be empty!")
} else if (~value.indexOf("foo")) {
nextError = ('foo is not alowed!');
}
this.setError(nextError);
}
render() {
const { myVal, error } = this.state;
return (
<div>
<input value={myVal} onChange={this.onChange} onBlur={this.validateInput} />
{error && <div>{error}</div>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
As a followup to your comments.
To make this solution more generic, i would pass the component a predicate function as a prop, only when the function will return a valid result i would call the onChange that passed from the parent or whatever method you pass that updating the store.
This way you can reuse this pattern in other components and places on your app (or even other projects).