Implementing Lodash's debounce in a ReactJS functional component - javascript

I'm trying to debounce text input field change with Lodash's debounce function.
import React from "react";
import debounce from 'lodash.debounce';
const Input = () => {
const onChange = debounce((e) => {
const { value } = e.target;
console.log('debounced value', value)
}, 1000)
return (
<input type="text" onChange={ onChange } />
)
};
The code above throws the following errors:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
Uncaught TypeError: Cannot read property 'value' of null
What is the correct implementation?

When to Use Refs There are a few good use cases for refs:
Managing focus, text selection, or media playback.
Triggering imperative animations.
Integrating with third-party DOM libraries.
Avoid using refs for anything that can be done declaratively.
Refs and the DOM
The way you defined Input, I am assuming it would be used in many places. So, I would do this:
import React from "react";
import debounce from 'lodash.debounce';
const Input = () => {
// Debounced function
// `printValue` only cares about last state of target.value
const printValue = debounce(value => console.log(value), 1000);
// Event listener called on every change
const onChange = ({ target }) => printValue(target.value);
return <input type="text" onChange={ onChange } />;
};

The fix was not to retrieve the value from the event but from the input directly with a ref.
import React, { useRef } from "react";
import debounce from 'lodash.debounce';
const Input = () => {
const input = useRef( null )
const onChange = debounce(() => {
console.log('debounced value', input.current.value)
}, 1000)
return (
<input ref={ input } type="text" onChange={ onChange } />
)
};

If we don't need to rely on any internal component state/props in our onChange handler, we should be able to lift the debounced function call out of the component (to avoid it being recreated on each render), which means we don't need to use refs or useCallback:
import debounce from "lodash.debounce";
import Input from "./Input";
const handleChange = debounce(({ target }) => {
console.log(target.value);
}, 500);
export default function App() {
return (
<div className="App">
<label htmlFor="input-first-name">First name</label>
<Input id="input-first-name" name="firstName" onChange={handleChange} />
</div>
);
}
CodeSandbox: https://codesandbox.io/s/react-input-debounce-feleu9?file=/src/App.js&expanddevtools=1

Related

Is there any way to trigger React.useEffect from different component?

Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);

Debounce on a React functional component when typing not working

I have a form component, and I would like to perform an API request as I am typing. I want to debounce this so I don't spam the server. Here is my code:
export default function MyItem ({ id, name }) {
const debouncePatch = (name) => {
debounce(patchItem.bind(this, id, name), 500)
}
return (
<div>
<form
type='text'
value={name}
onChange={(evt) => debouncePatch(evt.target.value)}
/>
</div>
)
}
The patch item does a simple patch call to the server so that the item's name updates as I am typing. It looks something like this:
export default function patchItem (id, name,) {
return axios.patch(`${MY_BASE_URL}/${id}`, { name })
}
With the debounce it is not working at all. The patchItem function is never called. How can I fix this?
You're calling debounce on every change to the input but it just creates a new debounced function every time. You need to create a single debounced function and then use it as event handler. Like this:
function MyItem({ id, name }) {
let debouncePatch = debounce((id, name) => {
patchItem(id, name);
}, 500);
// OR SIMPLER
let debouncePatch = _.debounce(patchItem, 500);
return (
<div>
<input
type="text"
defaultValue={name}
onChange={(event) => debouncePatch(id, event.target.value)}
/>
</div>
);
}
Also make sure the input gets the defaultValue rather than value so it can be edited.
There's 2 issues here. Firstly, debounce returns a function, so you need to call that one.
Secondly, if you don't memoize the debounced function then it won't work properly, as each re-render will create a new debounced function, and thus defeat the point of debouncing.
Try something like this:
const debouncePatch = useCallback(debounce(() => patchItem(this, id, name), 500), []);
Make your input controllable,
and you can use simple utile use-debounce
import { useDebounce } from 'use-debounce';
export default function MyItem ({ id, name }) {
const [value, setValue] = useState('')
const [debouncedValue] = useDebounce(value, 1000);
const handleChange = useCallback((e) => {setValue(e.target.value)}, [])
useEffect(() => {
console.log(debouncedValue);
*// here you can place any code what you need. **console.log** will be displayed every second, or any time that you may change in hook above.*
}, [debouncedValue])
return (
<div>
<form
type='text'
value={name}
onChange={handleChange}
/>
</div>
)
}
ps. you do not have to use bind in your example for sure. and I am not sure you can apply onChange event to form

useCallback necessary within React.memo?

Consider this example:
import React, { useCallback } from 'react';
type UserInputProps = {
onChange: (value: string) => void;
};
const UserInput = React.memo(({ onChange }: UserInputProps) => {
// Is this `useCallback` redundant?
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" onChange={handleChange} />;
});
export default UserInput;
My questions are:
When the props consist of only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
If we add an additional prop (say a value for the initial value for the <input>), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?
When the props contains only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
It's memoized based on all props, not just onChange. Yes, useCallback is unnecessary there.
If we add an additional prop (say a value for the initial value for the <input>), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?
If you want to update value and not update onChange when updating the input, you'd add value to your props and continue to use useCallback for your handleChange (if you like, so that React doesn't have to swap in a new event handler when it's setting the value; my impression is that's often overkill). E.g.:
const UserInput = React.memo(({ onChange, value }: UserInputProps) => {
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
});
That will use the latest value and reuse the previous handleChange if the onChange prop hasn't changed.
You might not keep React.memo in that case, if you're expecting that value will get changed as a result of the onChange. That is, if you expect the common case to be that value will be different each time your component is called, then React.memo's check of the props is extra work you might not keep:
const UserInput = ({ onChange, value }: UserInputProps) => {
const handleChange = useCallback(
(event) => {
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
};
But you can keep it if you want React.memo to keep checking the props and not call your function when neither prop changes.
If you just want to supply the initial value to your component and then control it locally, you could continue to use React.memo (since it still renders the same thing for the same input props), in which case you don't need useCallback:
const UserInput = React.memo(({ onChange, initialValue }: UserInputProps) => {
const [value, setValue] = useState(initialValue);
const handleChange = (event) => {
setValue(event.target.value);
onChange(event.target.value);
};
return <input type="text" value={value} onChange={handleChange} />;
});
That will only get called when onChange or initialValue changes. You could use also useCallback there in order to, again, only update the onChange on the input when the onChange prop changes, to avoid having React remove the old handler and set the new one when just the value changes:
const UserInput = React.memo(({ onChange, initialValue }: UserInputProps) => {
const [value, setValue] = useState(initialValue);
const handleChange = useCallback(
(event) => {
setValue(event.target.value);
onChange(event.target.value);
},
[onChange]
);
return <input type="text" value={value} onChange={handleChange} />;
});
Side note: One thing to remember is that a new handleChange function is created every time your component function is called, even when you're using useCallback. It has to be, so it can be passed into useCallback as an argument. The only difference is whether you use that new function, or the original one that was created the first time (the result of useCallback). I think the reason for reusing the first one that was created for a given set of dependencies is to minimize changes passed to child components.
When the props consist of only onChange and no other elements, is the useCallback unnecessary in this case, since the entire component is already memo-ed based on onChange?
No, it might be necessary, memo and useCallback don't serve the same purpose.
Without useCallback you may have "heavy computation" on every render:
"heavy computation" refers to a generic case and not to this specific example where you only pass the event's value.
const UserInput = React.memo(({ onChange = () => {} }) => {
// Uncomment for memoization
// Note that you can implement useCallback with useMemo
// const handleChange = useMemo(() => {
// console.log("heavy computation memoized");
// return event => {
// onChange(event.target.value);
// };
// }, [onChange]);
const handleChange = event => {
// Here we can have some heavy computation
// Not related to this specific usecase
console.log("heavy computation on every render");
onChange(event.target.value);
};
return <input type="text" onChange={handleChange} />;
});
If we add an additional prop (say a value for the initial value for the ), then I think useCallback becomes useful, since otherwise handleChange will be recreated even if onChange doesn't change but value is changed. Is that correct?
If you are going to use controlled component (due to use of value prop of input), the initialValue will be initialized once, so it pretty useless trying to memorize it (for what purpose?):
const UserInputWithValue = ({ onChange, initialValue }) => {
// initilized once, no need to memoize
const [value,setValue] = useState(initialValue);
const handleChange = useCallback(
(event) => {
setValue(event.target.value)
onChange(event.target.value);
},
[onChange]
);
// Controlled
return <input type="text" value={value} onChange={handleChange} />;
};

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

React: trigger onChange if input value is changing by state?

Edit: I don't want to call handleChange only if the button has been clicked. It has nothing to do with handleClick. I gave an example in the #shubhakhatri answer's comment.
I want to change the input value according to state, the value is changing but it doesn't trigger handleChange() method. How can I trigger handleChange() method ?
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
value: 'random text'
}
}
handleChange (e) {
console.log('handle change called')
}
handleClick () {
this.setState({value: 'another random text'})
}
render () {
return (
<div>
<input value={this.state.value} onChange={this.handleChange}/>
<button onClick={this.handleClick.bind(this)}>Change Input</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
Here is the codepen link: http://codepen.io/madhurgarg71/pen/qrbLjp
You need to trigger the onChange event manually. On text inputs onChange listens for input events.
So in you handleClick function you need to trigger event like
handleClick () {
this.setState({value: 'another random text'})
var event = new Event('input', { bubbles: true });
this.myinput.dispatchEvent(event);
}
Complete code
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
value: 'random text'
}
}
handleChange (e) {
console.log('handle change called')
}
handleClick () {
this.setState({value: 'another random text'})
var event = new Event('input', { bubbles: true });
this.myinput.dispatchEvent(event);
}
render () {
return (
<div>
<input readOnly value={this.state.value} onChange={(e) => {this.handleChange(e)}} ref={(input)=> this.myinput = input}/>
<button onClick={this.handleClick.bind(this)}>Change Input</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
Codepen
Edit:
As Suggested by #Samuel in the comments, a simpler way would be to call handleChange from handleClick if you don't need to the event object in handleChange like
handleClick () {
this.setState({value: 'another random text'})
this.handleChange();
}
I hope this is what you need and it helps you.
I tried the other solutions and nothing worked. This is because of input logic in React.js has been changed. For detail, you can see this link: https://hustle.bizongo.in/simulate-react-on-change-on-controlled-components-baa336920e04.
In short, when we change the value of input by changing state and then dispatch a change event then React will register both the setState and the event and consider it a duplicate event and swallow it.
The solution is to call native value setter on input (See setNativeValue function in following code)
Example Code
import React, { Component } from 'react'
export class CustomInput extends Component {
inputElement = null;
// THIS FUNCTION CALLS NATIVE VALUE SETTER
setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
}
constructor(props) {
super(props);
this.state = {
inputValue: this.props.value,
};
}
addToInput = (valueToAdd) => {
this.setNativeValue(this.inputElement, +this.state.inputValue + +valueToAdd);
this.inputElement.dispatchEvent(new Event('input', { bubbles: true }));
};
handleChange = e => {
console.log(e);
this.setState({ inputValue: e.target.value });
this.props.onChange(e);
};
render() {
return (
<div>
<button type="button" onClick={() => this.addToInput(-1)}>-</button>
<input
readOnly
ref={input => { this.inputElement = input }}
name={this.props.name}
value={this.state.inputValue}
onChange={this.handleChange}></input>
<button type="button" onClick={() => this.addToInput(+1)}>+</button>
</div>
)
}
}
export default CustomInput
Result
I think you should change that like so:
<input value={this.state.value} onChange={(e) => {this.handleChange(e)}}/>
That is in principle the same as onClick={this.handleClick.bind(this)} as you did on the button.
So if you want to call handleChange() when the button is clicked, than:
<button onClick={this.handleChange.bind(this)}>Change Input</button>
or
handleClick () {
this.setState({value: 'another random text'});
this.handleChange();
}
In a functional component you can do this, let's assume we have a input[type=number]
const MyInputComponent = () => {
const [numberValue, setNumberValue] = useState(0);
const numberInput = useRef(null);
/**
* Dispatch Event on Real DOM on Change
*/
useEffect(() => {
numberInput.current.dispatchEvent(
new Event("change", {
detail: {
newValue: numberValue,
},
bubbles: true,
cancelable: true,
})
);
}, [numberValue]);
return (
<>
<input
type="number"
value={numberValue}
ref={numberInput}
inputMode="numeric"
onChange={(e) => setNumberValue(e.target.value)}
/>
</>
)
}
The other answers talked about direct binding in render hence I want to add few points regarding that.
You are not recommended to bind the function directly in render or anywhere else in the component except in constructor. Because for every function binding a new function/object will be created in webpack bundle js file hence the bundle size will grow. Your component will re-render for many reasons like when you do setState, new props received, when you do this.forceUpdate() etc. So if you directly bind your function in render it will always create a new function. Instead do function binding always in constructor and call the reference wherever required. In this way it creates new function only once because constructor gets called only once per component.
How you should do is something like below
constructor(props){
super(props);
this.state = {
value: 'random text'
}
this.handleChange = this.handleChange.bind(this);
}
handleChange (e) {
console.log('handle change called');
this.setState({value: e.target.value});
}
<input value={this.state.value} onChange={this.handleChange}/>
You can also use arrow functions but arrow functions also does create new function every time the component re-renders in certain cases. You should know about when to use arrow function and when are not suppose to. For detailed explation about when to use arrow functions check the accepted answer here
you must do 4 following step :
create event
var event = new Event("change",{
detail: {
oldValue:yourValueVariable,
newValue:!yourValueVariable
},
bubbles: true,
cancelable: true
});
event.simulated = true;
let tracker = this.yourComponentDomRef._valueTracker;
if (tracker) {
tracker.setValue(!yourValueVariable);
}
bind value to component dom
this.yourComponentDomRef.value = !yourValueVariable;
bind element onchange to react onChange function
this.yourComponentDomRef.onchange = (e)=>this.props.onChange(e);
dispatch event
this.yourComponentDomRef.dispatchEvent(event);
in above code yourComponentDomRef refer to master dom of your React component for example <div className="component-root-dom" ref={(dom)=>{this.yourComponentDomRef= dom}}>
Approach with React Native and Hooks:
You can wrap the TextInput into a new one that watches if the value changed and trigger the onChange function if it does.
import React, { useState, useEffect } from 'react';
import { View, TextInput as RNTextInput, Button } from 'react-native';
// New TextInput that triggers onChange when value changes.
// You can add more TextInput methods as props to it.
const TextInput = ({ onChange, value, placeholder }) => {
// When value changes, you can do whatever you want or just to trigger the onChange function
useEffect(() => {
onChange(value);
}, [value]);
return (
<RNTextInput
onChange={onChange}
value={value}
placeholder={placeholder}
/>
);
};
const Main = () => {
const [myValue, setMyValue] = useState('');
const handleChange = (value) => {
setMyValue(value);
console.log("Handling value");
};
const randomLetters = [...Array(15)].map(() => Math.random().toString(36)[2]).join('');
return (
<View>
<TextInput
placeholder="Write something here"
onChange={handleChange}
value={myValue}
/>
<Button
title='Change value with state'
onPress={() => setMyValue(randomLetters)}
/>
</View>
);
};
export default Main;
I know what you mean, you want to trigger handleChange by click button.
But modify state value will not trigger onChange event, because onChange event is a form element event.
I had a similar need and end up using componentDidMount(), that one is called long after component class constructor (where you can initialize state from props - as an exmple using redux )
Inside componentDidMount you can then invoke your handleChange method for some UI animation or perform any kind of component properties updates required.
As an example I had an issue updating an input checkbox type programatically, that's why I end up using this code, as onChange handler was not firing at component load:
componentDidMount() {
// Update checked
const checkbox = document.querySelector('[type="checkbox"]');
if (checkbox)
checkbox.checked = this.state.isChecked;
}
State was first updated in component class constructor and then utilized to update some input component behavior
Try this code if state object has sub objects like this.state.class.fee. We can pass values using following code:
this.setState({ class: Object.assign({}, this.state.class, { [element]: value }) }

Categories

Resources