When a user submits a form, I have to show a modal asking them to connect if they haven't yet. On success, the modal sets the user data in a hook, and then I continue with the submission flow.
So there are 2 conditions to submit:
User submits form (clicks button)
User data is set
I solved it reactively, using an effect:
useEffect(() => {
async function nestedAsync() {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
await submit(
formData, // simplified - this is actually several hooks
userData
);
}
}
nestedAsync();
}, [pendingSubmitIntent, userData]);
And the submit click handler:
setPendingSubmitIntent(true);
if (!userData) {
setShowConnectModal(true);
}
The modal is in the component:
{setShowConnectModal && (
<ConnectModal
setUserData={setUserData}
/>
)}
This actually works, but I'm getting a warning that I'm not declaring formData in the dependencies array. I can't do this, because otherwise the effect will be called when editing the inputs and that's not correct. The effect has to be called only when submitting.
And this warning makes me think that there's something fundamentally wrong with this approach. A state machine comes to mind, but I feel that it should be simpler. Any ideas?
While it may be tempting to disable the linter warning for your dependencies, I would suggest not doing that and instead look at alternatives. By disabling the linter, any future updates to your useEffect callback can cause you to miss dependencies that you might actually need to include, leading to bugs with stale values. Below are some alternatives to consider.
Removing the useEffect()
I would reconsider the need for the useEffect() hook in the first place. From the user's perspective, their form data should be saved once they submit the form, or once they click "connect" on the subsequent modal. Both of these actions are user-triggered interactions, and so the saving logic should sit within these interactions' event handlers, and not within an effect. At the moment, your effect serves the purpose of sharing shared logic between your event handlers, which is an unnecessary need for an effect as it can be achieved by creating a shared function. You can remove the effect by putting the shared logic into its own function:
function saveData(userData) {
submit(formData, userData); // note, you don't need to `await` something if you don't need the function to wait for it to complete
}
This shared function can now be used by both of your event handlers - the one that handles the submission of the form itself, and the other that handles the connection and population of your user data from within ConnectModal. By using this function, you can "continue" your submission flow from both the modal connect event and the original form submission event. This would mean updating your form submission handler to use this shared function like so:
// Submit click handler
if(userData) {
saveData(userData);
} else {
setShowConnectModal(true);
}
It also means updating your ConnectModal event handler that sets your userData to call the saveData function. This can be done by passing through a new prop to ConnectModal, eg:
<ConnectModal
onConnect={saveData}
setUserData={setUserData}
/>
Within your modal component, you can then call onConnect(...) with your userData that you call setUserData(...) with (note, as you haven't shown how userData is actually set, I assume you have an event handler in ConnectModal that controls this, the below function call to onConnect() would sit in there also):
// Connect click handler within ConnectModal
onConnect(newUserData);
Giving formData a "stable identity"
Rather than storing formData as a state value, you can look at giving it a stable identity with the useRef() hook. A stable identity means that React will always returns the same ref object on every rerender. When an object has this property, then you don't need to include it in your dependency array. Moreover, including it in the dependency array doesn't hurt either as the object isn't changing, so it won't cause the effect to execute. If formData doesn't need to influence your UI, then you can swap out its state value with a ref, allowing you to omit it as a dependency:
const formDataRef = useRef(...);
// When you need to set your formData you would use `formDataRef.current = ...`
useEffect(() => {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
submit(formDataRef.current, userData); // use the ref's `.current` property which holds your formData
}
}, [pendingSubmitIntent, userData]); // no need to specify the ref as it's stable accross renders
Using the useEvent() hook
While it most likely isn't needed for your case as you can remove the effect (or use useRef()), there might be legitimate cases where you need an effect to run on a change of a particular value, but the dependency values that the linter is asking you to add will cause your effect to run too often. In such a case, you can consider using the useEvent() hook (it's currently experimental, but you can add a shim/polyfill for it if needed). For example, you could do:
const onSubmit = useEvent((userData) => {
submit(formData, userData);
});
useEffect(() => {
if (userData && pendingSubmitIntent) {
pendingSubmitIntent(false);
onSubmit(userData);
}
}, [pendingSubmitIntent, userData]);
Now the need for specifying formData within your effect is no longer needed as it's no longer used within the effect callback. The function provided to useEvent() also has access to the "latest" value of formData from the surrounding scope. Moreover, the onSubmit function returned by the useEvent() call is always the same, so it's stable. This means that it doesn't need to be provided as a dependency to the useEffect() hook call as you don't risk referring to the wrong onSubmit function. You can find more info about useEvent() here.
There are some great articles on React’s new beta documentation site relating to the above suggestions and how to best tackle your issue in different scenarios:
Removing Effect Dependencies
You Might Not Need an Effect
Separating Events from Effects
If the user already connected you can continue with the submission in the submitClickHandler.
const submitClickHandler = useCallback(()=>{
if(!userData) {
setShowConnectModal(true)
return
}
(async()=>{
if(userData && formData){
await submit(userData && formData)
}
})()
},[userData,formData])
Otherwise in the ConnectModal you will submit the form
First pass the submit and 'formData' to ConnectModal
{showConnectModal && (
<ConnectModal
setUserData={setUserData}
submit={submit}
formData={formData}
/>
)}
Then set the userData and submit the form in a useEffect hook
useEffect(()=>{
setUserData(()=>userData)
(async()=>{
if(userData && formData){
await submit(userData && formData)
}
})()
},[userData,formData])
finally close the modal
Related
I was doing some coding on React, and encountered an issue I would like to properly deal with. Details on the matter are provided below.
The Environment
Suppose you have a component FormDemo made to handle a potentially complex form, parts of which involve the dynamic management of certain input fields. As an example, the provided code sample allows to create any amount of fields for names between 0 and (232 - 1) fields due to JavaScript's limitations on array length.
Press Add New Name button above all name fields to append another name field. Press Remove button to the right of any input to delete it from the list.
Each name input created is handled by a separate component SubForm that takes three properties:
id: a unique generated identifier of the current field.
onChange: a function executing whenever the value of that input was changed.
onRemove: a function executing whenever the Remove button of that form was clicked.
The Sample
Here is a working sample of a code I've made on CodeSandbox provided for demonstration purposes.
The Problem
The approach used in the code sample works, but it has the eslint problem mentioned in Problems tab of CodeSandbox, and I am aware that it's not a CodeSandbox issue, as I've tested the same project in my local environment and got the same problem. Here are that problem's details taken right from the console:
React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback. (react-hooks/exhaustive-deps)
Following the advice from the problem directly (i.e. adding onChange to dependency list of SubForm's useEffect) results in infinite rendering, and thus is not a solution to the problem.
The Research
After some reading of the official React docs on useCallback, as well as the other part of these on useEffect, I've figured out that, when rendering a component, React creates new instances of functions declared in a component's body. Therefore, adding such functions to a dependency list of some useEffect hook that has an effect function attached to it will entail that function being called on each render.
In my approach, I pass update function to SubForm component in onChange property as a reference (proven here by React docs), hence the SubForm component's onChange property has exactly the same instance of the update function as the parent component. So, whenever the instance of the update function changes with it added to the dependencies of a useEffect hook, that executes the effect function attached to it, and, taking the above into account, this happens on each render of a parent component FormDemo.
The update function changes the value of forms, a state variable of FormDemo component, causing it to rerender. That recreates the instance of an update function. The SubForm component gets notified of that change and executes an effect function attached to a useEffect hook, calling the update function once again. In turn, this causes another change of a state variable forms, telling the parent component FormDemo to render again... and this continues indefinitely, creating an infinite loop of renders.
Some of you may ask why does it happen if an input field of the form was not changed between these two renders, thus the value passed to update function is effectively the same as before. After some testing, which you can try yourself here, I came to the conclusion that it's actually wrong: the value set to forms is always different. That's because even though the object's content is exactly the same, its instance is different, and React compares object instances instead of their contents, sending a command to rerender the component if these instances differ.
As useCallback hook memoizes the instance of the function between renders, recreating the function only when the values or instances of its dependencies change, I've assumed that wrapping update function in that hook will solve the original problem, because the instance of the function will always stay the same.
However, wrapping the update function in useCallback will result in another problem: I will need to add forms as a dependency, because I'm using it inside that function. But, taking the above into account, this will bring me the original problem back due to the instance of forms being different after each update, and that will command useCallback to recreate the instance of the function, too.
Potential Solution
With all that being said, I have a solution that I don't quite like, even though it works because it removes the need of adding the state variable forms to the list of dependencies of useCallback:
const update = useCallback((id, value) => {
setForms(prevState => {
const { form_list } = prevState,
new_forms = [...form_list],
mod_id = new_forms.map((e) => e.id).indexOf(id);
new_forms[mod_id] = value;
return { ...prevState, form_list: new_forms };
});
}, []);
So why am I against it, if it works and gives no problems in the console?
In my humble opinion (feel free to prove me wrong), because of these issues:
Direct usage of state setter function instead of a dedicated middleware function. This decentralizes direct state management.
Duplication of an original array, which may be expensive on memory if an array has a lot of values inside, not to mention that each value itself is an object.
The Question
What is the most memory-efficient and readable solution of the stated problem in the provided case that will use a middleware function setField? Alternatively, if it's possible to debunk my issues with a potential solution, prove that it's the best way to go.
Feel free to modify the contents of setField if necessary for the solution and remember that I'm all open for answering anything related to the question.
It seems you are duplicating state of each SubForm: you store it in parent and also in SubForm, why not store state only in parent and pass as props?
I am talking about something like this:
const SubForm = ({ id, form, onChange, onRemove }) => {
return (
<Form>
<Form.Group controlId={`form_text${id}`}>
<Form.Label>Name (ID {id})</Form.Label>
<InputGroup>
<Form.Control
type="text"
value={form.name}
onChange={(e) => onChange(id, { ...form, name: e.target.value })}
/>
<Button variant="danger" onClick={() => onRemove(id)}>
Remove
</Button>
</InputGroup>
</Form.Group>
<br />
<br />
</Form>
);
};
To pass each form data just do:
<SubForm key={e.id} form={e} id={e.id} onChange={update} onRemove={remove} />
No need for useEffect anymore.
You probably want to separate the management of IDs from SubFrom. SubForm shouldn't be able to change it's ID.
Wrap the update & remove functions - so SubForm doesn't need to send the ID back.
<SubForm key={e.id} id={e.id}
onChange={(form) => update(e.id, form)}
onRemove={() => remove(e.id) } />
Make sure that SubForm will not change ID as part of form
const update = (id, value) => {
setField(
"form_list",
// subform shouldn't change id, so we overriding it (to be sure)
forms.form_list.map(e => e.id===id?{...value, id}:e)
);
};
You still optionally may pass the ID to the SubForm, but the management of IDs is separated from it.
Modified code
This is for an open-source project called react-share, and their ShareButton component has a prop called beforeOnClick that you can pass to it. I'm using beforeOnClick to upload an image to our CDN so that we don't needlessly upload images that don't get shared, which causes the url prop passed to the button to update.
My current problem is, after beforeOnClick runs, the share button currently doesn't handle the updated url prop.
Basically, I have an async function that looks something like this:
const handleClick = async () => {
const { url, disabled, beforeOnClick } = this.props;
// beforeOnClick can cause this.props to change. beforeOnClick can also perform async operations, like making a fetch call
if (beforeOnClick) {
await beforeOnClick();
// call setTimeout to delay the next handleClick call in order to ensure this.props
// properly reflects changes from the parent component
setTimeout(handleClick);
return;
}
// Do stuff with url & disabled
};
I dumbed it down for the sake of keeping the question simple, but if you'd like to view the code I currently have, check out my fork. compare to the original.
Is setTimeout a reliable way to achieve this effect? Or, should I do something like this instead:
this.setState({ __rerender_component: true }, handleClick);
I'm not a huge fan of that solution, as I'd have to manage resetting that flag after the callback is run. Any thoughts are appreciated!
EDIT: Using setTimeout seems to work, but I'm not sure if it's reliable. If it fails 1/100 times, that sucks.
It might be easiest and feels more "reacty" to use setState to have a local copy of the props and let the beforeOnClick function use setState?
eg (beware, I have been using hooks only on my latest projects, so might be off)
const handleClick = async () => {
this.state = this.props; // can all props be changed?
if (beforeOnClick) {
await beforeOnClick(this.setState);
// Do stuff with this.state.url & this.state.disabled };
and beforeOnClick can use setState to change the url and others.
instead of giving full control to setState, you might want to have a different approach:
let newState= await beforeOnClick();
if (newState && newState.url && !newState.url.startsWith("http"))
throw 'url must start with http';
// that might be a wrong assumption, take it as an example
// whatever else you want to check, like disable is a boolean...
this.setState({...state, ...newState});
I am trying to use react hooks to make a Table component that displays rows of data from an API based on a set of filters that the user can choose. I want to make a new call to fetch data whenever the user clicks an 'Apply Filters' button, not when the user makes changes to the filters.
I am using context to manage the 'filters' state and a 'lastFetched' state which tracks when the user last clicked the 'Apply Filters' button (as well as other states on the page). Updates to the context are made via the useReducer hook and its dispatch method (see here).
The data fetching occurs in a useEffect hook that reruns whenever the 'lastFetched' state changes. This appears to be working correctly; however, the effect references other values from the context (i.e. the filters) that are not included in the dependencies. I am aware of the exhaustive-deps eslint rule, and I am concerned that I am not handling the hook's dependencies correctly.
const Table = () => {
const [context, dispatch] = useTableContext(); // implemented with createContext and useReducer
const { filters, lastFetched } = context;
useEffect(() => {
if (!filters.run) {
return;
}
dispatch({ type: 'FETCH_DATA_BEGIN' });
const params = convertContextToParams(context); // this is lazy, but essentially just uses the the filters and some other state from the context
API.fetchData(params)
.then((data) => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data.results });
})
.catch((e) => {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: e.response.data.message });
});
return () => { ... some cleanup... };
}, [lastFetched]); // <== This is the part in question
return <...some jsx.../>
};
Again, this appears to be working, but according to the react docs, it seems I should be including all the values from the context used in the hook in the hook's dependencies in order to prevent stale references. This would cause the logic to break, since I don't want to fetch data whenever the filters change.
My question is: when the user clicks 'Apply Filters', updates context.lastFetched, and triggers the useEffect hook, will the hook be referencing stale filter state from the context? If so, why? Since the effect is rerun whenever the button is clicked, and all the state updates are done via a reducer, does the usual danger of referencing stale variables in a closure still apply?
Any guidance appreciated!
Note: I have thought about using useRef to prevent this issue, or perhaps devising some custom async middleware to fetch data on certain dispatches, but this is the solution I currently have.
I am not an expert but I would like to provide my takes. According to my understanding of how Context works, you will not get stale filter data with the current implementation. useReducer updates the state with a new object which will trigger Table to be re-render.
Also, Table component doesn't really care about filter data unless lastFetched is changed by a click event. If lastFetched is changed, all the Consumer of TableContext will be re-render again. You should not get stale filter data either.
Update: Here is an example Pen
https://codepen.io/anon/pen/vwzGYY?editors=0011
Preface
Based on my research, it seems like I need a completely different approach. Maybe you can suggest one?
Context
I'm using a Redux-Form (technically an older version, but the API's
in question seem really stable. We can burn that bridge when we get there.) to set some "filters" for a sort of search results list.
In particular, since I want the pages to be link-able, I'm also setting the form content in the URL query params, via React-Router, or initially setting it on page load via similar mechanism.
The only field so far is "organization_name", a text field, used to set the query param value, and trigger an API request for /endpoint?name={some_name}.
E.g.,
<Field
name="organization_name"
component="input"
type="text"
placeholder="Organization Name"
value={value}
/>
I've tried several things, but here's a recent shot:
I'm grabbing reset, change, and other things from default props. I'm passing in a handleSubmit as required.
handleSubmit works correctly, to do some state updating, set/push the URL query params with React Router, and then make a new API call/update display of new results! Woot!
What I want / expect
In the long run, I would like a "reset filters" button that sets all filter values back to defaults (e.g., set the "name" value to empty string), and re-submits the form (thus triggering handleSubmit).
What I first tried to implement was a button, as such:
<button
name="reset_filters_button"
type="button"
onClick={resetAndSubmit}
disabled={pristine || submitting}
>
Clear Search
</button>
Where resetAndSubmit is defined on the form container as such:
const resetAndSubmit = event => {
reset();
handleSubmit();
};
What actually happens... (submit takes precedence over dispatched events?)
Using the Chrome dev tools debugger, I can clearly see that the reset method is called, and returns it's dispatch(...)'d event. However, the form and state values are not updated before handleSubmit() runs and submits the form.
I think this might have to do with the submit event taking priority?
I have also tried something janky, like importing change (default prop for the container) and defining the reset button thus:
<button
name="reset_filters_button"
type="button"
onClick={() => {
change('organization_name', '');
methodThatDispatchesSubmitAction();
}}
disabled={pristine || submitting}
>
Clear Search
</button>
Which (if I remove methodThatDispatchesSubmitAction()) works correctly to set the field value back to blank, making the form technically "pristine" again as well.
methodThatDispatchesSubmitAction() (if it's not obvious) is bound on the parent via dispatchToProps, and passed in to the form container, where it uses the "remote submit" suggestion, e.g,
// organization_list_filter == name of the Redux-Form to submit.
dispatch(submit('organization_list_filter'));
TL;DR and final question:
How does one properly reset a form and submit its' default/empty values?
Every time I dispatch or directly call Redux Form 'submit', it ends up submitting the form before clearing values from state, or the UI. I have walked through this with a debugger and it's not skipping my call to reset or change. It's like an async/race issue, but I admit I am out of my league in this particular case for sure.
Am I just Straight Up Doing It Wrong?
It is most definitely a race condition issue (or since we aren't actually dealing with threads, an order of events issue).
The reason using a methodThatDispatchesSubmitAction works when your current example does not, is because a dispatched action has the benefit of reading data directly from the redux store. Your example is not reading from the redux store, it's reading from a property that is passed in. Yes, this property comes from the redux store, but the problem you are seeing is that it hasn't been updated in your component yet.
Bear with me as this next piece is not going to be entirely accurate but it should suffice to explain what you are seeing.
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is fired using values prop (old state data still)
Component is updated with new props from redux state
As you can see, the order of events don't allow for an updated state to be given to the property until our click code has finished running. If you've ever watched a video on the JS Event Loop (I highly recommend it), you'll know that our onClick handle will run in full before any other async operations (or sync operations that come after our click) have a chance to run.
There are good reasons why Components aren't given updated props right away but the primary one is performance. You can see that this order is in fact the problem by wrapping the handleSubmit in an async event that fires immediately (it doesn't actually fire immediately, all other sync/async operations queued before it will finish).
const resetAndSubmit = (event) => {
reset();
setImmediate(() => handleSubmit());
}
This changes the order of events as follows:
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is queued on the event loop (not run yet)
Component is updated with new props from redux state
Event loop reaches queued code and runs is
-> Handle submit is fired using values prop (new state data)
Hopefully, this helps you understand why the problem is occurring. As for solutions to fix it. Obviously, you can queue the handle submit as I've shown above. Another option would the one you've described as using a dispatch to perform the submit. A third option would be to use something a bit heavier like redux-thunk or redux-sagas that tie the resetAndSubmit action into a single dispatch. Although honestly, this is the same as option two, just reduced into a single dispatch. Option four, don't use redux for all your data. Obviously, this fourth option comes with trade-offs but my point being, just because you are using redux in a project doesn't mean every single piece of data needs to be in redux. Though it completely defeats the purpose of redux-forms.
I should also add, you are not alone in being confused by this. When you introduce redux, it messes with how you traditionally think about working with code. Normally you think, I do A then B. But with redux, you do A, wait for A's changes to make it through the system, and then you do B. That's where Sagas or Thunks can be nice. You move more logic to the store to act on the dispatch rather than wait for it to all make its way back down to a component via props.
I have a custom input validation component that I use in a form. Something like 15 instances of this component around the app. It has a beforeDestroy method in which I unsubscribe from global event called triggerGlobalValidation which triggers validation before I send request to server. As expected it's triggered only once inside this certain component.
There is a container with v-if parameter which contains one instance of the component. So when v-if="false" I expect this certain component to unsubscribe from event and get destroyed. It goes well accept for one thing: somehow this component unsubscribes ALL other instances of it from the triggerGlobalValidation event as well.
I've tested the behavior with v-show and it works as expected - all other instances keep subscribed, but since the v-show field is required for the form it's blocking validation even without being shown in the DOM. I also tested above mentioned components behavior by removing the this.$root.$off("triggerGlobalValidation") and it also works as expected + polluting the global root.
Vue documentation on $off method is saying:
If no arguments are provided, remove all event listeners;
If only the event is provided, remove all listeners for that event;
If both event and callback are given, remove the listener for that
specific callback only.
Is it possible to somehow mention in the callback, that this $off method shouldn't unsubscribe all of its instances from the event, but just this certain one being destroyed?
Check it out in codesandbox
As answered in the issue, you need to save the handler and pass it again to $off
mounted() {
this.fn = () => {
this.toggleWarning();
}
this.$root.$on("triggerChildComponents", this.fn);
},
beforeDestroy() {
this.$root.$off("triggerChildComponents", this.fn);
},