How to create delayed effect with setTimeout and useEffect? - reactjs

Ref: https://stackblitz.com/edit/react-ts-8ykcuf?file=index.tsx
I created a small example to replicate the issue I am facing.
I am trying to create a delayed effect with setTimeout inside useEffect. I can see from console.log that setTimeout has already triggered and I expect the DOM to be updated, but actually the DOM is not rendered until the next human interaction.
The side effect in the sample example is to simulate a bot appending new message after user has entered a new message.
import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';
interface Chat {
messages: string[];
poster: string;
}
const App = () => {
const [activeChat, setActiveChat] = useState<Chat>({
poster: 'Adam',
messages: ['one', 'two', 'three'],
});
const [message, setMessage] = useState('');
const [isBotChat, setIsBotChat] = useState(false);
useEffect(() => {
if (isBotChat) {
setIsBotChat(false);
setTimeout(() => {
activeChat.messages.push('dsadsadsada');
console.log('setTimeout completed');
}, 500);
}
}, [isBotChat]);
const handleSubmit = (e) => {
e.preventDefault();
if (message !== '') {
activeChat.messages.push(message);
setMessage('');
setIsBotChat(true);
}
};
return (
<div>
<h1>Active Chat</h1>
<div>
{activeChat?.messages.map((m, index) => (
<div key={index}>{m}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.currentTarget.value)}
/>
<button type="submit">Send Message</button>
</form>
</div>
);
};
render(<App />, document.getElementById('root'));

To set your state you need to use setActiveChat, in this case something like:
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
The set state function React provides can accept a function, which we'll use in this case to avoid race conditions. previous is the previous value of activeChat (We can't rely on activeChat itself being up to date yet since the current render may be out of sync with the state) Then we expand the existing state and add the new property.
In your comments you mention only changing properties, and I'm afraid it's really not recommended to change anything in state directly, there are several in depth explanations of that here (StackOverflow question), and here (Documentation).
Full example (StackBlitz):
import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';
import './style.css';
interface Chat {
messages: string[];
poster: string;
}
const App = () => {
const [activeChat, setActiveChat] = useState<Chat>({
poster: 'Adam',
messages: ['one', 'two', 'three'],
});
const [message, setMessage] = useState('');
const [isBotChat, setIsBotChat] = useState(false);
useEffect(() => {
if (isBotChat) {
setIsBotChat(false);
setTimeout(() => {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
console.log('setTimeout completed');
}, 500);
}
}, [isBotChat]);
const handleSubmit = (e) => {
e.preventDefault();
if (message !== '') {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, message]
}))
setMessage('');
setTimeout(() => {
setActiveChat(previous => ({
...previous,
messages: [...previous.messages, 'dsadsadsada']
}))
console.log('setTimeout completed');
}, 500);
}
};
return (
<div>
<h1>Active Chat</h1>
<div>
{activeChat?.messages.map((m, index) => (
<div key={index}>{m}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.currentTarget.value)}
/>
<button type="submit">Send Message</button>
</form>
</div>
);
};
render(<App />, document.getElementById('root'));

Related

Using a High Order Component does not send data to the form in React

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;

React TypeError is not a function with Onboarding implementation

I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.

Test failing in React Testing Library / Jest despite correct DOM behavior

I'm pretty new to Jest and testing, so I'm making an app using React, React Testing Library, and Jest to improve my skills.
One of my tests is failing, and I can't figure out why. Here is the code from my test:
import { render, screen, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// using UrlShortener since the state has been lifted up for UrlList
import UrlShortener from '../../pages/UrlShortener/UrlShortener'
...
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
userEvent.click(submitBtn);
const listButton = screen.getByText('Link History');
userEvent.click(listButton);
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
The thing that's confusing me is that I can see that the list is 4 elements long in the log from the failing test, but for some reason it's not getting picked up in the expect() function. Here's what the log is giving me (it clearly shows 4 elements in the list):
expect(received).toHaveLength(expected)
Expected length: 4
Received length: 3
Received array: [<p>Visits: 2</p>, <p>Visits: 1</p>, <p>Visits: 5</p>]
...
<div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/123"
>
test1
</a>
</p>
<p>
Visits:
2
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/456"
>
test2
</a>
</p>
<p>
Visits:
1
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/789"
>
test3
</a>
</p>
<p>
Visits:
5
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/shorten/123"
>
test4
</a>
</p>
<p>
Visits:
9
</p>
</div>
</div>
How is it possible that the DOM is behaving as expected in the log, but is failing in the actual test?
Update:
I'm adding more information so it's obvious what I'm doing. Basically, I've lifted state up from a child component (UrlList) to the parent (UrlShortener) so that I could pass a state updater function down to a sibling (UrlBar). The UrlShortener makes an axios call to the backend, then passes down a list of URLs to the UrlList component. When you click the submit button in the UrlBar component, it re-runs the axios call and updates the list with the new URL added.
Parent component:
import { useEffect, useState } from 'react';
import { SectionPage, BackButton, PageTitle } from './style';
import axios from 'axios';
import UrlBar from '../../components/UrlBar/UrlBar';
import UrlList from '../../components/UrlList/UrlList';
import { Url } from '../../types/types';
const UrlShortener = () => {
const [urls, setUrls] = useState<Url[] | []>([]);
const getUrls = () => {
axios
.get('https://fullstack-demos.herokuapp.com/shorten/urls/all')
.then((res) => setUrls(res.data));
};
useEffect(() => {
getUrls();
}, []);
return (
<SectionPage>
<BackButton href='/'>Go Back</BackButton>
<PageTitle>URL Shortener</PageTitle>
<UrlBar getUrls={getUrls} />
<UrlList urls={urls} />
</SectionPage>
);
};
export default UrlShortener;
Children:
import React, { useState } from 'react';
import {
ComponentWrapper,
Subtitle,
Triangle,
LinksContainer,
LinkGroup,
} from './style';
import { Url } from '../../types/types';
interface IProps {
urls: Url[] | [];
}
const UrlList: React.FC<IProps> = ({ urls }) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prevState) => !prevState);
};
return (
<ComponentWrapper>
<Subtitle onClick={handleClick}>
Link History <Triangle>{open ? '▼' : '▲'}</Triangle>
</Subtitle>
<LinksContainer>
<div>
{open &&
urls.map(({ urlId, shortUrl, urlName, visits }: Url) => (
<LinkGroup key={urlId}>
<p>
<a href={shortUrl}>{urlName}</a>
</p>
<p>Visits: {visits}</p>
</LinkGroup>
))}
</div>
</LinksContainer>
</ComponentWrapper>
);
};
export default UrlList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { UrlInput, NameInput, UrlButton } from './style';
import { validateUrl } from '../../utils/utils';
interface IProps {
getUrls: () => void;
}
const UrlBar: React.FC<IProps> = ({ getUrls }) => {
const [urlInput, setUrlInput] = useState('');
const [nameInput, setNameInput] = useState('');
const [error, setError] = useState<boolean | string>(false);
useEffect(() => {
// Cleanup fixes React testing error: "Can't perform a React state update on an unmounted component"
return () => {
setUrlInput('');
};
}, []);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrlInput(e.target.value);
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNameInput(e.target.value);
};
const handleSubmit = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (!nameInput) {
setError('Please name your URL');
} else if (!validateUrl(urlInput)) {
setError('Invalid Input');
} else {
setError(false);
await axios.post('https://fullstack-demos.herokuapp.com/shorten', {
longUrl: urlInput,
urlName: nameInput,
});
setUrlInput('');
setNameInput('');
getUrls();
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<NameInput
type='text'
name='nameInput'
id='nameInput'
placeholder='Name your URL...'
maxLength={20}
onChange={handleNameChange}
value={nameInput}
/>
<UrlInput
type='text'
name='urlInput'
id='urlInput'
placeholder='Enter URL here...'
onChange={handleUrlChange}
value={urlInput}
/>
<UrlButton name='button' type='submit'>
Shorten!
</UrlButton>
{error && <label htmlFor='urlInput'>{error}</label>}
</form>
</div>
);
};
export default UrlBar;
So after fighting to get my tests to pass for another component, I finally figured out how to get this one to pass. Apparently I just needed to add a few more waitFor() and await statements to catch some of the async stuff happening in my component. I'd be lying if I said I understand why this fixes my problem, but now I know that if my tests are failing even though I can see the right results in the JEST DOM, it probably has to do with missing waitFor / awaits.
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
await waitFor(() => userEvent.click(submitBtn));
const listButton = await screen.findByText('Link History');
await waitFor(() => userEvent.click(listButton));
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
});

Preventing unnecessary rendering with React.js

I have a very basic application to test how to prevent unnecessary rendering, but I'm very confused as it is not working no matter what I try. Please take a look.
App.js
import { useState, useCallback } from "react";
import User from "./User";
let lastId = 0;
function App() {
const [users, setUsers] = useState([
{ id: 0, name: "Nicole Kidman", gender: "Female" },
]);
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
const newUsers = [...users];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
setUsers(newUsers);
}
},
[users]
);
const addNewUser = useCallback(() => {
let newUser = { id: ++lastId, name: "John Doe", gender: "Male" };
setUsers((prevUsers) => [...prevUsers, newUser]);
}, []);
return (
<div className="App">
<button onClick={addNewUser}>Add user</button>
<br />
{users.map((user) => (
<User key={user.id} user={user} handleUserChange={handleUserChange} />
))}
</div>
);
}
export default App;
User.js
import { useRef, memo } from "react";
const User = memo(({ user, handleUserChange }) => {
const renderNum = useRef(0);
return (
<div className="user">
<div> Rendered: {renderNum.current++} times</div>
<div>ID: {user.id}</div>
<div>
Name:{" "}
<input
name="name"
value={user.name}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<div>
Gender:{" "}
<input
name="gender"
value={user.gender}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<br />
</div>
);
});
export default User;
Why the useCallback and memo doesn't do the job here? How can I make it work, prevent rendering of other User components if another User component is changing(typing something in Input)?
Thank you.
useCallback and useMemo take a dependency array. If any of the values inside that array changes, React will re-create the memo-ized value/callback.
With this in mind, we see that your handleUsersChange useCallback is recreated every time the array users changes. Since you update the users state inside the callback, every time you call handleUsersChange, the callback is re-created, and therefore the child is re-rendered.
Solution:
Don't put users in the dependency array. You can instead access the users value inside the handleUsersChange callback by providing a callback to setUsers functions, like so:
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
setUsers((oldUsers) => {
const newUsers = [...oldUsers];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
return newUsers;
}
return oldUsers;
})
},
[]
);

How to start search only when user stops typing?

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.

Resources