Related
I want to adjust a demo provided by some tutorial about React Design Patterns, subject: Higher Order Component, and want to use an external data source from the url:
https://jsonplaceholder.typicode.com/users/1
to display the data within my form.
I guess since it's an async call, my Form always displays the "Loading part". What's the best way to solve this issue to ultimately receive the data? I can clearly see response.data not being empty when I log it, but the State variables are when I log them inside of the useEffect Hook
This is what I got so far.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my HOC which I just copied:
import React, { useState, useEffect } from "react";
import axios from "axios";
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
export const withEditableResource = (Component, resourcePath, resourceName) => {
return (props) => {
const [originalData, setOriginalData] = useState(null);
const [editedData, setEditedData] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get(resourcePath);
setOriginalData(response.data);
setEditedData(response.data);
})();
}, []);
const onChange = (changes) => {
setEditedData({ ...editedData, ...changes });
};
const onSave = async () => {
const response = await axios.post(resourcePath, {
[resourceName]: editedData,
});
setOriginalData(response.data);
setEditedData(response.data);
};
const onReset = () => {
setEditedData(originalData);
};
const resourceProps = {
[resourceName]: editedData,
[`onChange${capitalize(resourceName)}`]: onChange,
[`onSave${capitalize(resourceName)}`]: onSave,
[`onReset${capitalize(resourceName)}`]: onReset,
};
return <Component {...props} {...resourceProps} />;
};
};
That's my form, I want to use - in the last lines you can find the hard-coded URL path, I want to swap for a parameter once this problem is done:
import { withEditableResource } from "./withEditableResource";
export const UserInfoFormImproved = withEditableResource(
({ user, onChangeUser, onSaveUser, onResetUser }) => {
const { name, email, username } = user || {};
return user ? (
<>
<label>
Name:
<input
value={name}
onChange={(e) => onChangeUser({ name: e.target.value })}
/>
</label>
<label>
Email:
<input
value={email}
onChange={(e) => onChangeUser({ email: e.target.value })}
/>
</label>
<label>
Username:
<input
value={username}
onChange={(e) => onChangeUser({ username: e.target.value })}
/>
</label>
<button onClick={onResetUser}>Reset</button>
<button onClick={onSaveUser}>Save Changes</button>
</>
) : (
<p>Loading...</p>
);
},
`https://jsonplaceholder.typicode.com/users/3`,
"User"
);
And that's the actual use of this two components within my App - I've added my idea on how to solve the parameter argument here:
import { UserInfoFormImproved } from "./HigherOrderComponents/UserInfoFormImproved";
function App() {
return (
<UserInfoFormImproved userId={1} />
);
}
export default App;
I'm new with testing-library and jest and I'm trying to test a function inside of the component that changes the value of an input. The component is a form with another component that it's an input.
export const Form = () => {
const [name, setName] = useState("");
const handleOnSubmit = e => {
e.preventDefault();
const form = e.target;
};
const inputChange = (param) => (e) => {
const inputValue = e.target.value;
setName(inputValue);
};
return (
<form className="form" onSubmit={handleOnSubmit}>
<InputGroup text="name" type="text" value={name} functionality={inputChange("name")}/>
<Button type="submit" disabled={name === undefined}/>
</form>
);
};
export default Form;
The InputGroup Component looks like this
export const InputGroup = ({type, id, value, required, functionality, text}) => {
return (
<label>{text}</label>
<input className="input" type={type} id={id} name={id} value={value}
required={required} onChange={functionality}
/>
);
};
I have tried something like this, but I'm not pretty sure on how to test a function that is directly on the component Form and that it's being passed to the component InputGroup.
describe("Form", () => {
let value;
let component;
const handleSubmit = jest.fn();
const handleChange = ev => {
ev.preventDefault();
value = ev.currentTarget.value;
}
beforeEach(() => {
component = render(
<Form onSubmit={handleSubmit} functionality={handleChange} />
);
});
it("check error name is triggered", () => {
const input = component.getByText("name");
fireEvent.change(input, {target: {value: "aaa"}});
});
});
I get an error thats says "The given element does not have a value setter", so how can I pass the inputChange function to the InputGroup Component?
Ok, thanks for sharing the InputGroup. So you can test the InputGroup easily.
describe("InputGroup", () => {
it("check error name is triggered", () => {
const fn = jest.fn()
render(<InputGroup functionality={functionality} />)
fireEvent.change(input, {target: {value: "aaa"}});
expect(fn).toHaveBeenLastCalledWith(...)
});
});
Just an idea. The above code might not run as posted. Ok, now comes to the Form. The step might be similar but it might take you some time to get around, but you don't need to test the functionality again, simply because this prop has nothing to do with Form. From the form perspective, it only cares onSubmit, so you can test against that in the similar way that posted earlier.
I am making a Shopify app using Shopify Polaris.
I used the ActionList component.
https://polaris.shopify.com/components/actions/action-list
I want to change the state value on the onAction event.
I did like this.
const [searchValue, setSearchValue] = useState('');
const handleAction = (value) => {
setSearchValue(value);
}
const a = ["A","B"];
const searchResultsMarkup = (
<ActionList
items={[
{
content: a[0],
onAction: handleAction(a[0]),
},
{
content: a[1],
onAction: handleAction(a[1]),
},
/>
);
I am a beginner in React.
So maybe a silly question.
but kindly teach me.
Thanks
You are passing it down as a prop, so you will have to change it within the Component.
import React, { useState } from 'react';
export const Parent = () => {
const [searchValue, setSearchValue] = useState('');
const handleAction = value => {
setSearchValue(value);
}
return <ActionList changeSearchVal={handleAction} />
}
export const ActionList = ({ changeSearchVal }) => {
return (
<form>
<input type="text" onChange={e => changeSearchVal(e.target.value)}/>
</form>
)
}
If you want to change the searchValue within ActionList.
I am using React and have formatted a controlled input field, which works fine when I write some numbers and click outside the input field. However, when I want to edit the input, the cursor jumps to the front of the value in the input field. This only occur in IE, and not in e.g. Chrome. I've seen that for some programmers the cursor jumps to the back of the value. So I think the reason that my cursor is jumping to the front is because the value is aligned to the right instead of to the left in the input field. Here is a senario:
My first input is 1000
Then I want to edit it to 10003, but the result is
31000
Is there a way to controll that the cursor should not jump?
Here's a drop-in replacement for the <input/> tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
Taking a guess by your question, your code most likely looks similar to this:
<input
autoFocus="autofocus"
type="text"
value={this.state.value}
onChange={(e) => this.setState({value: e.target.value})}
/>
This may vary in behaviour if your event is handled with onBlur but essentially its the same issue. The behaviour here, which many have stated as a React "bug", is actually expected behaviour.
Your input control's value is not an initial value of the control when its loaded, but rather an underlying value bound to this.state. And when the state changes the control is re-rendered by React.
Essentially this means that the control is recreated by React and populated by the state's value. The problem is that it has no way of knowing what the cursor position was before it was recreated.
One way of solving this which I found to work is remembering the cursor position before it was re-rendered as follows:
<input
autoFocus="autofocus"
type="text"
value={ this.state.value }
onChange={(e) => {
this.cursor = e.target.selectionStart;
this.setState({value: e.target.value});
}
}
onFocus={(e) => {
e.target.selectionStart = this.cursor;
}
}
/>
This is my solution:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
//get reference for input
this.nameRef = React.createRef();
//Setup cursor position for input
this.cursor;
}
componentDidUpdate() {
this._setCursorPositions();
}
_setCursorPositions = () => {
//reset the cursor position for input
this.nameRef.current.selectionStart = this.cursor;
this.nameRef.current.selectionEnd = this.cursor;
};
handleInputChange = (key, val) => {
this.setState({
[key]: val
});
};
render() {
return (
<div className="content">
<div className="form-group col-md-3">
<label htmlFor="name">Name</label>
<input
ref={this.nameRef}
type="text"
autoComplete="off"
className="form-control"
id="name"
value={this.state.name}
onChange={event => {
this.cursor = event.target.selectionStart;
this.handleInputChange("name", event.currentTarget.value);
}}
/>
</div>
</div>
);
}
}
export default App;
This is an easy solution. Worked for me.
<Input
ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
value={this.state.inputtext}
onChange={(e)=>{
this.cursor = e.target.selectionStart;
this.setState({inputtext: e.target.value})
/>
Explanation:
What we are doing here is we save the cursor position in onChange(), now when the tag re-renders due to a change in the state value, the ref code is executed, and inside the ref code we restore out cursor position.
If you're using textarea, then here's the hook based on Daniel Loiterton's code using TypeScript:
interface IControlledTextArea {
value: string
onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
[x: string]: any
}
const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
const [cursor, setCursor] = useState(0)
const ref = useRef(null)
useEffect(() => {
const input: any = ref.current
if (input) {
input.setSelectionRange(cursor, cursor)
}
}, [ref, cursor, value])
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCursor(e.target.selectionStart)
onChange && onChange(e)
}
return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
}
As (I think) others have mentioned, React will keep track of this if you make your changes synchronously. Unfortunately, that's not always feasible. The other solutions suggest tracking the cursor position independently, but this will not work for input types like 'email' which will not allow you to use cursor properties/methods like selectionStart, setSelectionRange or whatever.
Instead, I did something like this:
const Input = (props) => {
const { onChange: _onChange, value } = props;
const [localValue, setLocalValue] = useState(value);
const onChange = useCallback(
e => {
setLocalValue(e.target.value);
_onChange(e.target.value);
},
[_onChange]
);
useEffect(() => {
setLocalValue(value);
}, [value]);
// Use JSX here if you prefer
return react.createElement('input', {
...props,
value: localValue,
onChange
});
};
This allows you to delegate the cursor positioning back to React, but make your async changes.
My cursor jumped always to the end of the line. This solution seems to fix the problem (from github):
import * as React from "react";
import * as ReactDOM from "react-dom";
class App extends React.Component<{}, { text: string }> {
private textarea: React.RefObject<HTMLTextAreaElement>;
constructor(props) {
super(props);
this.state = { text: "" };
this.textarea = React.createRef();
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const cursor = e.target.selectionStart;
this.setState({ text: e.target.value }, () => {
if (this.textarea.current != null)
this.textarea.current.selectionEnd = cursor;
});
}
render() {
return (
<textarea
ref={this.textarea}
value={this.state.text}
onChange={this.handleChange.bind(this)}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Here is my solution
const Input = () => {
const [val, setVal] = useState('');
const inputEl = useRef(null);
const handleInputChange = e => {
const { value, selectionEnd } = e.target;
const rightCharsCount = value.length - selectionEnd;
const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
const newPosition = formattedValue.length - rightCharsCount;
setVal(formattedValue);
setTimeout(() => {
inputEl.current.setSelectionRange(newPosition, newPosition);
}, 0);
};
return <input ref={inputEl} value={val} onChange={handleInputChange} />;
};
// Here is a custom hook to overcome this problem:
import { useRef, useCallback, useLayoutEffect } from 'react'
/**
* This hook overcomes this issue {#link https://github.com/reduxjs/react-redux/issues/525}
* This is not an ideal solution. We need to figure out why the places where this hook is being used
* the controlled InputText fields are losing their cursor position when being remounted to the DOM
* #param {Function} callback - the onChangeCallback for the inputRef
* #returns {Function} - the newCallback that fixes the cursor position from being reset
*/
const useControlledInputOnChangeCursorFix = callback => {
const inputCursor = useRef(0)
const inputRef = useRef(null)
const newCallback = useCallback(
e => {
inputCursor.current = e.target.selectionStart
if (e.target.type === 'text') {
inputRef.current = e.target
}
callback(e)
},
[callback],
)
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
}
})
return newCallback
}
export default useControlledInputOnChangeCursorFix
// Usage:
import React, { useReducer, useCallback } from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ({ ...state, ...action })
const initialState = { street: '', address: '' }
const SomeComponent = props => {
const [state, setState] = useReducer(setStateReducer, initialState)
const handleOnChange = useControlledInputOnChangeCursorFix(
useCallback(({ target: { name, value } }) => {
setState({ [name]: value })
}, []),
)
const { street, address } = state
return (
<form>
<input name='street' value={street} onChange={handleOnChange} />
<input name='address' value={address} onChange={handleOnChange} />
</form>
)
}
For anybody having this issue in react-native-web here is solution written in TypeScript
const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
if(typeof refInput === "function") {
console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
return <TextInput key={"invalid-ref"} {...props} />;
}
if(!("HTMLInputElement" in self)) {
return <TextInput key={"no-web"} {...props} />;
}
const { value, onChange, ...restProps } = props;
const defaultRefObject = useRef<TextInput>(null);
const refObject: RefObject<TextInput> = refInput || defaultRefObject;
const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
useEffect(() => {
if(refObject.current instanceof HTMLInputElement) {
refObject.current.setSelectionRange(selection.start, selection.end);
}
}, [ refObject, selection, value ]);
return (
<TextInput
ref={refObject}
value={value}
onChange={event => {
const eventTarget = event.target as any;
if(eventTarget instanceof HTMLInputElement) {
setSelection({
start: eventTarget.selectionStart,
end: eventTarget.selectionEnd
});
}
if(onChange) {
onChange(event);
}
}}
{...restProps}
/>
)
});
The simplest and safest way of doing this is probably to save the cursor position before React renders the input and then setting it again after React finishes rendering.
import React, {ReactElement, useEffect, useRef} from "react";
/**
* Text input that preserves cursor position during rendering.
*
* This will not preserve a selection.
*/
function TextInputWithStableCursor(
props: React.InputHTMLAttributes<HTMLInputElement> & {type?: "text"}
): ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
// Save the cursor position before rendering
const cursorPosition = inputRef.current?.selectionStart;
// Set it to the same value after rendering
useEffect(function () {
if (
typeof cursorPosition === "number" &&
document.activeElement === inputRef.current
) {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
}
});
return <input ref={inputRef} {...props} />;
}
If you faced an issue with the cursor jumping to the end after updating the input state and updating the cursor using refs -> I found a workaround for it by setting the cursor in Promise.resolve's microtask.
<input
value={value}
onChange={handleValueUpdate}
ref={inputRef}
/>
const handleValueUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
// ...
// some value handling logic
setValue(newValue)
const cursorPosition = getCursorPositionLogic();
/**
* HACK: set the cursor on the next tick to make sure that the value is updated
* useTimeout with 0ms provides issues when two keys are pressed same time
*/
Promise.resolve().then(() => {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
});
}
I know the OP is 5 years old but some are still facing the same kind of issue and this page has an high visibility on Google search.
Try by replacing :
<input value={...}
with
<input defaultValue={...}
This will solve most of the cases i've seen around there.
I tried all of the above solutions and none of them worked for me. Instead I updated both the e.currentTarget.selectionStart & e.currentTarget.selectionEnd on the onKeyUp React synthetic event type. For example:
const [cursorState, updateCursorState] = useState({});
const [formState, updateFormState] = useState({ "email": "" });
const handleOnChange = (e) => {
// Update your state & cursor state in your onChange handler
updateCursorState(e.target.selectionStart);
updateFormState(e.target.value);
}
<input
name="email"
value={formState.email}
onChange={(e) => handleOnChange(e)}
onKeyUp={(e) => {
// You only need to update your select position in the onKeyUp handler:
e.currentTarget.selectionStart = cursorState.cursorPosition;
e.currentTarget.selectionEnd = cursorState.cursorPosition;
}}
/>
Also, be aware that selectionStart & selectionEnd getters are not available on input fields of type email.
I need to perform a Search when user stops typing.I know I am supposed to use setTimeout() . But with Reactjs I cant find how it works. Can someone please tell me how to invoke a method (that will handle Search) when the user stops typing for a few seconds (suppose 5).I cant figure out where to write the code to check that the user has stopped typing.
import React, {Component, PropTypes} from 'react';
export default class SearchBox extends Component {
state={
name:" ",
}
changeName = (event) => {
this.setState({name: event.target.value});
}
sendToParent = () => {
this.props.searching(this.state.name);
}
render() {
return (
<div>
<input type="text" placeholder='Enter name you wish to Search.' onChange={this.changeName} />
</div>
);
}
}
I want to invoke the sendToParent method when the user stops typing.
Implement using useEffect hook:
function Search() {
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
console.log(searchTerm)
// Send Axios request here
}, 3000)
return () => clearTimeout(delayDebounceFn)
}, [searchTerm])
return (
<input
autoFocus
type='text'
autoComplete='off'
className='live-search-field'
placeholder='Search here...'
onChange={(e) => setSearchTerm(e.target.value)}
/>
)
}
You can use setTimeout with respect to your code as follows,
state = {
name: '',
typing: false,
typingTimeout: 0
}
changeName = (event) => {
const self = this;
if (self.state.typingTimeout) {
clearTimeout(self.state.typingTimeout);
}
self.setState({
name: event.target.value,
typing: false,
typingTimeout: setTimeout(function () {
self.sendToParent(self.state.name);
}, 5000)
});
}
Also, you need to bind changeName handler function in constructor.
constructor(props) {
super(props);
this.changeName = this.changeName.bind(this);
}
Another way that worked with me:
class Search extends Component {
constructor(props){
super(props);
this.timeout = 0;
}
doSearch(evt){
var searchText = evt.target.value; // this is the search text
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
//search function
}, 300);
}
render() {
return (
<div className="form-group has-feedback">
<label className="control-label">Any text</label>
<input ref="searchInput" type="text" onChange={evt => this.doSearch(evt)} />
</div>
);
}
}
This library (use-debounce) is nice and simple.
Setup
yarn add use-debounce
or
npm i use-debounce --save
Usage sample from documentation
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
Things that I liked at this moment, things could be different in
future!:
Easy to setup & use
Less Boilerplate code
Modest ratings (~1K) and usage (npm - 200K downloads/Week)
Supports timeout, MaxWait and other features
I used the debounce function of lodash
onChangeSearchInput = (evt)=> {
this.debouncedSearch(evt.target.value);
};
debouncedSearch = debounce(function (query) {
this.setState({query});
}, 1000);
Somewhere in my render method i have this input field
<input
type='text'
onChange={this.onChangeSearchInput}
className='uk-input'
placeholder={'search by name or email...'}
/>
I have use this custom hook and it's work perfectly no issue still.
export function useSearchDebounce(delay = 350) {
const [search, setSearch] = useState(null);
const [searchQuery, setSearchQuery] = useState(null);
useEffect(() => {
const delayFn = setTimeout(() => setSearch(searchQuery), delay);
return () => clearTimeout(delayFn);
}, [searchQuery, delay]);
return [search, setSearchQuery];
}
Use in any place like
const [search, setSearch] = useSearchDebounce();
<input onChange={(e) => setSearch(e.target.value)}/>
I think we can do it in a more simpler and cleaner manner, without abrupting the state parameter which calls the complete component life cycle like this:
constructor(props) {
super(props);
//Timer
this.typingTimeout = null;
//Event
this.onFieldChange = this.onFieldChange.bind(this);
//State
this.state = { searchValue: '' };
}
/**
* Called on the change of the textbox.
* #param {[Object]} event [Event object.]
*/
onFieldChange(event) {
// Clears the previously set timer.
clearTimeout(this.typingTimeout);
// Reset the timer, to make the http call after 475MS (this.callSearch is a method which will call the search API. Don't forget to bind it in constructor.)
this.typingTimeout = setTimeout(this.callSearch, 475);
// Setting value of the search box to a state.
this.setState({ [event.target.name]: event.target.value });
}
<div className="block-header">
<input
type="text"
name="searchValue"
value={this.state.searchValue}
placeholder="User Name or Email"
onChange={this.onFieldChange}
/>
</div>
you can use react hooks useEffect with the use of setTimeOut function since it always return the timer id and you could easily clear the timer with that id as follows
export const Search = () => {
const [term, setTerm] = useState();
const [results, setResult] = useState([]);
useEffect(() => {
const searchWiki = async () => {
const { data } = await axios.get('https://en.wikipedia.org/w/api.php', {
params: {
srsearch: term,
},
});
setResult(data.query.search);
};
const timerId = setTimeout(() => {
searchWiki();
// make a request after 1 second since there's no typing
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [term]);
How about a custom hook?
import {useEffect, useRef, useState} from "react";
export default function useSearchInputState(searchHandler) {
// to prevent calling the handler on component mount
const didMountRef = useRef(false);
const [searchValue, setSearchValue] = useState(null);
useEffect(() => {
let delayDebounceFn;
if (didMountRef.current) {
delayDebounceFn = setTimeout(searchHandler, 600)
} else {
didMountRef.current = true;
}
return () => clearTimeout(delayDebounceFn);
}, [searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
return [searchValue, setSearchValue];
}
Usage:
function MyComponent(props) {
const [searchValue, setSearchValue] = useSearchInputState(() => {
resetData(searchValue ?? null, selectedFilterPos); // replace with your code
});
return (
<input className="Search"
onChange={e => setSearchValue(e?.target?.value ?? null)}
/>
);
}
you can just use the debounce from lodash or simulate using setTimeout.
import React, {Component, PropTypes} from 'react';
export default class SearchBox extends Component {
constructor(props){
super(props);
this.state={ name:" "}
this.timeout = null;
}
changeName = (event) => {
clearTimeout(timeout);
if(timeout){
setTimeout((event)=> this.setState({name: event.target.value}), 200)
}
}
sendToParent = () => {
this.props.searching(this.state.name);
}
render() {
return (
<div>
<input type="text" placeholder='Enter name you wish to Search.' onChange={this.changeName} />
</div>
);
}
}
I made my own custom component like this.
import React, { useState, useEffect } from 'react'
const InputDebounce = props => {
const { onChange, ...otherProps } = props
const [inputTimeout, setInputTimeout] = useState(null)
useEffect(() => () => clearTimeout(inputTimeout), [inputTimeout])
const inputOnChange = value => {
if (inputTimeout) clearTimeout(inputTimeout)
setInputTimeout(
setTimeout(() => {
if (onChange) onChange(value)
}, 1000)
)
}
return (
<input
{...otherProps}
onChange={e => inputOnChange(e.target.value)}
/>
)
}
export default InputDebounce
And using anywhere like this.
import React from 'react'
import ReactDOM from 'react-dom'
import InputDebounce from './InputDebounce'
const App = () => {
const usernameOnChange = value => {
console.log(value)
}
return (
<div>
<InputDebounce
type='text'
name='username'
placeholder='Username'
onChange={usernameOnChange}
/>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
For React hooks:
First we'll define a component
import React, { useEffect, useState } from "react";
const SearchInputText = ({ value, name, placeholder, onChange }) => {
// state for keepign search text
const [searchText, setSearchText] = useState(value);
// state for keeping the timeout
const [searchTextTimeout, setSearchTextTimeout] = useState(null);
// handler for form submit (pressing enter without waiting for setimeout to trigger)
const handleSubmit = (e) => {
e.preventDefault();
// clear timeout as it'll that would be triggered
if (searchTextTimeout) {
clearTimeout(searchTextTimeout);
}
onChange(searchText);
};
// onChange handler
const handleOnChange = (e) => {
// cancelling previous timeouts
if (searchTextTimeout) {
clearTimeout(searchTextTimeout);
}
// first update the input text as user type
setSearchText(e.target.value);
// initialize a setimeout by wrapping in our searchTextTimeout so that we can clear it out using clearTimeout
setSearchTextTimeout(
setTimeout(() => {
onChange(searchText);
// timeout is 2500ms, change it to less or more.
}, 2500),
);
};
// making sure that we clear the timeout if/when the component unmount
useEffect(() => {
return () => clearTimeout(searchTextTimeout);
}, [searchTextTimeout]);
return (
<form onSubmit={handleSubmit}>
<input
name={name}
placeholder={placeholder}
type="text"
value={searchText}
onChange={handleOnChange}
/>
</form>
);
};
export default SearchInputText;
Usage:
const Parent = () => {
const handleChange = (e) => {
// your implementation here
};
return (
<div>
<SortSearchInput name="search" placeholder="Enter Search" onChange={handleChange} />
</div>
);
};
The code below works well for me :
const [filter, setFilter] = useState()
useEffect(() => {
const search = setTimeout(() => {
getList()
//Your search query and it will run the function after 3secs from user stops typing
}, 3000);
return () => clearTimeout(search)
}, [filter])
and add HTML like this:
<input type="text" onInput={(e) => setFilter(e.target.value)} value={filter} />
Here is an approach using functional components and the useRef hook.
import React, { useRef, useEffect } from "react";
function Search() {
const [searchTerm, setSearchTerm] = React.useState("");
const inputRef = useRef<any>()
useEffect(() => {
let timer: NodeJS.Timeout | null = null
const sendData = () => {
// If the user keeps on typing then the timeout is cleared and restarted
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
setSearchTerm(inputRef.current.value)
}, 3000)
}
const element = inputRef.current;
// Set listener and start timeout
element.addEventListener('keyup', sendData);
return () => {
// Remove listener wwhen unmounting
element.removeEventListener('keyup', sendData);
};
}, []);
return (
<div>
<input
ref={inputRef}
autoFocus
type="text"
autoComplete="off"
className="live-search-field"
placeholder="Search here..."
/>
<p>searchTerm: {searchTerm}</p>
</div>
);
}
export default Search;
This approach avoids unnecessary re-renders and utilizes event listeners to handle the search submission when user stops typing.
Here's a working component template with some useful parameters to get your started.
import React, { Component } from 'react'
const initialState = { results: [], value: '' }
export default class SearchBox extends Component {
state = initialState
timeout = null
search_url = "https://example.com/search?q="
min_query_length = 2
timeout_duration = 300
handleSearchChange = (e) => {
let value = e.target.value
clearTimeout(this.timeout);
if (value.length < 1) {
return this.setState(initialState)
} else {
this.setState({ value })
if (value.length>=this.min_query_length) {
this.timeout = setTimeout(this.search, this.timeout_duration);
}
}
}
search = () => {
// assuming your results are returned as JSON
fetch(`${this.search_url}${this.state.value}`)
.then(res => res.json())
.then(data => {
this.setState({
results: data,
})
})
}
render() {
return (
<input
onChange={this.handleSearchChange}
/>
)
}
}
using react hooks, modified from #anoNewb's answer. With additions:
prevent multiple triggers when there's still timer running
add on Form Submit event
codesandbox
import React, { useState, useEffect } from "react";
export default function App() {
const [search, setSearch] = useState("");
const [searchTimeout, setSearchTimeout] = useState(null);
useEffect(() => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
setSearchTimeout(
setTimeout(() => {
loadUsers();
}, 1000),
);
return () => clearTimeout(searchTimeout);
}, [search]);
const loadUsers = () => {
console.log("axios call with query: ", search);
};
return (
<div className="App">
<form
onSubmit={(e) => {
e.preventDefault();
if (searchTimeout) {
clearTimeout(searchTimeout);
}
loadUsers();
}}
>
<input
onChange={(e) => {
setSearch(e.target.value);
}}
/>
</form>
</div>
);
}
The code below works for me.
const[isReady, setReady] = useState(true);
const onSearchSet =(event:React.ChangeEvent<HTMLInputElement>) => {
setCriteria(event.target.value);
if(isReady) {
setReady(false);
const delayDebounceFn = setTimeout(() => {
// Send Axios request here
props.returnCall(props.RDropID, sortCriteria, event.target.value);
setReady(true);
}, 1000)
}
};
Can I use this code with Saga? It will help send the latest request. The time on the set time out can be changed. In my case, I used 600ms.
const dispatch = useDispatch();
const [searchText, setSearchText] = useState('');
useEffect(() => {
const sendSearchRequest = setTimeout(() => {
if (searchText && searchText.length > 2) {
dispatch(sendRequestToSaga(searchText));
}
}, 600);
return () => clearTimeout(sendSearchRequest);
}, [searchText]);
This is much easier now with useEffect and does not need any library
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
const FuncDemo = () => {
const [searchStr, setSearchStr] = useState('')
useEffect(() => {
const makeApiCall = async () => {
try {
// your axios call
} catch (e) {
}
}
const triggerCall = setTimeout(() => {
makeApiCall()
}, 500)
return () => clearTimeout(triggerCall)
}, [searchStr])
return (
<input
name='search'
onChange={e => setSearchString(e.target.value)}
/>
)
}
ReactDOM.render(<FuncDemo/>, document.getElementById('root'))
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function search(){
console.log('search');
}
const processChange = debounce(() => search());
It can be used in input
<input type="text" onkeyup="processChange()" />
User lodash javascript library and use [_.debounce][1]
changeName: _.debounce(function (val) {
console.log(val)
}, 1000)
Problem of Typeahead library https://twitter.github.io/typeahead.js/
Since the case here is simple, I can use a quick and dirty solution:
onChange: (event) ->
if #_timeoutTask?
clearTimeout #_timeoutTask
#_timeoutTask = setTimeout (=>
#sendToParent event.target.value
clearTimeout #_timeoutTask
), 5000
In this way, the task will be triggered 5s after input event. If new event happens, the old task will be cancelled and a new task is scheduled, then it's another 5s to wait.
The difference in React is the where to store the computation state like _timeoutTask. The file scope, the component state, or the component instance.
Since _timeoutTask is component level, it should be be store globally. And it does not affect rendering, so not in component state too. So I suggest attaching it to component instance directly.