I have a huge form and sumbit is triggered from outside the form
<App>
<Form/>
<Button/>
</App>
The problem is that I need to have current form fields object in button component. I've tried to pass state through multiple layers by passing setState function:
const [formFields, setFormFields] = useState(null);
<App>
<Form setData={setFormFields}/>
<Button data={formFields}/>
</App>
And also to use redux dispatch(on form field changes) and useSelector in button component to get current data. But both methods seems to really slow down the application when I'm writing some text in input fields.
What would be the best solution to optimize it?
really slow down the application when I'm writing some text in input fields.
This is because you're tying the form's data to the rendering. It would be fine to store the whole form through Redux as well (or wherever else you want), as long as you're not creating a dependency between the component's rendering lifecycle and those mutations in the form field's values.
Does the whole form need to update on each individual components' events? Not really. However, since the whole form is gathered in one big object, and that object's reference is changed after an update, then the whole component tree gets re-rendered.
To reduce the need for re-rendering, you can synchronize the data (note: the data, not the validations, etc...) outside of React, and only fetch it when you submit.
import React, { useState } from "react";
const externalFormData = {};
function Form() {
return (
<input
onChange={function(ev) {
externalFormData.input = ev.target.value;
}}
/>
);
}
export default function App() {
const [formData, setFormData] = useState();
return (
<div>
<Form />
<input
type={"button"}
onClick={function() {
setFormData(externalFormData);
}}
value="Submit form"
/>
<p>{`Submitted form is: ${JSON.stringify(formData)}`}</p>
</div>
);
}
If that's your choice, I suggest looking for some existing form library which handles that for you (including validation and etc).
Another idea would be decoupling this "big object" you mentioned into individual objects which gets updated separately, thus only triggering re-renders on the components affected by the value.
Related
I have written a form which has more than 30 fields(material-ui components) using react-hook form.
All of the fields have different kinds of validation depending upon the type of the field.
The sample folder structure is as shown in this link.
folderStructure
detail_page.js is the root component. In this component useForm is initialized with defaultValues. And the form is wrapped with FormProvider(Context to pass to the deeply nested components). Also handleSave method is written in this component only. Upon clicking SAVE button, handleSubmit(handleSave) is called. From this component TopLevel(top_level.js) component is called.
top_level.js file groups fields(passed as props from detail_page.js) into sections.
A render method(renderSections) is written to display each section in mui-accordion, and all the fields within that section is passed to iterator.js file. the renderSections method is memoized with the dependency array of sectionobjects.
iterator.js file loops through each of the the fields passed as props from top_level.js and calls respective system_component based on the type of the field (Radio,checkbox,textbox,textarea,email,password,file_uploader,address,latlong,phone_number,list,date,date_range etc).
system_component is the actual component which wraps the material-ui's component using useController/Controller. Based on the type of field, respective system_component is called in iterator. for eg:-
SystemTextBox for type input
SystemEmail for type email in input
SystemDate for type date
and so on.
Some system component may be a combination of 2 or more mui's components. for eg
-SystemLatlong- combination of 4 or more text inputs and number input to display address components of the particular latlong.
SystemArray - any combination of rest of the system components(calling iterator again for this purpose)
and so on.
Now the problem is, whenever I click on save button, the save button freezes, In handleSubmit method I am setting MuiBackdrop component to display "Saving" text. This backdrop is called after 5+ seconds upon clicking save button. In detail page I have consoled isSubmitting, isValidating, isSubmitted state of formState. I figured out that, for almost 5+ seconds isSubmitting state holds true value, until which handleSave function is not triggered.
the reason for this is that react-hook-form uses context which is not optimized for performant re-renders. With context all consumers of that context will re-render regardless of if they care of the value that changed. With react hook form having one central context every input re-renders when isSubmitting is changed unfortunately.
I'm building a library that will solve this issues that is currently in beta called #zerry/react-formz. It guarantees isolated re-renders for inputs which means near constant performance no matter how many inputs you are rendering.
Its' in beta and currently being worked on but it might solve the issues you are having.
You can check it out here if you feel so inclined: https://react-formz.zerry.dev/docs/performance/
import {
Form,
TextField,
NumberField,
DependentTextField,
} from "#zerry/react-formz";
const NestedInput = () => {
return (
<TextField
required
name="name"
as={({ input }) => <input {...input} />}
/>
)
}
const MyForm = () => {
return (
<Form initialValues={{ name: "", age: "" }}>
<NestedInput />
<NumberField
required
name="age"
as={({ input }) => <input {...input} />}
/>
</Form>
);
};
when "Search" is clicked, next page is loaded by the below code.
import React from "react";
import { Link } from "react-router-dom";
function name(){
return(
<div className="search">
<input type="text"placeholder="city name"></input>
<input type="text"placeholder="number of people"></input>
<p><Link to="/nextpage">Search</Link</p>
</div>
)
}
I want to take data of these input fields to fetch api using that data to make cards on next page.
How to do it?
Here's the general idea of how you could accomplish this. You need to store the form input (in your case, the city and number of people) in application state. You can do that using the useState hook. Now this state can be managed by your first component (the one which renders the input fields) and accessed by the second component (the one that will display the values).
Because the values need to be accessed by both components, you should store the state in the parent component (see lifting state up). In my example, I used App which handles routing and renders FirstPage and SecondPage. The state values and methods to change it are passed as props.
Here's how you initialise the state in the App component:
const [city, setCity] = useState(null);
const [peopleCount, setPeopleCount] = useState(null);
city is the value, setCity is a function which enables you to modify the state. We will call setCity when the user makes a change in the input, like this:
export const FirstPage = ({ city, setCity, peopleCount, setPeopleCount }) => {
...
const handleCityChange = (e) => {
setCity(e.target.value);
};
...
<input
type="text"
placeholder="city name"
value={city}
onChange={handleCityChange}
/>
When a change in the input is made, the app will call the setCity function which will update the state in the parent component. The parent component can then update the SecondPage component with the value. We can do it simply by passing the value as a prop:
<NextPage city={city} peopleCount={peopleCount} />
Now you can do whatever you want with the value in your SecondPage component. Here's the full code for it, where it just displays both values:
export const NextPage = ({ city, peopleCount }) => {
return (
<div>
<p>city: {city}</p>
<p># of people: {peopleCount}</p>
</div>
);
};
And here's the full working example: https://stackblitz.com/edit/react-uccegh?file=src/App.js
Note that we don't have any field validation, and we have to manually write the change handlers for each input. In a real app you should avoid doing all this by yourself and instead use a library to help you build forms, such as Formik.
I am trying to explore react library with next framework. Since I am an angular developer, I like to reuse some of my reactive-form to my new application. I found this library since it has almost the same implementation of reactive-form.
Now, I am using state variable on my parent form; however, whenever I try to update the value from child (which is the reactive form). I cannot accomplish it.
Here's my simple code to replicate this.
import React, { useState } from "react";
import { FieldGroup, FieldControl } from "react-reactive-form";
export default function App() {
const [isRegistered, setIsRegistered] = useState(false);
async function submitForm(e) {
e.preventDefault();
setIsRegistered(state => !state);
console.log(isRegistered);
//await function call .....
}
return (
<div>
<h1>Hello StackBlitz!</h1>
<FieldGroup
control={form}
render={({ get, invalid }) => (
<form onSubmit={submitForm}>
<FieldControl
name="email"
render={TextInput}
meta={{ label: "Email" }}
/>
<button type="submit">Submit</button>
<p>{isRegistered.toString()}</p>
{isRegistered ? <span>Registered Successfully</span> : null}
</form>
)}
/>
</div>
)
}
Just to keep it short, the form and TextInput is just a model and an element.
As you can see., I am updating my state variable on the submitForm function by putting it as an onSubmit function of my form; however, I am able to trigger the submitForm whenever I am trying to submit, but the state variable value doesn't change.
The thing is, when I try to call the submitForm function outside the child (FieldGroup), I am able to update the value.
I created a sample app so you can check as well.
It seems like you need to set strict prop to false for FieldGroup, like described here: https://github.com/bietkul/react-reactive-form/blob/master/docs/api/FieldGroup.md
strict: boolean;
Default value: true
If true then it'll only re-render the component only when any change happens in the form group control irrespective of the parent component(state and props) changes.
I don't know this library, but to me it just looks like the FormGroup is not re-render, because none of it's props are being changed.
The documentation says that passing strict={false} to the <FieldGroup /> component should allow it to re-render when the parent component updates as well. In your given example (thanks for making an example) that also does the trick.
So we all know uncontrolled components are usually a bad thing, which is why we usually want to manage the state of an input (or group of inputs) at a higher-level component, usually some kind of container. For example, a <Form /> component manages state and passes down state as values to its <Input /> components. It also passes down functions such as handleChange() that allow the Input to update the state.
But while implementing my own <NumericInput /> component, it got me thinking that fundamentally this component is not self-reliant. It's reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my <NumericInput />, two additional functions to control the stepper arrows.
If I (or someone who took over my code) wanted to use this <NumericInput />, but they forget to run to a different container component and copy the stepUp() and stepDown() functions to pass down as props, then there will just be two non-functional arrows. I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive. Am I thinking about this incorrectly, or is there a better way of managing this?
I recognize this is more of a theory/design question, but I'm including my code below for reference:
NumericInput:
const NumericInput = ({label, stepUp, stepDown, ...props}) => (
<>
{label && <Label>{label}</Label>}
<InputContainer>
<Input type={props.type || "number"} {...props} />
<StepUp onClick={stepUp}>
//icon will go here
</StepUp>
<StepDown onClick={stepDown}>
//icon will go here
</StepDown>
</InputContainer>
</>
);
Form.js
const Form = (props) => {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
}
const stepUp = () => {
setValue(value++);
}
const stepDown = () => {
setValue(value--);
}
return (
<NumericInput
stepUp={stepUp}
stepDown={stepDown}
handleChange={handleChange}
label="Numeric"
)
}
Let's try to boil down your questions a bit:
[NumericInput] is reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my , two additional functions to control the stepper arrows.
The only repetition you have is to define a value and a handleChange callback property for <NumericInput />. The value prop contains your numeric input value and is passed in from the parent container component owning that state. You need the callback to trigger a change request from the child (controlled component). That is the ususal approach, when you need to lift state up or divide your components in container and presentational components.
stepUp/stepDown methods are an implementation detail of NumericInput, so you can remove them from Form. Form just wants to know, if a value change has been triggered and what the new changed numeric value is.
Concerning where to store the state: There is probably a reason, why you want to save the state in the Form component. The user enters some input in (possibly) multiple fields before pressing the submit button. In this moment your form needs all the state of its child fields to trigger further processing based on the given input. And the idiomatic React way is to lift state up to the form to have the information.
I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive.
A presentational component is rather less error prone and is independent, because it doesn't care about state! For sure, you write a bit more boilerplate code by exposing callbacks and value props in child. The big advantage is, that it makes the stateless components far more predictable and testable and you can shift focus on complex components with state, e.g. when debugging.
In order to ease up props passing from Form to multiple child components, you also could consolidate all event handlers to a single one and pass state for all inputs. Here is a simple example, how you could handle the components:
NumericInput.js:
const NumericInput = ({ label, value, onValueChanged }) => {
const handleStepUp = () => {
onValueChanged(value + 1);
};
const handleStepDown = () => {
onValueChanged(value - 1);
};
return (
<>
...
<Input value={this.state.value} />
<StepUp onClick={handleStepUp}></StepUp>
<StepDown onClick={handleStepDown}></StepDown>
</>
);
};
Form.js:
const Form = (props) => {
const [value, setValue] = useState(0);
const handleValueChanged = (value) => {
setValue(value);
}
return (
<NumericInput
onValueChanged={handleValueChanged}
value={value}
label="Numeric"
)
}
Move the setValue function to the NumericInput component
Manage your state through the component.
return (
<NumericInput
setValue={setValue}
label="Numeric"
/>
)
I would recommend you use hooks
I am building a simple database and I am implementing Search Criteria form. I want to dispatch an action to Reducer only when the form is submitted.
const mapDispatchToProps = dispatch => {
return {
onSubmittedForm: (criteria) => dispatch(compActions.submitCriteria(criteria))
};
};
criteria is an object stored is a local state:
state = {
criteria: {
group: '',
province: '',
realms: []
}
}
and in the same manner (currently) in Reducer.
When I dispatch action to Reducer then the local state gets reset. I can see the criteria correctly in Reducer, they are mapped then correctly to props. The local state is also working fine.
To initialise Criteria component I use:
<Criteria
options={this.props.options}
realms={this.state.criteria.realms}
realmsChanged={this.handleCriteriaRealmsChange}
formSubmitted={this.handleCriteriaFormSubmission}
/>
realms is one of the criteria.
I am forced to use the local state, otherwise, I have to dispatch action to Reducer whenever input changes (to be able to obtain the updated value from this.props.criteria.realms instead of this.state.criteria.realms, as it is now) which works fine too, but the essential requirement is to update Reducer' state only upon form submission, not on input change.
To reflect the issue, it would be tempting to use as a value something like:
const realms = (this.props.criteria.realms && this.props.criteria.realms.length > 0) ? this.props.criteria.realms : this.state.criteria.realms;
but I know it isn't the right approach.
Perhaps there is a way to keep the state untouched after the action is dispatched to Reducer? Could you please advise?
UPDATE
I simplified the component. Currently, on every change, the component (and all other components that rely on <Criteria />) re-render because onChange dispatches new group value to Reducer (which is then mapped to this.props.criteria.group). This works but I want to perform re-render for all the components only when I submit the form via a button click.
Currently, the onCriteriaFormSubmission does nothing because the state is already updated due to onChange.
That's why I thought the idea of the local state will work but it doesn't because group field expects value that must be this.props.criteria.groups if I pass this already to Reducer, otherwise it's always empty...
So the only thing that comes to my mind is to not pass the criteria values to Reducer upon a single criteria field change (in this case groups but can be more of them) but somehow submit them all together upon form submission via: onCriteriaFormSubmission). However, I am ending up in an infinite loop because then I have no way to display the true value when a criteria field changes.
import React, { Component } from 'react';
import Select from '../../UI/Inputs/Select/Select';
import { Button, Form } from 'semantic-ui-react';
import * as companiesActions from '../../../store/actions/companies';
import { connect } from 'react-redux';
class Criteria extends Component {
render = () => {
return (
<section>
<Form onSubmit={this.props.onCriteriaFormSubmission}>
<Form.Field>
<Select
name='group'
options={this.props.options.groups}
placeholder="Choose group"
multiple={false}
value={this.props.criteria.group}
changed={this.props.onGroupChange}
/>
</Form.Field>
<Button
type='submit'
primary>
Search
</Button>
</Form>
</section>
);
}
}
const mapStateToProps = state => {
return {
criteria: state.r_co.criteria,
options: {
groups: state.r_op.groups
}
};
};
const mapDispatchToProps = dispatch => {
return {
onCriteriaFormSubmission: (criteria) => dispatch(companiesActions.criteriaFormSubmission(criteria)),
onGroupChange: (event, data) => dispatch(companiesActions.groupChange(event, data))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Criteria);
UPDATE 2 with RESOLUTION:
I tried the preventDefault() method of event upon submit, suggested in the comments but it didn't help either. I started to think the problem may be somewhere else, outside the component as the state clearly got reset upon dispatch (submitting form did not cause reset).
I started to learn about React Router and Store persistence and decided to double check the Redux implementation.
My index.js was like this:
<Provider store={store}>
<App />
</Provider>
while inside App.js my <BrowserRouter> was wrapped around <Aux> component. Once I moved <BrowserRouter> directly under <Provider> the reset problem disappeared and everything behaves as it should now.
So my index.js is now
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
Although I've never had to retain local state after form submission, this behavior seems pretty strange. Could you maybe use preventDefault on the method that handles the form to prevent the page from reloading after submission?
Let me know if that works.
It does not seem like you have prevented the default behavior of form reload(given that you put the submit handler on a form rather than a button. This is what you should try: create a submit method before your render method. This method should take a parameter 'event' which will then call the prevent default method before calling the methods to send form data to store.
criteriaSubmisssion = event => { event.preventDefault(); this.props.onCriteriaFormSubmission() }
Your form OnSubmit method should call criteriaSubmission instead.