I'm using react-select loading the results from an api and debouncing the queries with lodash.debounce:
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import {search} from './api';
const _loadSuggestions = (query, callback) => {
return search(query)
.then(resp => callback(resp));
};
const loadSuggestions = debounce(_loadSuggestions, 300);
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
It seems to work fine when I enter something in the searchbox for the first time (query is debounced and suggestions populated correctly), but if I try to enter a new value, a second fetch query is fired as expected, but the suggestions coming from that second call are not displayed (and I don't get the "Loading..." message that react-select displays).
When I don't debounce the call the problem seems to go away (for the first and any subsequent calls):
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import {search} from '../../api';
const loadSuggestions = (query, callback) => {
return search(query)
.then(resp => callback(resp));
};
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
Any idea what is going on? Any help to understand this issue would be much appreciated.
M;
I was aslo facing the same issue and got this solution.
If you are using callback function, then You DON'T need to return the
result from API.
Try removing return keyword from _loadSuggestions function.
as shown below
import React, {useState} from 'react';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import {search} from './api';
const _loadSuggestions = (query, callback) => {
search(query)
.then(resp => callback(resp));
};
const loadSuggestions = debounce(_loadSuggestions, 300);
function SearchboxTest() {
const [inputValue, setInputValue] = useState("");
const onChange = value => {
setInputValue(value);
};
return (
<AsyncSelect
value={inputValue}
loadOptions={loadSuggestions}
placeholder="text"
onChange={onChange}
/>
)
}
Use react-select-async-paginate package - This is a wrapper on top of react-select that supports pagination. Check it's NPM page
React-select-async-paginate works effectively with internal denounce. You can pass debounce interval in the props.
<AsyncPaginate
value={value}
loadOptions={loadOptions}
debounceTimeout={300}
onChange={setValue}
/>
Here is codesandbox example
For all of you people still struggling with this and don't want to install lodash only for debounce function like I do, here's my solution
debounce function
export default function debounce(fn, delay = 250) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args);
}, delay);
};
}
debounce implementation
const loadOptionsDebounced = useCallback(
debounce((inputValue: string, callback: (options: any) => void) => {
fetchOptions(inputValue).then(options => callback(options))
}, 500),
[]
);
fetchOptions() is my async fetch function that returns array of options
Maybe this will help someone.
freeze = false //mark delay
timer //saved timer
loadAddress = async (strSearch: string) => {
this.freeze = true //set mark for stop calls
return new Promise(async (res, err) => { //return promise
let p = new Promise((res, err) => {
if(this.freeze) clearTimeout(this.timer) //remove prev timer
this.timer = setTimeout(async () => {
this.freeze = false
const r = await this.load(strSearch)//request
res(r);
}, 2000)
})
p.then(function (x) {
console.log('log-- ', x);
res(x);
})
});
};
What worked for me was, instead of making use of the default debounce from lodash, making use of the debounce-promise package:
import React from 'react';
import debounce from 'debounce-promise';
import AsyncSelect from 'react-select/async';
const debounceFunc = debounce(async () => {
return [] // SelectOptions
}, 200);
export function MyComponent() {
return <AsyncSelect loadOptions={debounceFunc} defaultOptions />
}
In my solution help me following.
I have AsyncSelect where i want data from https://github.com/smeijer/leaflet-geosearch. My provider is:
const provider = new OpenStreetMapProvider({
params: {
countrycodes: "cz",
limit: 5
}
});
const provider you will see below one more time.
<AsyncSelect className="map-search__container"
classNamePrefix="map-search"
name="search"
cacheOptions
components={{DropdownIndicator, IndicatorSeparator, Control, NoOptionsMessage, LoadingMessage}}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
loadOptions={getData}
onChange={handleChange}
isClearable
placeholder={inputSave || ""}
value=""
inputValue={input}
onMenuClose={handleMenuClose}
onInputChange={handleInputChange}
onFocus={handleFocus}
blurInputOnSelect
defaultOptions={true}
key={JSON.stringify(prevInputValue)}
/>
cachceOptions - is basic
components - my components
loadOptions - is the most important!
anything else is not important
const getData = (inputValue: string, callback: any) => {
debouncedLoadOptions(inputValue, callback);
}
const debouncedLoadOptions = useDebouncedCallback(fetchData, 750);
import {useDebouncedCallback} from 'use-debounce';
This use was my key for solution. I tried debounce from lodash, but i had still problem with not working debouncing. This use help me and did my problem solve.
const fetchData = (inputValue: string, callback: any) => {
return new Promise((resolve: any) => {
resolve(provider.search({query: inputValue || prevInputValue}));
}).then((json) => {
callback(json);
});
};
prevInputValue is props from parent... (you don't need it)
Callbacks was second key. First was useDebouncedCallback.
#sorryForMyEnglish
Related
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.
I am trying to make a socket io simple chat app and the connection works fine. The problems comes with the const renderChat. The console.log is read and correctly printed (with the value in the textbox) also if I put a static text it is also displayed on the frontend, however, for some reason it doesn't print
{msg.data["message"]}
which has to print each value in an array. Also I am almost certain that the array is emptied every time and the useEffect doesn't work properly but I can't really test that yet. Would be glad if I get solution to both problems!
import './App.css';
import io from 'socket.io-client'
import { useEffect, useState } from 'react'
import React from 'react';
import ReactDOM from "react-dom/client";
const socket = io.connect("http://localhost:3001");
function App() {
const [message, setMessage] = useState("");
const [chat, setChat] = useState([]);
const sendMessage = () => {
socket.emit("send_message", { message });
};
const renderChat = () => {
return (
chat.forEach(msg => {
<h3>{msg.data["message"]}</h3>
console.log(msg.data)
})
)
}
useEffect(() => {
socket.on("receive_message", message => {
setChat([...chat, message]);
});
}, [socket])
return (
<div className="App">
<input placeholder="Message..." onChange={(event) => {
setMessage(event.target.value);}}
/>
<button onClick={sendMessage}>Send Message</button>
<h1>Message:</h1>
{renderChat()}
</div>
);
}
export default App;
EDIT:
Thanks to Ibrahim Abdallah, now the printing works. The only thing that doesn't work is as I feared, storing the information. Here is the piece of code
useEffect(() => {
socket.on("receive_message", message => {
setChat([...chat, message]);
});
}, [socket])
For some reason when I enter for example "text1" it saves it but then if I enter "text2" it overwrites the first value "text1".
modify your renderChat function to use map instead and the console log message should be before the return statement of the map function
const renderChat = () => {
return (
chat.map(msg => {
console.log(msg.data)
return (
<h3>{msg.data["message"]}</h3>
)
})
)
}
Please use map instead of forEach
const renderChat = () => {
return (
chat.map(msg => {
<h3>{msg.data["message"]}</h3>
console.log(msg.data)
})
)
}
Use map instead of forEach
map actually returns an array of the returned results.
forEach on the other hand does not return anything.
const renderChat = () => {
return (
chat.map(msg => {
console.log(msg.data);
return <h3>{msg.data["message"]}</h3>;
})
)
}
I am trying to implement debouncing in my app, however, the most I am able to achieve is to debounce the speed of the input. The gist of the App, is that it first takes input from the user, to generate a list of cities, according to the input and when selected, will provide a forecast for the whole week.
Here is the original code, without debouncing:
import React, { useState, useEffect } from 'react';
import * as Style from './Searchbar.styles';
import { weatherAPI } from '../API/api';
import endpoints from '../Utils/endpoints';
import { minCharacters } from '../Utils/minChars';
import MultiWeather from './MultiCard';
import useDebounce from '../Hooks/useDebounce';
export default function SearchBar() {
const [cityName, setCityName] = useState('');
const [results, setResults] = useState([]);
const [chooseCityForecast, setChooseCityForecast] = useState([]);
useEffect(() => {
if (cityName.length > minCharacters) {
loadCities();
}
}, [cityName, loadCities]);
//this is used to handle the input from the user
const cityValueHandler = value => {
setCityName(value);
};
//first API call to get list of cities according to user input
const loadCities = async () => {
try {
const res = await weatherAPI.get(endpoints.GET_CITY(cityName));
setResults(res.data.locations);
} catch (error) {
alert(error);
}
};
//second API call to get the forecast
const getCurrentCity = async city => {
try {
const res = await weatherAPI.get(endpoints.GET_DAILY_BY_ID(city.id));
console.log(res);
setChooseCityForecast(res.data.forecast);
setCityName('');
setResults([]);
} catch (error) {
alert(error);
}
};
return (
<Style.Container>
<h1>Search by City</h1>
<Style.Search>
<Style.SearchInner>
<Style.Input type="text" value={cityName} onChange={e => cityValueHandler(e.target.value)} />
</Style.SearchInner>
<Style.Dropdown>
{cityName.length > minCharacters ? (
<Style.DropdownRow results={results}>
{results.map(result => (
<div key={result.id}>
<span
onClick={() => getCurrentCity(result)}
>{`${result.name}, ${result.country}`}</span>
</div>
))}
</Style.DropdownRow>
) : null}
</Style.Dropdown>
</Style.Search>
{chooseCityForecast && (
<section>
<MultiWeather data={chooseCityForecast} />
</section>
)}
</Style.Container>
);
}
The code above works perfectly, aside from creating an API call everytime I add an additional letter. I have refered to this thread on implementing debouncing. When adjusted to my code, the implementation looks like this:
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
const debouncedHandler = useCallback(debounce(cityValueHandler, 200), []);
But, as mentioned before, this results in debouncing/delaying user input by 200ms, while still creating additional API calls which each extra letter.
If I try to debounce the loadCities function or add a setTimeout method, it will delay the function, but will still make the API calls with each additional letter.
I have a hunch, that I need to remake the logic, which is handling the input, but at this point I am out of ideas.
After some digging I found a simple solution, that will be refactored later, but the gist of it is to move the loadCities function inside the use effect and implenet useTimeout and clearTimeout methods within the useEffect and wrapping the API call function, as seen below:
useEffect(() => {
let timerID;
if (inputValue.length > minCharacters) {
timerID = setTimeout(async () => {
try {
const res = await weatherAPI.get(endpoints.GET_CITY(inputValue));
setResults(res.data.locations);
} catch (error) {
alert(error);
}
}, 900);
}
return () => {
clearTimeout(timerID);
};
}, [inputValue]);
Hope this will help someone.
My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;
I'm trying to create an input field that has its value de-bounced (to avoid unnecessary server trips).
The first time I render my component I fetch its value from the server (there is a loading state and all).
Here is what I have (I omitted the irrelevant code, for the purpose of the example).
This is my debounce hook:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
(I got this from: https://usehooks.com/useDebounce/)
Right, here is my component and how I use the useDebounce hook:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, commitsCount]);
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here is the parent component:
function App() {
const [title, setTitle] = useState("");
useEffect(() => {
setTimeout(() => setTitle("This came async from the server"), 2000);
}, []);
return (
<div className="App">
<h1>Example</h1>
<ExampleTitleInput title={title} />
</div>
);
}
When I run this code, I would like it to ignore the debounce value change the first time around (only), so it should show that the number of commits are 0, because the value is passed from the props. Any other change should be tracked. Sorry I've had a long day and I'm a bit confused at this point (I've been staring at this "problem" for far too long I think).
I've created a sample:
https://codesandbox.io/s/zen-dust-mih5d
It should show the number of commits being 0 and the value set correctly without the debounce to change.
I hope I'm making sense, please let me know if I can provide more info.
Edit
This works exactly as I expect it, however it's giving me "warnings" (notice dependencies are missing from the deps array):
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
// I added this line here
setLastCommittedTitle(props.title || "");
}, [props]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here it is: https://codesandbox.io/s/optimistic-perlman-w8uug
This works, fine, but I'm worried about the warning, it feels like I'm doing something wrong.
A simple way to check if we are in the first render is to set a variable that changes at the end of the cycle. You could achieve this using a ref inside your component:
const myComponent = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
// ...
You can extract it into a hook and simply import it in your component:
const useIsFirstRender = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
return is_first_render.current;
};
Then in your component:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [updatesCount, setUpdatesCount] = useState(0);
const is_first_render = useIsFirstRender(); // Here
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
// I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
if (is_first_render) { // Here
return;
}
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setUpdatesCount(updatesCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, updatesCount]);
// ...
You can change the useDebounce hook to be aware of the fact that the first set debounce value should be set immediately. useRef is perfect for that:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
const firstDebounce = useRef(true);
useEffect(() => {
if (value && firstDebounce.current) {
setDebouncedValue(value);
firstDebounce.current = false;
return;
}
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
I think you can improve your code in some ways:
First, do not copy props.title to a local state in ExampleTitleInput with useEffect, as it may cause excessive re-renders (the first for changing props, than for changing state as an side-effect). Use props.title directly and move the debounce / state management part to the parent component. You just need to pass an onChange callback as a prop (consider using useCallback).
To keep track of old state, the correct hook is useRef (API reference).
If you do not want it to trigger in the first render, you can use a custom hook, such as useUpdateEffect, from react-use: https://github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts, that already implements the useRef related logic.