Timeout not clearing when used in onChange function - reactjs

I'm creating a search function for a website that I'm working on. When a user types in a keyword, a get request will be sent to retrieve the matching information. However, it feels very wasteful to have it fire every time a key is pressed. Say the user wants to search for sandwiches, they will most likely enter that in pretty quick succession I just want to fire it after a certain amount of time after the user stopped typing(say 250ms). My idea is to set a timeout, which will be cleared on a consecutive keystroke. Unfortunately, the timeout does not reset and just ends up being delayed. I tried having it in a useEffect hook, and then the timeout worked fine but I had some other problems which prompted me to try and do it this way.
const onChangeBrand = (e) => {
const brand = e.target.value
setBrand(brand)
const timeout = setTimeout(()=>{
url.get(`brands?search=${encodeURI(brand.toLowerCase())}&rows=5`)
.then(res => {
setBrands(res.data)
if(res.data.length === 1 && res.data[0].urlEncoding === encodeURI(brand.toLowerCase())){
setPageType("models")
}else{
setPageType("brands")
}
})
},2500)
return () => clearTimeout(timeout);
}
Any help would be much appreciated!

You've got the right idea but the wrong execution. Returning a function from an onChange handler inherently does nothing–this would have worked fine with useEffect so I see where it came from. This pattern is known as throttling / debouncing a function and there are tons of premade libraries out there to help you throttle a function (like lodash.throttle) but it's perfectly cool to spin your own!
The key here would be:
Use a timeout variable that is scoped outside the method
At the start of execution of your onChange, check to see if the timeout variable has a value–if it does, clear it.
Execute onChange, assign new timeout.
You could use a ref or something here but I personally think it's easiest to define your timeout holder outside the scope of your component entirely.
let CHANGE_TIMEOUT = null;
function MyComponent(props) {
// .. component code
const onChangeBrand = (e) => {
if (CHANGE_TIMEOUT) {
// we already have a previous timeout, clear it.
clearTimeout(CHANGE_TIMEOUT);
}
const brand = e.target.value
setBrand(brand)
// Set the timeout again
CHANGE_TIMEOUT = setTimeout(()=>{
url.get(`brands?search=${encodeURI(brand.toLowerCase())}&rows=5`)
.then(res => {
setBrands(res.data)
if(res.data.length === 1 && res.data[0].urlEncoding === encodeURI(brand.toLowerCase())){
setPageType("models")
}else{
setPageType("brands")
}
})
},2500);
}
// .. other component code here
}

Related

Limit for function that can run before browser goes to background (mobile browser)

I have code to handle visibilitychange like below
As far as I know, browser has fraction of time to run function before the browser's visibility become hidden.
Furthermore, the OS(android or iOS) can freely decide to stop the any running functions and put the browser into background.
Here is my simple code:
const Component = () => {
const [isHidden, setIsHidden] = useState(false)
useEffect( function whenHidden() => {
if(isHidden){
// when page visible === false,
// the order of execution:
// setIsHidden(true) > useEffect() > whenHidden() > anotherLongLogic() > reactDOM rerender
// considering this function is 3rd in order above, does this function guaranteed to run?
anotherLongLogic()
}
},[isHidden])
useEffect(() => {
window.addEventListener('visibilitychange', (e) => {
if(document.hidden){
// the order when hidden is
setIsHidden(true)
// how long does this function allow to run?
anotherLongLogic()
}
})
})
}
My Questions are (for mobile browser mainly):
with react, is it guaranteed that setState and useEffect will run within the limited time?
Is there any limit for the anotherLongLogic (how much time is allowed to run this function)?
can we block or tell the OS to give more time to browser to complete any functions that is still running?
any best practice to handle this case?

How to correctly display the stream output of a container in docker desktop extension

As shown here using docker extension API's you can stream the output of a container but when I try to store the data.stdout string for each line of log it simply keep changing like if it's a object reference....I even tried to copy the string using data.stdout.slice() or transforming it in JSON using JSON.stringify(data.stdout) in order to get a new object with different reference but doesn't work :/
...
const[logs,setLogs]=useState<string[]>([]);
...
ddClient.docker.cli.exec('logs', ['-f', data.Id], {
stream: {
onOutput(data): void {
console.log(data.stdout);
setLogs([...logs, data.stdout]);
},
onError(error: unknown): void {
ddClient.desktopUI.toast.error('An error occurred');
console.log(error);
},
onClose(exitCode) {
console.log("onClose with exit code " + exitCode);
},
splitOutputLines: true,
},
});
Docker extension team member here.
I am not a React expert, so I might be wrong in my explanation, but
I think that the issue is linked to how React works.
In your component, the call ddClient.docker.cli.exec('logs', ['-f'], ...) updates the value of the logs state every time some data are streamed from the backend. This update makes the component to re-render and execute everything once again. Thus, you will have many calls to ddClient.docker.cli.exec executed. And it will grow exponentially.
The problem is that, since there are many ddClient.docker.cli.exec calls, there are as many calls to setLogs that update the logs state simultaneously. But spreading the logs value does not guarantee the value is the last set.
You can try the following to move out the Docker Extensions SDK of the picture. It does exactly the same thing: it updates the content state inside the setTimeout callback every 400ms.
Since the setTimeout is ran on every render and never cleared, there will be many updates of content simultaneously.
let counter = 0;
export function App() {
const [content, setContent] = React.useState<string[]>([]);
setTimeout(() => {
counter++;
setContent([...content, "\n " + counter]);
}, 400);
return (<div>{content}</div>);
}
You will see weird results :)
Here is how to do what you want to do:
const ddClient = createDockerDesktopClient();
export function App() {
const [logs, setLogs] = React.useState<string[]>([]);
useEffect(() => {
const listener = ddClient.docker.cli.exec('logs', ['-f', "xxxx"], {
stream: {
onOutput(data): void {
console.log(data.stdout);
setLogs((current) => [...current, data.stdout]);
},
onError(error: unknown): void {
ddClient.desktopUI.toast.error('An error occurred');
console.log(error);
},
onClose(exitCode) {
console.log("onClose with exit code " + exitCode);
},
splitOutputLines: true,
},
});
return () => {
listener.close();
}
}, []);
return (
<>
<h1>Logs</h1>
<div>{logs}</div>
</>
);
}
What happens here is
the call to ddClient.docker.cli.exec is done in an useEffect
the effect returns a function that closes the connection so that when the component has unmounted the connection to the spawned command is closed
the effect takes an empty array as second parameter so that it is called only when the component is mounted (so not at every re-renders)
instead of reading logs a callback is provided to the setLogs function to make sure the latest value contained in data.stdout is added to the freshest version of the state
To try it, make sure you update the array of parameters of the logs command.

Is it possible to queue function calls in react?

I'm having a page with a textbox on (Textfield from fluent UI). I want to update a server stored value every time the user changes the text. I do not want the user to click a separate 'Save' button since that will interfer with the workflow on the page.
Everytime the user changes the text an event is raised with the new text. Simply enough I catch the event and updates the value. However, an event is raised for every character entered creating a lot of events. And if the user types quickly enough, the server will raise an error saying that I can't update the value cause there is already an update going on.
Is there a way to queue function calls? Meaning that I put the changes in a queue and a separate function handles them one after another?
<TextField
value={info.location}
onChange={locationChanged}
/>
const locationChanged = React.useCallback(
(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
// update server here
},
[]
);
You are probably looking for a debounce function. This function will cancel the call if time between previous call and current call is less than a given time.
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
const debounceChange = debounce(locationChanged);
<TextField
value={info.location}
onChange={debounceChange}
/>

Issues accessing react state in firestore onSnapshot listener

I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.

Infinite loop when set with React Hooks

there is such a production in react. I want the user to delete posts that they have shared. I do this:
When the user logs in, I keep the user id as a cookie.
I get the user name of the user that is equivalent to the id in the database with the captured id - HaoAsk
I check with the username of the user who shares the post with the username I received
if it is equal, I allow the user with setOwner.
But it goes into an endless loop.
const user = response.data.filter((dataItem) => (dataItem._id === userid));
const userNickname = JSON.stringify(user.map((value) => { return value.nickName }))
const nickName = userNickname.slice(2, -2)
const deneme1 = posts.filter( (data) => (data.who === nickName))
deneme1 ? setOwner([{ who: nickName, status: true}]) : setOwner([{ status: false }])
console.log(owner)
When I use the following code, everything I write with console.log enters an infinite loop. I couldn't figure it out.
deneme1 ? setOwner ([{who: nickName, status: true}]): setOwner ([{status: false}])
Thanks in advance, Yours!
For any functional component, you normally want to make sure you don't use set outside the event function.
In your case,
const onClick = e => {
// safe to use set method
setOwner()
}
return (
<Component onClick={onClick} user={user} />
)
Because everything inside a functional component is inside the render cycle. It'll run when react try to refresh the display. In your case you set a state variable which triggers the render and then set the variable and then the infinite circle :)
What this means is that you have to find out an event after you initialize your component. In your case, it's a bit tricky, because you want to call this event right away automatically for you.
Please refer to something like this, How to call loading function with React useEffect only once

Resources