-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(ComboBox): add missing dependencies to useEffect #8308
fix(ComboBox): add missing dependencies to useEffect #8308
Conversation
Deploy preview for carbon-elements ready! Built with commit dde99eb |
Deploy preview for carbon-components-react ready! Built with commit dde99eb https://deploy-preview-8308--carbon-components-react.netlify.app |
@@ -150,7 +150,7 @@ const ComboBox = (props) => { | |||
if (onInputChange) { | |||
onInputChange(inputValue); | |||
} | |||
}); | |||
}, [onInputChange, inputValue]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be helpful if we added a useSavedCallback
type hook for these situations? It seems like the effect is parameterized by inputValue
and we wouldn't want to re-run this effect when onInputChange
happens (especially if we're passed a function that's updated each render)
useSavedCallback
could look like:
const savedOnInputChange = useSavedCallback(onInputChange);
// ...
useEffect(() => {
const { current: onInputChange } = savedOnInputChange;
if (onInputChange) {
onInputChange(inputValue);
}
}, [inputValue]);
And the implementation:
function useSavedCallback(callback) {
const ref = useRef(callback);
useEffect(() => {
ref.current = callback;
});
return ref;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I see this optimization as strictly necessary in this case but it may be worth keeping in mind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would useCallback
be enough to memoize onInputChange
and avoid unnecessarily re-running the effect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@emyarod I think it's something that's helpful for us to do. In this case, the effect would synchronize with the input value changing and not the handler changing (although it would need that to correctly dispatch the update).
A totally extreme example of this idea would be if the parent component had a timer:
function Parent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = window.setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
window.clearInterval(intervalId);
};
}, []);
return (
<>
<p>The current count is: {count}</p>
<ComboBox onChange={() => { /* ... */ }} />
</>
);
}
Because we have the effect synchronizing with the callback, we end up with the effect being called repeatedly with the same value. Instead, if the callback is saved and then invoked by an effect parameterized by inputValue
it would only run when the value changes.
Here is a quick demo of this idea: https://codesandbox.io/s/vigorous-goldwasser-rp3fe?file=/src/App.js
Screen.Recording.2021-04-08.at.11.01.58.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't saving the handler in a ref inside the parent function accomplish the same effect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, would need to explore. In this scenario, I'm imagining that the parent would be the person using Carbon so just was illustrating that splitting the effect out in this way would help out in the default case without them having to change how they author their code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something like this I assume https://codesandbox.io/s/pensive-sun-4ijti
given that the event handler is provided by the Carbon consumer, I am not sure about optimizing it for them by default (including useCallback
usage as @tay1orjones suggested). although I think I am less hesitant about baking useCallback
into our component if we are certain we want to optimize the handler no matter what
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to optimizing it now to prevent problems down the line, though if it is an uncommon use case that the majority of consumers won't run into we can point them here (or other docs?) to instruct on how they can fix downstream in their application code.
Having this hook could be useful though, it would apply to any callback placed in a useEffect that has a dependency on one or more variables/props. I'm not sure how common that is in the codebase right now though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely meant the counter example as the extreme case, the big thing is that in a default setup this implementation would:
- Have the effect and handler be called for non-related updates
- The effect would be called because the identity changes for function declarations or arrow functions
- Have the handler be called twice in intended situations like if the parent is using the value from
onChange
to update state- State update triggers a render that then creates a new reference for a function declaration or arrow function, this then calls the effect to be run which is the point above
And it seems like introducing this convention would:
- Make sure the handler is only called when the value is changed and at no other time
- Make sure the handler is only called once per input change
Would it be possible to have a first pass of a fix that only considers the inputValue changing and not the callback function? As a consumer, I only expect In my application, Thanks for looking at this so deeply. I hope that we can figure out a good solution for the long term. |
@nertzy would this thread address what you're describing or would it be another change? 👀 #8308 (comment) |
If I understand it correctly, I think it would work for my use case. As long as I don't know enough about the |
@joshblack @tay1orjones circling back on this, so is the expectation here to include a new |
e6647e6
to
7eaad2b
Compare
@emyarod I don't think you need to do the hook explicitly, but I think saving the callback in a ref and calling it would be great for this PR and then we can get it in 👍 (Like in this pattern: https://github.com/carbon-design-system/carbon/blob/main/docs/style.md#hooks-that-use-a-callback) |
8f4df8b
to
f9efcc7
Compare
f9efcc7
to
1ced73f
Compare
Closes #8306
This PR adds missing dependencies to the Combobox
useEffect
to avoid a render loop when the component is controlled and rerendered on input changesTesting / Reviewing
Substitute the Combobox component in the example code sandbox with a built version of the component from this PR, and observe that removing the setTimeout in the example app no longer causes the component to render infinitely