How to make javascript debounce work in react - javascript

I'm having trouble using debounce method in react which works perfectly in javascript when we set input field onInput to a function , like , using setSearchData function , <input type='text onInput='setSearchData ()'/>
but the same debounce logic doesn't work . I don't get any text on my textarea nor do I get any console.log
My debounce logic consists of setSearchData ,searchData2 and searchData3 functions.
But here I think since I'm using event I can't get any value. I've also tried using
document.getElementById('texx).addEventListener('input',setSearchData,false)
import React, { useEffect, useState } from 'react'
import { TextField, Grid, Box, makeStyles, Toolbar, createMuiTheme, ThemeProvider, Container, AppBar, Divider, Card, CardMedia } from '#material-ui/core'
import PrimarySearchAppBar from '../Appbar'
import axios from 'axios';
import Wearcard from './Wearcard';
const useStyles = makeStyles({
})
const Weather = () => {
const [search, setSearch] = useState('')
const [city, setCity] = useState([])
const [addCity, setaddCity] = useState([])
const fetchLocat = async () => {
try {
const { data } = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid=${process.env.REACT_APP_API_KEY}`)
console.log(data)
setCity(data)
} catch (error) {
console.error(error)
}
}
// debounce method for search hook
// document.getElementById('texx').addEventListener('input', setSearchData, false)
**const searchData3 = (event) => {
setSearch(event.target.value)
let ds = 1;
console.log('wow', ds++)
}
const searchData2 = (func, delay) => {
let timer;
return function () {
let context = this;
let argss = arguments;
clearTimeout(timer)
timer = setTimeout(() => {
searchData3.apply(context, arguments);
}, delay)
}
}
const setSearchData = searchData2(searchData3, 1200);**
useEffect(() => {
fetchLocat()
}, [search])
console.log(this)
return (
<div>
<PrimarySearchAppBar color='secondary' />
<Toolbar />
<Container>
<Divider />
<br />
**<TextField label='Enter pin code' id='texx' className='searchhold' value={search} variant='filled' size='medium' type='search' name='cityName' onInput={setSearchData}>**
</TextField>
<br /><br /><br /><br />
<Grid>
<Wearcard />
</Grid>
</Container>
</div >)
}
export default Weather;

As you said, you are using like, This will not work
<input type='text onInput='setSearchData ()'/>
To Make it work, Update like
<input type='text onInput={setSearchData}/>
Clean and reusable approach
const debounce = (func, delay) => {
let timer;
return function (...args) {
let context = this;
clearTimeout(timer);
timer = setTimeout(() => func.apply(context, args), delay);
};
};
const searchData3 = (event) => {
setSearch(event.target.value);
let ds = 1;
console.log("wow", ds++);
};
const setSearchData = debounce(searchData3, 1200);

Aside from the answer above,
You may use lodash, which done the debounce function for you
import delay from "lodash/debounce";
const delay = /* some number */;
const searchData3 = event => debounce(() => {
setSearch(event.target.value);
}, delay);
React 18 (still in alpha) now support this natively and much better than setTimeout.
import { startTransition } from "react"
const searchData3 = event => startTransition(() => {
setSearch(event.target.value); // this will run in lower priority then other setState
});

Related

React initiates the state to default after calling the API with DEBOUNCING

I created a simple "notes app" by just passing the props and callback functions to the child and nested child components. My CRUD is working fine when I update the note on each keystroke. However, when I call the API using the Debouncing concept, the App.js forgets the state and re-initiates it to the default value.
here is the following code -
App.js
const addNote = async (note) => {
const newNote = await CreateNote(note); // this is API call
let newNotes = [...notes]; // notes is state - array of note object
newNotes.unshift(newNote);
setNotes(newNotes);
setActiveNote(newNote);
};
note-editor.js
const handleNoteChange = (e) => {
let newNote = { ...activeNote, [e.target.name]: e.target.value };
activateNote(newNote);
//addOrUpdateNote(newNote); // this code is working and updating the list correctly
optimizedAddOrUpdateNote(newNote); // this code re-initiates the "notes" state in App.js to default []
};
const addOrUpdateNote = (note) => {
if (!note.createdDate) {
if (note.title.trim() || note.body.trim()) {
addNote(note); // this is coming from app.js as prop callback
}
} else {
updateNote(note); // this is coming from app.js as prop callback
}
};
const debounce = (func) => {
let timer;
return function(...args) {
const context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
func.apply(context, args);
}, 500);
}
}
const optimizedAddOrUpdateNote = useCallback(debounce(addOrUpdateNote), []);
return (
<div className={Classes["note-editor-body"]}>
<input
type='text'
name='title'
placeholder='title...'
onChange={handleNoteChange} //trying to call the API using debounciing
value={activeNote.title}
/>
<textarea
maxLength={AppConstants.NOTE_BODY_CHARACTER_LIMIT}
name='body'
placeholder='add your notes here'
onChange={handleNoteChange} //trying to call the API using debounciing
value={activeNote.body.slice(
0,
AppConstants.NOTE_BODY_CHARACTER_LIMIT
)}
/>
</div>
)
Any help would be appreciated. Thanks!
React has its own build in debounce functionality with useDeferredValue(). There a good article about it here: https://blog.webdevsimplified.com/2022-05/use-deferred-value/.
So in your case you could replace your optimizedAddOrUpdateNote function with a useEffect hook that have a dependency on the deferredValue. Something like this:
import { useState, useDeferredValue, useEffect } from "react";
export default function App() {
const [note, setNote] = useState("");
const deferredNote = useDeferredValue(note);
useEffect(() => {
console.log("call api with deferred value");
}, [deferredNote]);
function handleNoteChange(e) {
setNote(e.target.value);
}
return (
<>
<input type="text" value={note} onChange={handleNoteChange} />
<p>{note}</p>
</>
);
}

Debouncing and Timeout in React

I have a here a input field that on every type, it dispatches a redux action.
I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?
useTimeout
import { useCallback, useEffect, useRef } from "react";
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}, [delay]);
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
return { reset, clear };
}
useDebounce
import { useEffect } from "react";
import useTimeout from "./useTimeout";
export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay);
useEffect(reset, [...dependencies, reset]);
useEffect(clear, []);
}
Form component
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const { handleChangeProductName = () => {} } = props;
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
useDebounce(() => handleChangeProductName(e.target.value), 1000, [
e.target.value,
]);
}}
/>
);
}
I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.
Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example usage:
export default function ProductInputs({ handleChangeProductName }) {
const debouncedHandler = useCallback(
debounce(handleChangeProductName, 200),
[handleChangeProductName]
);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandler(e.target.value);
}}
/>
);
}
If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.
Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...
In my opinion, useDebounce should return a "debounced" version of callback
Here is my take on useDebounce:
export default function useDebounce(callback, delay) {
const [debounceReady, setDebounceReady] = useState(true);
const debouncedCallback = useCallback((...args) => {
if (debounceReady) {
callback(...args);
setDebounceReady(false);
}
}, [debounceReady, callback]);
useEffect(() => {
if (debounceReady) {
return undefined;
}
const interval = setTimeout(() => setDebounceReady(true), delay);
return () => clearTimeout(interval);
}, [debounceReady, delay]);
return debouncedCallback;
}
Usage will look something like:
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const handleChangeProductName = useCallback((value) => {
if (props.handleChangeProductName) {
props.handleChangeProductName(value);
} else {
// do something else...
};
}, [props.handleChangeProductName]);
const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandleChangeProductName(e.target.value);
}}
/>
);
}
Debouncing onChange itself has caveats. Say, it must be uncontrolled component, since debouncing onChange on controlled component would cause annoying lags on typing.
Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.
With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:
const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);
useEffect(() => {
if (isValueSettled) {
props.onChange(text);
}
}, [text, isValueSettled]);
...
<input value={value} onChange={({ target: { value } }) => setText(value)}
And useIsSetlled itself will debounce:
function useIsSettled(value, delay = 500) {
const [isSettled, setIsSettled] = useState(true);
const isFirstRun = useRef(true);
const prevValueRef = useRef(value);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
setIsSettled(false);
prevValueRef.current = value;
const timerId = setTimeout(() => {
setIsSettled(true);
}, delay);
return () => { clearTimeout(timerId); }
}, [delay, value]);
if (isFirstRun.current) {
return true;
}
return isSettled && prevValueRef.current === value;
}
where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).
And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.

Why `setTimeout` call more than one time when I use `useState`?

I'm so confused about useState in React hooks.
I do not know why console.log in setTimeout function calls more than one time when I use useState.
If I remove useState it normally calls only once.
And If I use Class state instead hooks, it normally calls only once as well.
Why is it happened that ?
And how can I handle it ?
(here is my code)
import React, { useState, useEffect } from "react";
import "./App.css";
const usePassword = () => {
const [passwordValue, setPasswordValue] = useState({
password: "",
passwordHidden: "",
});
let timer = null;
const trigger = () => {
clearTimeout(timer);
timer = setTimeout(() => console.log("end"), 1000);
};
const onPasswordChanged = (name, value) => {
setPasswordValue((prev) => ({ ...passwordValue, passwordHidden: value }));
trigger();
};
return { passwordValue, onPasswordChanged };
};
function App() {
const { passwordValue, onPasswordChanged } = usePassword();
const onChanged = (event) => {
const { name, value } = event.target;
onPasswordChanged(name, value);
};
const onSubmit = () => {
console.log("submitted!", passwordValue);
};
return (
<div className="App">
<header className="App-header">
<input name="password" onKeyUp={onChanged} />
<button onClick={onSubmit}>Submit</button>
</header>
</div>
);
}
export default App;
Whenever you set the state using useState you get a new timer variable, as the function is called again. This is why your clearTimeout does not work.
You can use a ref to hold on to the value between render cycles:
const timer = useRef(null);
const trigger = () => {
clearTimeout(timer.current);
timer.current = setTimeout(() => console.log("end"), 1000);
};

How can I change the onClick function of a button saved in a useRef object in a react component?

I'm learning state and hooks in React. I'm doing an exercise to deal cards from a deck using the deckofcardsAPI, not the most important code on the interwebs, but it does bring about a question. In fact, it's not even part of the exercise, I just really want to do it.
I have a button that is stored in a useRef object. I really would like the onClick function to change, but I'm not sure how to do it.
import React, { useState, useEffect, useRef } from "react";
import axios from 'axios';
// auto dealer, 1 card/s turned on or off
function CardTable2() {
const [src, setSrc] = useState('');
const deckId = useRef();
const remaining = useRef(52);
const isDealing = useRef(false);
const timerId = useRef();
const button = useRef();
useEffect(() => {
timerId.current = setInterval(() => {
if (isDealing.current) {
dealCard();
};
}, 1000);
async function createDeck() {
const res = await axios.get(`http://deckofcardsapi.com/api/deck/new/shuffle/?deck_count=1`);
deckId.current = res.data.deck_id;
};
createDeck();
return () => { clearInterval(timerId.current) }
}, []);
async function dealCard() {
if (remaining.current === 0) {
alert('You are out of cards');
isDealing.current = false;
button.current.innerText = "Shuffle deck";
button.current.onClick = shuffleDeck;
return;
}
console.log(button);
const res = await axios.get(`http://deckofcardsapi.com/api/deck/${deckId.current}/draw/?count=1`);
remaining.current = res.data.remaining;
setSrc(res.data.cards[0].image);
};
async function shuffleDeck() {
const res = await axios.get(`http://deckofcardsapi.com/api/deck/${deckId.current}/shuffle/`);
setSrc('');
button.current.innerText = "Start dealing cards";
button.current.onClick = toggleDealing;
}
function toggleDealing() {
isDealing.current = !isDealing.current;
button.current.innerText = "Start dealing cards";
};
return (
<>
<button onClick={toggleDealing} ref={button}>{isDealing.current ? "Stop dealing cards" : "Start dealing cards"}</button>
<div>
<img className="CardBox" src={src} alt="a card" />
</div>
</>
)
};
export default CardTable2;
I've tried setting the button.current.onClick to the function, as seen above, but it doesn't actually seem to have an effect. Am I missing something?
I think what's happening is the return is just putting back {toggleDealing} every time.
Why not get rid of the ref and just have a generic function handle both cases?
const [buttonMode, setButtonMode] = useState("shuffle")
function handleClick() {
if (buttonMode === "deal") {
deal()
} else if (buttonMode === "shuffle") {
shuffle()
}
}

How can I call a function from another component react

I am trying to call a function from a different component but when I console.log('hi') it appear but it didn't call the messageContext.
Here is my follwing code from Invitees.js:
const [showPreview, setShowPreview] = useState(false);
const toggleUserPreview = () => {
setShowPreview(!showPreview);
};
{showPreview && (
<ResultsWrappers togglePreview={toggleUserPreview}>
<UserPreview
userInfo={applicant}
skillStr={applicant.Skills}
togglePreview={toggleUserPreview}
/>
</ResultsWrappers>
)}
Here is the component have the function I want to call UserPreview.js:
import { useMessageContextProvider } from "../context/MessageContext";
const UserPreview = ({ userInfo, skillStr, togglePreview }) => {
const messageContextProvider = useMessageContextProvider();
const messageUser = () => {
togglePreview();
messageContextProvider.updateActiveUserToMessage(userInfo);
console.log('hi');
};
...
};
Here is my messageContext:
import { createContext, useContext, useState } from "react";
const messageContext = createContext();
export const MessageContextProvider = ({ children }) => {
const [activeUserToMessage, setActiveUserToMessage] = useState({});
const [isOpenMobileChat, toggleMobileChat] = useState(false);
const updateActiveUserToMessage = (user) => {
setActiveUserToMessage(user);
};
return (
<messageContext.Provider
value={{
updateActiveUserToMessage,
activeUserToMessage,
isOpenMobileChat,
toggleMobileChat,
}}
>
{children}
</messageContext.Provider>
);
};
export const useMessageContextProvider = () => {
return useContext(messageContext);
};
When the messageContext called it should open the chatbox like this:
The code you showing is not enough to say it for 100%, but it seems like toggleUserPreview - function called twice, so it reverted to original boolean value.
One time as <ResultsWrappers togglePreview={toggleUserPreview}/>
and second time as <UserPreview togglePreview={toggleUserPreview}/>.

Categories