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>
</>
);
}
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'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);
};
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} />;
}
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;
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>
);
}
}