How to make useEffect rerun only on change of a single dependency? - javascript

I have created a custom input component that has an attached 'edit/submit' button, like so:
...........................
I want the onChange handler passed to it to fire only when user hits the submit button i.e. (✓) and not on every keypress. So, in useEffect I did:
useEffect(() => {
if (!editMode) { // only on submit
onChange(value) // "value" is a state of my component
}
}, [editMode])
return (
<div className='my-input'>
<input value={value} onChange={({target}) => setValue(target.value)}/>
<Icon
name={editMode ? 'tick' : 'pencil'}
onClick={() => setEditMode(editMode => !editMode)}
/>
</div>
)
But the linter starts to bang:
useEffect has missing dependencies: 'onChange' and 'value'...
How to fix this issue or am I using an incorrect hook here?

The warning you're seeing is because your useEffect function doesn't depend on a key press, it depends on the value of onChange and value. A better way to accomplish this might be to send the event inside of the onChange handler itself:
const onClick = React.useCallback(() => {
const nextEditMode = !editMode
setEditMode(nextEditMode)
// If we're flipping editMode on, fire the event.
if (nextEditMode) {
onChange(value)
}
}, [value, onChange])
useEffect should be used when you can only represent the thing you want as a side-effect, but there's no need to do that in this case; this can handled in event handler, and it is easier to read when you do so.

Related

"Reset" button won't work unless it refreshes page, and gives me an error otherwise (React.JS)

So I'm working in React.JS, and I'm trying to make something like an input field, where a user can enter a number of their choice, and it will show up in the text.
I decided to add a "Reset to zero" button, as an extension.
<div>
Count: {this.state.value}
<form>
<input type="number" value = {this.state.value} onChange = {this.inputChange}/>
<br/>
<button onClick = {this.reset}>reset to zero</button>
</form>
</div>
It works, but it refreshes the page every time it does so.
I read online, and I decided to add "type=button" to my button as so:
<button type="button" onClick = {this.reset}>reset to zero</button>
When I run my code again, it still increments fine, but when I click the button, nothing happens, and when I try to increment it again, I get an error, "TypeError: this.setState is not a function".
The error is coming from this method:
inputChange = event => {
this.setState ({value: event.target.value})
}
I know where the error is coming from, but I don't know why it happened, or how to fix it (note that I'm also a beginner at JavaScript and React.JS)
I hope someone can help me.
Here's my code in entirety, for reference.
class Slider extends React.Component {
constructor(props){
super(props);
this.state = {
value: 0
}
}
inputChange = event => {
this.setState ({value: event.target.value})
}
reset = () => {
this.setState = ({
count: 0
})
}
render() {
return (
<div>
Count: {this.state.value}
<form>
<input type="number" value = {this.state.value} onChange = {this.inputChange}/>
<br/>
<button type = "button"onClick = {this.reset}>reset to zero</button>
</form>
</div>
);
}
}
Thank you guys, in advance.
The reason nothing happens on reset and you are getting that error on input change, is that you are reassigning this.setState in your reset function rather than calling it. Also, you are setting count instead of value, which would lead to the wrong state being set.
This is what your reset function should be:
reset = () => {
this.setState ({
value: 0
})
}
When you call this.setState, React will trigger a re-render in your component with the new state.
That is currently not happening when you click reset. On your subsequent call to inputChange, this.setState has been reassigned with an object, and is no longer callable, throwing that error.
Try replacing your button like this:
<button type = "button"onClick = {this.reset.bind(this)}>reset to zero</button>
This will force the method to execute being this the scope.

search results are dislayed on second click n not for the first react js

M doing asearch what happens is search results are dislayed on second click n not for the first react js i dont unstand whats happenindg on the second click
this is not dynamic have used statiic value
any help is appreciated
handleChange(e) {
this.setState({ value: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
this.setState({
listArr: this.props.list.filter(e1 => e1 === this.state.value),
});
this.props.searchItem(this.state.listArr);
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Search..."
onChange={this.handleChange}
/>
<input type="Submit" value="sech" />
</form>
</div>
);
}
what is it displaying onto second click
setState is asynchronous, so after you set your state other parts of your function can't catch up. Try with a callback with setState.
handleSubmit(e){
e.preventDefault();
this.setState({
listArr:this.props.list.filter(e1=>
(e1===this.state.value))
}, () => this.props.searchItem(this.state.listArr) );
}
Update after comments
As I tried to explain, setState is asynchronous. It does not update the state immediately. First, you set your state with filtering. But when you invoke searchItem function, setState is still trying to update the state, it does not finish its job. So, your listArr is still empty or not updated at that time. Hence, your search does not work.
But why does it work in the second time? Because when you hit the button for the second time setState has already finished its job, there is actually a listArr now. So, your search works.
Here, callback function in setState waits for the update, then invokes what you provide to it.

Trigger change event for React rendered input (type=range)

"react": "^15.4.2"
I am having trouble triggering the change event from jquery for an input rendered with React.
I need to do this for some end to end testing.
here's a sandbox as a full showcase: https://codesandbox.io/s/pp1k1kq3om
with jquery already loaded.
If you run : $('#reactSlider').val(50).change() in the console, you'll notice that the change handler isn't called.
In the sandbox above there's also a pure html slider for comparison, that one works as expected (change function is called).
also tried to dispatch a new event like this:
var event = new Event('change')
var target = $('#reactSlider')[0];
target.dispatchEvent(event);
The above doesn't work either (and does work on the pure html slider).
import React from "react";
import { render } from "react-dom";
const changeHandler = event => {
alert(`react slider changed:${event.target.value}`);
};
const App = () => (
<div>
<label htmlFor="reactSlider">react-slider</label>
<input
id="reactSlider"
type="range"
onChange={changeHandler}
min={10}
max={100}
step={10}
value={20}
/>
<p>run in the console: $('#reactSlider').val(50).change()</p>
<br />
<br />
<p>Notice that for the React slider, the change event is not triggered.</p>
</div>
);
render(<App />, document.getElementById("root"));
Ok, so I've found sort of a workaround.
it looks like the onInput event can be triggered just fine. In the case of input type='range' the "input" event is triggered in the same situation as the "change" is so I was able to switch it.
Workaround would be: use onInput instead of onChange and trigger it like this:
function triggerChange(selector,evt) {
let ev = new Event(evt, { bubbles: true });
let el = $(selector)[0]
el.dispatchEvent(ev);
};
triggerChange('#reactSlider','input');

onClick function is running on another event listener

Have been playing around with react. Have two event listeners the input which listens onChange and the button which should push the value to the array when its clicked.
Here's the code:
import React, { Component } from 'react';
let arr = [];
class App extends Component {
constructor() {
super();
this.state = {text: 'default'}
}
update( e ) {
this.setState({text: e.target.value})
}
add ( value ) {
arr.push(value)
console.log(arr)
}
render() {
return (
<div>
<h1>{this.state.text}</h1>
<input onChange={this.update.bind(this)}/>
<button onClick={this.add(this.state.text)}>Save</button>
</div>
)
}
}
export default App
The problem that the add function is running on change. Can't really get why.
Any suggestions?
onChange() triggers update()
update() calls this.setState() which changes state.
A state change causes render() to be invoked to re-render according to new state.
Rendering <button onClick={this.add(this.state.text)}>Save</button> invokes add() every time render() runs.
In order to defer invoking add(), you can define a function which gets triggered by the click event, as was shown in another answer. Alternatively, you can achieve the same functionality by adding a class method which encapsulates the trigger functionality:
addText() {
this.add(this.state.text)
}
render() {
…
<button onClick={this.addText.bind(this)}>Save</button>
This may or may not work for you, but in the context of the example, given, this would work.
Change <button onClick={this.add(this.state.text)}>Save</button>
To <button onClick={() => this.add(this.state.text)}>Save</button>
In your variant function add firing when component is rendering, and when you call setState with onChange of input you call this re-render.
The problem is add(this.state.text) is called whenever render() is called. To avoid this, you do not need to send the state as parameter, all you need to do is
<button onClick={this.add}>Save</button
or if you want to send a parameter you should bind it
<button onClick={this.add.bind(this, this.state.text)}>Save</button>

Using debouncer with React event

I have an onchange event for a field that needs to be debounced, I'm using underscore for that, however when I use the debouncer the event that is passed to the React handler appears to be out of date.
<div className='input-field'>
<input onChange={_.debounce(this.uriChangeHandler.bind(this), 500)} id='source_uri' type='text' name='source_uri' autofocus required />
<label htmlFor='source_uri'>Website Link</label>
</div>
uriChangeHandler(event) {
event.preventDefault();
let uriField = $(event.target);
let uri = uriField.val();
this.setState({
itemCreateError: null,
loading: true
});
this.loadUriMetaData(uri, uriField);
}
I'm getting this error:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're calling preventDefault on a released/nullified synthetic event. This is a no-op. See https‍://fb‍.me/react-event-pooling for more information.
Using the onchange without the debouncer works fine.
I ended up with a solution I saw on github which worked well for me. Basically you wrap the debounce function in a custom function debounceEventHandler which will persist the event before returning the debounced function.
function debounceEventHandler(...args) {
const debounced = _.debounce(...args)
return function(e) {
e.persist()
return debounced(e)
}
}
<Input onChange={debounceEventHandler(this.handleInputChange, 150)}/>
This got rid of the synthetic event warning
in yout case it might help
class HelloWorldComponent extends React.Component {
uriChangeHandler(target) {
console.log(target)
}
render() {
var myHandler = _.flowRight(
_.debounce(this.uriChangeHandler.bind(this), 5e2),
_.property('target')
);
return (
<input onChange={myHandler} />
);
}
}
React.render(
<HelloWorldComponent/>,
document.getElementById('react_example')
);
JSBin
Also you can use _.clone instead of _.property('target') if you want to get the complete event object.
EDITED
To prevent React nullifies the event you must call event.persist() as stated on React doc:
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.
And hence you could use e => e.persist() || e instead of _.clone
JSBin
I went with a combination of xiaolin's answer and useMemo:
const MyComponent = () => {
const handleChange = useMemo(() => {
const debounced = _.debounce(e => console.log(e.target.value), 1000);
return e => {
e.persist();
return debounced(e);
};
}, []);
return <input onChange={handleChange} />;
};
What I think is happening is that the event is being nullified in the time in between the actual event and when your method gets called. Looking at the _.debounce source code (and using what we know about debouncing functions) will tell you that your method isn't called until 500 milliseconds until after the event fires. So you've got something like this going on:
Event fires
_.debounce() sets a 500 millisecond timeout
React nullifies the event object
The timer fires and calls your event handler
You call event.stopPropagation() on a nullified event.
I think you have two possible solutions: call event.stopPropagation() every time the event fires (outside of the debounce), or don't call it at all.
Side note: this would still be a problem even with native events. By the time your handler actually gets called, the event would have already propagated. React is just doing a better job at warning you that you've done something weird.
class HelloWorldComponent extends Component {
_handleInputSearchChange = (event) => {
event.persist();
_.debounce((event) => {
console.log(event.target.value);
}, 1000)(event);
};
render() {
return (
<input onChange={this._handleInputSearchChange} />
);
}
}
The idea here is that we want the onChange handler to persist the event first then immediately debounce our event handler, this can be simply achieved with the following code:
<input
onChange={_.flowRight(
_.debounce(this.handleOnChange.bind(this), 300),
this.persistEvent,
)}
</input>
persistEvent = e => {
e.persist();
e.preventDefault();
return e;
};
handleOnChange = e => {
console.log('event target', e.target);
console.log('state', this.state);
// here you can add you handler code
}

Categories