Clean way to use React hook state in a debounced callback? - javascript

function Foo() {
const [state, setState] = useState(0);
const cb = useCallback(debounce(() => {
console.log(state);
}, 1000), []);
return ...;
}
In this example, state can become stale in the callback. One way I can think of to fix this is something like:
function Foo() {
const [state, setState] = useState(0);
const cbHelper = useCallback(debounce((state2) => {
console.log(state2);
}, 1000), [])
const cb = () => cbHelper(state);
return ...;
}
However, this looks pretty messy. Is there a cleaner/better way to do this?
Edit:
I can't just do the following because debounce won't work:
useCallback(debounce(() => {
console.log(state);
}, 1000), [state]);

I've used use-debounce to manage that
import { useDebouncedCallback } from 'use-debounce';
function Input({ defaultValue }) {
const [value, setValue] = useState(defaultValue);
// Debounce callback
const [debouncedCallback] = useDebouncedCallback(
// function
(value) => {
setValue(value);
},
// delay in ms
1000
);
// you should use `e => debouncedCallback(e.target.value)` as react works with synthetic evens
return (
<div>
<input defaultValue={defaultValue} onChange={(e) => debouncedCallback(e.target.value)} />
<p>Debounced value: {value}</p>
</div>
);
}

Related

How to handle callbacks for dependencies of children/custom hooks

Wondering what the best method is to handle callbacks that get passed to children/into custom hooks that are used inside useEffect blocks (or any hook with dependency arrays)
This is going by the assumption we don't have access to that callback to wrap in a useCallback or define it outside of the parent/changing scope ourselves.
Is there better ways than suggested below? Do I need to worry about stale fns/closures?
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = (value) => {
setValue(value);
}
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
useEffect(() => {
// ...
onChange(changingValue);
}, [changingValue, onChange]); // <- always changing
}
// Is there a drawback to this approach?
function Child({ onChange }) {
const callbackRef = useRef();
callbackRef.current = onChange;
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue])
}
// or should it be updated in useLayoutEffect?
function Child({ onChange }) {
const callbackRef = useRef();
useLayoutEffect(() => {
callbackRef.current = onChange;
});
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue]);
}
I think the best is the first one, it's simpler, and if you use a useCallback where you define onChange, then the function will be called just one time every time the value changes, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
const [changingValue, setChangingValue] = useState('');
useEffect(() => {
onChange(changingValue);
}, [changingValue, onChange]);
return (
<input type="text" value={changingValue} onChange={(event) => setChangingValue(event.target.value)} />
)
}
By the way, in this particular case, of course you can call the function directly on the onChange, passing the value, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} value={value} />
)
}
function Child({ onChange, value }) {
return (
<input type="text" value={value} onChange={(event) => onChange(event.target.value)} />
)
}

How to stop calling useEffect again

Container
import { InputField } from './InputField';
const sleep = (time: number) => new Promise((res) => setTimeout(res, time, ''));
export const Container = () => {
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
(async () => await sleep(1000))();
async function fetchMyAPI(time, value) {
await sleep(time);
setInputValue(value);
}
fetchMyAPI(1000, 'vbc1');
fetchMyAPI(2000, 'dgi1');
}, []);
const inputChange = (value) => {
setInputValue(value);
};
return <InputField inputValue={inputValue} inputChange={inputChange} />;
};
InputField
export const InputField = ({
inputValue,
inputChange,
}: {
inputValue: string;
inputChange: (value: string) => void;
}) => {
const [value, setValue] = React.useState('');
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
const handleChange = (event) => {
setValue(event.target.value);
inputChange(event.target.value + '1');
};
return <input value={value} onChange={handleChange} />;
};
inputValue above can change multiple times.
also a local variable in input is used to display , and inputValue is slightly different from it. So when we keep track of InputValue , we pass the cleared data to the local variable. And vice versa, we modify the data to put in the inputValue.
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
Every time we call handleChange : we do setValue and inputChange. Thus, we change the value variable and the inputValue variable. After the inputValue is changed, useEffect is called which observes the inputValue. And overwrites exactly the same value of the Value variable. This is problem!
What is the correct solution to this problem?
You can create a boolean state effectRan to track whether the effect already ran or not, and only invoke the effect's logic if effectRan == false, then set it to true.
When the effect runs again with it as true, have it set it back to false to prepare to run again in the next change.
I changed the code a bit to highlight the approach:
const {useState, useEffect } = React
const InputField = () => {
const [value, setValue] = React.useState('');
const [effectRan, setEffectRan] = useState(true);
React.useEffect(() => {
if (!effectRan) {
setValue(prev => prev + '-');
setEffectRan(true)
console.log('Effect just ran');
} else {
setEffectRan(false)
}
}, [value]);
const handleChange = (event) => {
setValue(event.target.value);
};
return <input onChange={handleChange} value={value} />;
};
ReactDOM.render(<InputField />, root)
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Debounce function not working as expected, as if it's not being executed

I'm trying to avoid using lodash in my project, and I've eliminated everything except this one debounce function. According to this You-Dont-Need, this debounce should work just fine. But, for some reason, it's not being called.
Here's what I have,
import * as React from "react";
export default function DebouncedSearch() {
const [value, setValue] = React.useState("");
const debounce = (
func: (...params: any[]) => any,
wait: number,
immediate = false
) => {
let timeout;
return function () {
const later = () => {
timeout = null;
if (!immediate) func();
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func();
};
};
const handleChange = React.useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
debounce(() => {
setValue(value);
}, 500);
},
[]
);
return <input value={value} onChange={handleChange} />;
}
What am I doing wrong here?
Edit
If I do this,
const handleChange = React.useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
debounce(() => {
console.log("called");
setValue(value);
}, 500)();
},
[]
);
It prints called as many times as the onChange event fires, not once.
One way of doing this and avoiding render state issues, is to use a useRef.
Below I've just used a setInterval, instead of debounce logic, I prefer that to debounce..
But if you prefer debounce, it should be trivial to alter to do it using that instead.
const {useState, useEffect, useRef} = React;
function MyInput() {
const [value, setValue] = useState('');
const v = useRef({value: '', changed:false});
useEffect(() => {
const i = setInterval(() => {
if (v.current.changed) {
console.log('change: ' + v.current.value);
v.current.changed = false;
}
}, 500);
return () => clearInterval(i);
}, []);
return <input
onChange={(e) => {
setValue(e.target.value);
v.current.changed = true;
v.current.value = e.target.value;
}}
value={value}
/>;
}
ReactDOM.render(<MyInput/>, document.querySelector('#mount'));
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="mount"></div>

Using React useEffect hook with rxjs mergeMap operator

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;

Executing async code on update of state with react-hooks

I have something like:
const [loading, setLoading] = useState(false);
...
setLoading(true);
doSomething(); // <--- when here, loading is still false.
Setting state is still async, so what's the best way to wait for this setLoading() call to be finished?
The setLoading() doesn't seem to accept a callback like setState() used to.
an example
class-based
getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (this.state.pagesSeen.includes(this.state.page + 1)) {
return this.setState({
page: this.state.page + 1,
});
}
if (this.state.prefetchedOrders) {
const allOrders = this.state.orders.concat(this.state.prefetchedOrders);
return this.setState({
orders: allOrders,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
prefetchedOrders: null,
});
}
this.setState(
{
isLoading: true,
},
() => {
getOrders({
page: this.state.page + 1,
query: this.state.query,
held: this.state.holdMode,
statuses: filterMap[this.state.filterBy],
})
.then((o) => {
const { orders } = o.data;
const allOrders = this.state.orders.concat(orders);
this.setState({
orders: allOrders,
isLoading: false,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
// Just in case we're in the middle of a prefetch.
prefetchedOrders: null,
});
})
.catch(e => console.error(e.message));
},
);
};
convert to function-based
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
};
In the above, we want to run each setWhatever call sequentially. Does this mean we need to set up many different useEffect hooks to replicate this behavior?
useState setter doesn't provide a callback after state update is done like setState does in React class components. In order to replicate the same behaviour, you can make use of the a similar pattern like componentDidUpdate lifecycle method in React class components with useEffect using Hooks
useEffect hooks takes the second parameter as an array of values which React needs to monitor for change after the render cycle is complete.
const [loading, setLoading] = useState(false);
...
useEffect(() => {
doSomething(); // This is be executed when `loading` state changes
}, [loading])
setLoading(true);
EDIT
Unlike setState, the updater for useState hook doesn't have a callback, but you can always use a useEffect to replicate the above behaviour. However you need to determine the loading change
The functional approach to your code would look like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const prevLoading = usePrevious(isLoading);
useEffect(() => {
if (!prevLoading && isLoading) {
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
}
}, [isLoading, preFetchedOrders, orders, page, pagesSeen]);
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
};
Wait until your component re-render.
const [loading, setLoading] = useState(false);
useEffect(() => {
if (loading) {
doSomething();
}
}, [loading]);
setLoading(true);
You can improve clarity with something like:
function doSomething() {
// your side effects
// return () => { }
}
function useEffectIf(condition, fn) {
useEffect(() => condition && fn(), [condition])
}
function App() {
const [loading, setLoading] = useState(false);
useEffectIf(loading, doSomething)
return (
<>
<div>{loading}</div>
<button onClick={() => setLoading(true)}>Click Me</button>
</>
);
}
Created a custom useState hook which works similar to the normal useState hook except that the state updater function for this custom hook takes a callback that will be executed after the state is updated and component rerendered.
Typescript Solution
import { useEffect, useRef, useState } from 'react';
type OnUpdateCallback<T> = (s: T) => void;
type SetStateUpdaterCallback<T> = (s: T) => T;
type SetStateAction<T> = (newState: T | SetStateUpdaterCallback<T>, callback?: OnUpdateCallback<T>) => void;
export function useCustomState<T>(init: T): [T, SetStateAction<T>];
export function useCustomState<T = undefined>(init?: T): [T | undefined, SetStateAction<T | undefined>];
export function useCustomState<T>(init: T): [T, SetStateAction<T>] {
const [state, setState] = useState<T>(init);
const cbRef = useRef<OnUpdateCallback<T>>();
const setCustomState: SetStateAction<T> = (newState, callback?): void => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Javascript solution
import { useEffect, useRef, useState } from 'react';
export function useCustomState(init) {
const [state, setState] = useState(init);
const cbRef = useRef();
const setCustomState = (newState, callback) => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Usage
const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
// Function called after state update and component rerender
})
you can create a async state hooks
const useAsyncState = initialState => {
const [state, setState] = useState(initialState);
const asyncSetState = value => {
return new Promise(resolve => {
setState(value);
setState((current) => {
resolve(current);
return current;
});
});
};
return [state, asyncSetState];
};
then
const [loading, setLoading] = useAsyncState(false)
const submit = async () => {
await setLoading(true)
dosomething()
}
I have a suggestion for this.
You could possibly use a React Ref to store the state of the state variable. Then update the state variable with the react ref. This will render a page refresh, and then use the React Ref in the async function.
const stateRef = React.useRef().current
const [state,setState] = useState(stateRef);
async function some() {
stateRef = { some: 'value' }
setState(stateRef) // Triggers re-render
await some2();
}
async function some2() {
await someHTTPFunctionCall(stateRef.some)
stateRef = null;
setState(stateRef) // Triggers re-render
}
Pass a function to the setter instead of value!
instead of giving a new value to the setter directly, pass it an arrow function that takes the current state value and returns the new value.
it will force it to chain the state updates and after it's done with all of them, it will rerender the component.
const [counter, setCounter] = useState(0);
const incrementCount = () => {
setCounter( (counter) => { return counter + 1 } )
}
now every time incrementCount is called, it will increase the count by one and it will no longer be stuck at 1.

Categories