Lodash Debounce sends request without callback React JS - javascript

I'm having problems with setting up lodash debounce in the function to make an API request. For some reason callback doesn't happen and the value sends every time I type.
import debounce from "lodash/debounce";
const handleChange = (event) => {
const { value } = event.target;
const debouncedSave = debounce((nextValue) => dispatch(movieActions.getMovies(nextValue), 1000));
debouncedSave(value);
};
I'm using material ui and have this in return:
<Autocomplete
onInputChange={handleChange}
/>

Your debounced function is created multiple times for each change event and that causes the problem. I will use a simplified example with a simple input and a console.log instead of your dispatch, but you can apply the solution to your case as well.
The simplest solution would be to move the debouncedSave declaration outside your component.
const debouncedSave = debounce((nextValue) => console.log(nextValue), 1000);
export default function App() {
const handleChange = (e) => {
const { value } = e.target;
debouncedSave(value);
};
return <input onChange={handleChange} />;
}
or else if you want to keep the debounced function declaration inside your component you can use a ref, to create and use the same instance each time, no matter the re-renders:
export default function App() {
const debouncedSaveRef = useRef(
debounce((nextValue) => console.log(nextValue), 1000)
);
const handleChange = (e) => {
const { value } = e.target;
debouncedSaveRef.current(value);
};
return <input onChange={handleChange} />;
}

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>
</>
);
}

Getting API with useEffect, inside a handle change function

I'm using the YTS API and I need to change the link for the call, I have to use
?query_term= and add the text that the user is typing, for autocomplete. I'm using mantine components for the autocomplete. I tried putting the call inside the handlechange function, but this is not possible.
const [movieNames, setMovieNames] = useState([])
const onChangeHandler = (text) => {
useEffect(() => {
const loadMovieNames = async () => {
const response = await axios.get('https://yts.mx/api/v2/list_movies.json?query_term='+text);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames)
}
loadMovieNames()
}, [])
}
.
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={e => onChangeHandler(e.target.value)}
/>
You MUST use hooks in the execution context of Function Component, you used the useEffect inside a function not in the execution context of Function Component.
const YourComponent = () => {
const [movieNames, setMovieNames] = useState([]);
const loadMovieNames = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach((i) => {
arrayOfMoviesNames.push(i.title);
});
setMovieNames(arrayOfMoviesNames);
};
return (
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(value) => loadMovieNames(value)}
/>
);
};
It is also possible without useEffect, so without making it so complicated by using useEffect and onChangeHandler both, only use onChangeHandler function to update the movieNames and it will automatically update the DOM texts (I mean where ever you use)...
import React, { useState } from "react";
function MoviesPage(props) {
const [ movieNames, setMovieNames ] = useState([]);
const [ searchValue, setSearchValue ] = useState("");
const onChangeHandler = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames);
}
return (
<div>
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(e) => onChangeHandler(e.target.value)}
/>
</div>
);
}
export default MoviesPage;
...and just to clarify, you can use useEffect in case of API if you want to initialize the page with the API data. You can use this hook if you don't have any onChange handlers. Another way you can approach is you can update a state hook (like searchData) on the change of the Search Bar, and lastly add the the searchData variable to the useEffect dependency array:
useEffect(() => {
// use the searchData variable to populate or update the page
// ...
},
[
searchData, // <-- talking about this line
]);
So, this was my solution. Hope this helps you mate!
useEffect is a hook, which executes on state change, So keep the useEffect funtion outside the onChangeHandler and add a new state for 'query param' and setQueryState(text) inside the onChangeHandler, and put the state param as dependency in useEffect, So whenever this state gets changed this will call the use effect function automatically.

Wait for change of prop from parent component after changing it from a child in React

I have rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>

React Context - State value is not up-to-date inside a function

I have the following context:
import React, { createContext, useState } from "react";
const OtherUsersContext = createContext(null);
export default OtherUsersContext;
export function OtherUsersProvider({ children }) {
const [otherUsers, setOtherUsers] = useState(new Map([]));
const addUser = (userId, userData) => {
setOtherUsers(
(prevOtherUsers) => new Map([...prevOtherUsers, [userId, userData]])
);
};
const updateUser = (userId, userData, merge = true) => {
...
};
const getUser = (userId) => otherUsers.get(userId);
const resetUsers = () => {
setOtherUsers(new Map([]));
};
return (
<OtherUsersContext.Provider
value={{
addUser,
updateUser,
getUser,
resetUsers,
}}
>
{children}
</OtherUsersContext.Provider>
);
}
In my app, when a user signs out, I need to reset this context's map, using the function "resetUsers".
Currently this is working good, but there has no sense to reset the map if it has no values, so I have changed the "resetUsers" function to:
const resetUsers = () => {
if(otherUsers.size) {
setOtherUsers(new Map([]));
}
}
And, this is not working good, because inside resetUsers, otherUsers.size is always 0. Something which disturbs me because outside the function, the value is the correct one...
...
const resetUsers = () => {
console.log(otherUsers.size); // 0
setOtherUsers(new Map([]));
};
console.log(otherUsers.size); // 5
return ( ...
Any ideas?
The functional updates part of the hooks docs. says:
If the new state is computed using the previous state, you can pass a function to setState.
So instead of just passing the new value to your setter, you can pass a function that depends on the previous state.
This means that you can do:
const resetUsers = () => {
setOtherUsers(prevOtherUsers => prevOtherUsers.size ? new Map([]): prevOtherUsers);
}
One tip, if you are not getting the most updated state value inside a function, then wrap it inside an useCallback.
Try this:
const resetUsers = useCallback(() => {
if (otherUsers.size > 0) {
console.log(otherUsers.size); // 5
setOtherUsers(new Map([]));
}
}, [otherUsers]);

Adding debounce to mobx function

I am creating a React App that makes search request to server as the user types. I want to debounce this search request, but not sure how to implement it in my existing code:
Mobx Store:
// function which initiates a fetch request to server
#action searchPlanet = async (event) => {
this.searchString = event.target.value;
this.planets = await getPlanets(this.searchString);
}
React Component calling searchPlanet:
const Search = observer(({ store }) => {
const planetList = toJS(store.planets);
return (
<div>
<div className={style.search_container}>
<input type="text" id="search" onChange={e => store.searchPlanet(e)} value={store.searchString} placeholder="search planet" />
</div>
</div>
)
})
I can't use debounce function directly on onChange because that will also delay the re-rendering of Search component, so the user will see the typed text after some time. But I am not able to figure out to how to implement debounce function in my store? I can do something like:
import _ from lodash
#action searchPlanet = async (event) => {
this.searchString = event.target.value;
this.planets = await getPlanets(this.searchString);
}
debounceSearch = _.debounce(this.searchPlanet, 250);
The issue with this is that I can't call debounceSearch directly from Search component because of reason mentioned above. But I want to debounce getPlanets function, which returns a promise (I am not sure if Lodash debounce function can return the promise returned by the wrapped function)?
Instead of assigning a value to planets in your searchPlanet action, you could do it in the debounced function instead.
Example
#observer
class App extends Component {
#observable value = "";
#observable query = "";
onChange = action(event => {
const { value } = event.target;
this.value = value;
this.search(value);
});
search = debounce(action(query => {
this.query = query;
}), 250);
render() {
const { value, query, onChange } = this;
return (
<div>
<input value={value} onChange={onChange} />
<div>{query}</div>
</div>
);
}
}

Categories