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.
Related
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.
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.
I've got a simple login dialog that uses redux (just the dialog is shown below for reference). Each time the user types a character into either of the input fields a state change is fired through redux and when the button is pressed a state change also fires.
I plan on changing the state directly when the button is pressed to be something like "LoginPending", and when the REST call returns, the state will change to either "LoggedIn" or "LoginFailed".
My current plan is to add an if statement to the MapStateToProps method that check the old LoginPending status to see if that changed, and if it did, then dispatch the proper action (In reality I will execute a toast notify message).
My question is, "Is this the proper way to check for something like "logged in""? It seems awkward to do it in MapStateToProps, but since there are so many state changes happening (because of the onchange in the input elements), I can't think of a better place to do it.
class App extends Component {
....
render() {
return (
<div className="App">
<input onChange={this.handleChange('username')}/><br/>
<input onChange={this.handleChange('password')}/><br/><br/>
<button onClick={this.handleClick}>Login</button>
</div>
);
}
}
There are several ways to do it. One of the most popular way is to use redux-thunk. docs and example / official recommendation
Then all the logic will reside on the action creator:
export const exampleAction = (username, password) => (dispatch, getState) => {
dispatch({type: "exampleLoading"}); // the store sets a state variable so the view can render something while this "loading" state is true
doApiCall(username,password).then(response => {
// here the feedback message is added on the store, so my feedback manager can render the new feedback message
dispatch({type: "addFeedback", payload:{message:"success!", type:"success"}});
// here the store cleans up the "loading" variable and uses the api response if needed
dispatch({type: "exampleLoaded", payload: response});
});
}
I have a React component that dispatches a redux state change in its componentWillMount function. The reason is that when the component is loaded, it needs to get the id from the url (powered by react-router), and trigger an action that sets up the state with that id's data.
Here is the component:
class Editor extends React.Component {
componentWillMount() {
const { dispatch, params } = this.props
dispatch(editItem(params.id))
}
render() {
const item = this.props.item
console.log("Editing", item)
}
}
export default connect(state => ({item: state.item}))(Editor)
Here's the catch: render is getting called twice. item is undefined on the first call, and valid on the second. Ideally, it should only be called once this.props.item actually exists (after the editItem action has been dispatched and run).
According to the React docs: "If you call setState within this method, render() will see the updated state and will be executed only once despite the state change."
In redux, dispatch is the equivalent of calling setState, as it results in a state change. However, I'm guessing something in the way connect works is still causing render to be called twice.
Is there a way around this besides adding a line like if (!item) return; ?
One thing you might do is create a higher order component that handles the basic pattern of loading a different component (or no component) before the required props are loaded.
export const LoaderWrapper = function(hasLoaded, Component, LoaderComponent, onLoad) {
return props => {
if (hasLoaded(props)) {
return <Component {...props} />
}
else {
if (onLoad) onLoad(props)
return { LoaderComponent ? <LoaderComponent /> : null }
}
}
}
Then you can wrap your component before connecting it to get the desired behaviour.
export default connect(state => ({item: state.item}))(LoaderWrapper(
((props) => !!props.item),
Editor,
null,
(props) => props.dispatch(editItem(props.params.id))
))
You might want to add some currying magic to make sure you can compose these kinds of wrapper functions more nicely. Take a look at recompose for more info.
It looks like there's already an issue in the react-redux library.
https://github.com/rackt/react-redux/issues/210
What does editItem do? Does it add item to the redux state or is it there already?
If it is adding I imagine what is happening is that a render cycle happens with the current props, ie item being blank.
Then it gets rendered again when the props have changed, via setting the item.
One approach to fixing this sort of thing is to create a higher order component that wraps Editor and calls the dispatch action the rendering though is set either to a loading screen or and empty div until item is set. That way you can be assured that Editor will have an item.
But without knowing what editItem does it's sort of hard to know. Maybe you could paste the code for that?
I occasionally have react components that are conceptually stateful which I want to reset. The ideal behavior would be equivalent to removing the old component and readding a new, pristine component.
React provides a method setState which allows setting the components own explicit state, but that excludes implicit state such as browser focus and form state, and it also excludes the state of its children. Catching all that indirect state can be a tricky task, and I'd prefer to solve it rigorously and completely rather that playing whack-a-mole with every new bit of surprising state.
Is there an API or pattern to do this?
Edit: I made a trivial example demonstrating the this.replaceState(this.getInitialState()) approach and contrasting it with the this.setState(this.getInitialState()) approach: jsfiddle - replaceState is more robust.
To ensure that the implicit browser state you mention and state of children is reset, you can add a key attribute to the root-level component returned by render; when it changes, that component will be thrown away and created from scratch.
render: function() {
// ...
return <div key={uniqueId}>
{children}
</div>;
}
There's no shortcut to reset the individual component's local state.
Adding a key attribute to the element that you need to reinitialize, will reload it every time the props or state associate to the element change.
key={new Date().getTime()}
Here is an example:
render() {
const items = (this.props.resources) || [];
const totalNumberOfItems = (this.props.resources.noOfItems) || 0;
return (
<div className="items-container">
<PaginationContainer
key={new Date().getTime()}
totalNumberOfItems={totalNumberOfItems}
items={items}
onPageChange={this.onPageChange}
/>
</div>
);
}
You should actually avoid replaceState and use setState instead.
The docs say that replaceState "may be removed entirely in a future version of React." I think it will most definitely be removed because replaceState doesn't really jive with the philosophy of React. It facilitates making a React component begin to feel kinda swiss knife-y.
This grates against the natural growth of a React component of becoming smaller, and more purpose-made.
In React, if you have to err on generalization or specialization: aim for specialization. As a corollary, the state tree for your component should have a certain parsimony (it's fine to tastefully break this rule if you're scaffolding out a brand-spanking new product though).
Anyway this is how you do it. Similar to Ben's (accepted) answer above, but like this:
this.setState(this.getInitialState());
Also (like Ben also said) in order to reset the "browser state" you need to remove that DOM node. Harness the power of the vdom and use a new key prop for that component. The new render will replace that component wholesale.
Reference: https://facebook.github.io/react/docs/component-api.html#replacestate
The approach where you add a key property to the element and control its value from the parent works correctly. Here is an example of how you use a component to reset itself.
The key is controlled in the parent element, but the function that updates the key is passed as a prop to the main element. That way, the button that resets a form can reside in the form component itself.
const InnerForm = (props) => {
const { resetForm } = props;
const [value, setValue] = useState('initialValue');
return (
<>
Value: {value}
<button onClick={() => { setValue('newValue'); }}>
Change Value
</button>
<button onClick={resetForm}>
Reset Form
</button>
</>
);
};
export const App = (props) => {
const [resetHeuristicKey, setResetHeuristicKey] = useState(false);
const resetForm = () => setResetHeuristicKey(!resetHeuristicKey);
return (
<>
<h1>Form</h1>
<InnerForm key={resetHeuristicKey} resetForm={resetForm} />
</>
);
};
Example code (reset the MyFormComponent and it's state after submitted successfully):
function render() {
const [formkey, setFormkey] = useState( Date.now() )
return <>
<MyFormComponent key={formkey} handleSubmitted={()=>{
setFormkey( Date.now() )
}}/>
</>
}
Maybe you can use the method reset() of the form:
import { useRef } from 'react';
interface Props {
data: string;
}
function Demo(props: Props) {
const formRef = useRef<HTMLFormElement | null>(null);
function resetHandler() {
formRef.current?.reset();
}
return(
<form ref={formRef}>
<input defaultValue={props.data}/>
<button onClick={resetHandler}>reset</button>
</form>
);
}