react-autosuggest with debounce and distinctUntilChanged - reactjs

Intent:
I'm trying to achieve an ideal search field that will not make any API calls untill:
The debounce time of 350ms has been reached
AND until there's a change in the value of the input field.
What I've tried so far:
I've used a Subject to track for changes in the input field. Every time there's a change in the input field and handleSuggestionsFetchRequested is called, I'm pushing a new value down the Subject using searchString$.next(userInput);
And in the useEffect hook, I'm pipeing the searchString$ with debounceTime(350) and distinctUntilChanged(). Something like this:
useEffect(() => {
searchString$
.pipe(
debounceTime(350),
distinctUntilChanged(),
switchMap(searchString =>
ajax(`https://api.github.com/search/users?q=${searchString}`)
),
map((networkResponse: any) => networkResponse.response.items)
)
.subscribe((suggestions: Array<User>) => setSuggestions(suggestions));
}, [searchString$]);
But the API calls are still going everytime there's a change in the userInput.
The Issue:
I think the issue is that every time the value of the input field changes, I'm setting the state as well:
const handleChange = (
event: React.ChangeEvent<{}>,
{ newValue }: Autosuggest.ChangeEvent
) => {
setUserInput(newValue);
};
This is causing the Component to re-render and calling the useEffect, which is eventually making the API call again and again.
I could be wrong.
How to replicate:
I've created a Sample Code Sandbox that replicates the issue.
Thanks a lot in advance.

Thanks to the comments from yurzui on my tweet, I was able to figure out the reason for the issue.
I was creating a new Subject on every reconciliation as the line:
const searchString$: Subject<string> = new Subject<string>();
was right inside my component function.
I moved it out and it was working like a charm.
NOTE: As suggested by yurzui, don't forget to catch errors in the ajax call otherwise the Subject will die.
I've updated the Code Sandbox Sample, just in case somebody needs to refer to it.

Related

user types a long number very slowly in typeahead input box, onSearch cannot get the completed number using react-bootstrap-typeahead version 5.2.1

I am using react-bootstrap-typeahead version 5.2.1 with onSearch method to handle async typeahead search. The issue is when user types a long number(eg: 99999180) very slowly or stop typing after 999999 then continue typing 180(you can see the sequence from the network), we cannot get the returned results as the query parameter shows 9999918 only. However, 999999180 does exists in the database. The same codes are working fine with same scenario based on the old react-bootstrap-typeahead version 3.4.7. Not quite sure this is a defect for version 5.2.1? thanks in advance.
const onSearch = async (query) => {
setIsLoading(true);
try {
const data = await typeaheadSearch(query);
......
} finally {
setIsLoading(false);
......
}
};
Got answer from https://github.com/ericgio/react-bootstrap-typeahead/blob/master/docs/Upgrading.md#v50-breaking-changes release notes
AsyncTypeahead rewritten with hooks
This should generally be a transparent change. There is at least one instance where it could break existing code, however: if your onSearch handler is re-instantiated on each render, this will cancel the debounced function and potentially prevent onSearch from being called. To avoid this, either bind the handler in the constructor or use class properties (if using a class component) or use useCallback with a dependency array (if using a functional component):

RxJS and repeated events

I am new to RxJs in general but am investigating a bug in some React code in which, upon an unrelated action, an old event seems to be emitted and rendered to a display error. Think if you had two buttons that generated two messages somewhere on screen, and clicking one button was showing the message for the other button.
Being new to RxJs I'm not positive where the problem lays. I don't see a single ReplaySubject in the code, only Obserables, Subjects, and BehaviourSubjects. So this is either misuse of an RxJs feature or just some bad logic somewhere.
Anyway I found the code with the related Observable and I'm not quite sure what this person was trying to accomplish here. I have read up on combineLatest, map, and pipe, but this looks like pointless code to me. Could it also be somehow re-emitting old events? I don't see dynamic subscriptions anywhere, especially in this case.
Tldr I don't understand the intent of this code.
export interface IFeedback {
id: number
text: string
}
export interface IFeedbackMessages {
message: IFeedback | undefined
}
feedback$ = new BehaviorSubject<IFeedback | undefined>(undefined)
feedbackNotifs$: Observable<IFeedbackMessages> = combineLatest([
feedback$
]).pipe(
map(([feedback]) => ({
feedback
})
))
I also found this which maybe be an issue. In the React component that displays this message, am I wrong but does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
const FeedbackDisplay: React.FC () => {
const [feedbackNotifications, setFeedbackNotifications] = React.useState<IFeedbackMessages>()
React.useEffect(() =>
{
const sub = notification$.subscribe(setFeedbackNotifications)
return () => sub?.unsubscribe()
}, [notifications$])
}
Could it also be somehow re-emitting old events?
Yes, it probably is. BehaviorSubject has the unique property of immediately emitting the last value pushed to it as soon as you subscribe to it.
It's great when you want to model some persistent state value, and it's not good for events whose actual moment of occurrence is key. It sounds like the feedback messages you're working with fall into the second category, in which case Subject is probably a better choice.
does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
Not exactly. useEffect accepts a callback, and within that callback you can optionally return a "cleanup" function. React will hang onto that function until the effect is triggered again, then it calls it to clean things up (which in this case consists of closing out the subscription) to make room for the next effect.
So in this case, the unsubscribe will only happen when the component is rendered with a new value for notifications$. Also worth pointing out that notifications$ will only change if it's either passed as a prop or created within the component function. If it's defined outside the function (imported from another file for example), you don't need to (and in fact should not) put it into useEffect's dependency array.

Why does axios.get a side effect and axios.post not one?

I'm following a react tutorial about todo-list with jsonplaceholder. In it we need to make a get request to https://jsonplaceholder.typicode.com/todos, and we have this code:
const [todos, setTodos] = useState([]);
useEffect(
() => {
Axios.get('https://jsonplaceholder.typicode.com/todos?_limit=10')
.then(({data}) => {
setTodos(data);
});
}, []);
According to what I read, effects mean that function changes something outside of its scope, and not just return a value, and also with the same parameters it can give different results. At least that's how I understand about side-effects. But how does it apply in the context of this get? Does axios.get change anything, or does it return different value with different calls? I know that making a request to a third party is always an effect, but how does that work? At the same time I have addTodo function:
const addTodo = (title) => {
Axios.post("https://jsonplaceholder.typicode.com/todos", {
title: title,
completed: false
}).then(({data}) => setTodos([...todos, data]));
}
Why does this not need useEffect hook. It seems like addTodo changes the value of todos state, does it not? Why is there not useEffect() this time. Thanks for reading and my apology if there are a little bit too many questions. I don't think they need to be asked seperately.
This has nothing to do with axios and everything to do with when you want to execute your code. Just to clarify, axios doesn't need useEffect to work at all.
useEffect is a hook that allows you to perform actions after your component has mounted. It makes sense to place code in here that you perhaps only need to run once e.g. loading some data, hooking up event handlers, update the DOM etc. In your example, you load your list of Todos here, which makes sense as you probably only need to do this one time.
For code that doesn't need to run right away, like your addTodo, then you can trigger this as and when e.g. on a button click, timer, whatever makes sense for your application.
Note - useEffect can be triggered more than once using dependencies but it's not important for this example

React hooks: am I doing an anti pattern? updating state in function outside the component

I am beginning to use React (hooks only), and facing a strange issue. I am trying to reproduce the problem in a small test code, but can't get it to happen, except in my full blown app. This leads me to wonder if I'm doing something really wrong.
I have an array of objects, declared as a state. I map this array to display its content. Except that nothing gets displayed (the array is filled, but nothing gets displayed). Now if I declare an un-related state, make it a boolean which flips each time my array gets updated, then my array gets displayed properly. As if, in the render phase itself, React did not detect the array's changes.
A few things:
the array gets updated by a socketIO connection, I simulate it here with a timer
I update my array OUTSIDE of my component function, BUT providing the setter function to the update function
I also create part of the render fields outside my component function (this has no effect, just for readability in my full app)
I essence, this is what I am doing:
const updateArray = (setTestArray, setTestTag, addArray) => {
setTestArray(prevTestArray => {
let newTestArray = prevTestArray.map((data, index) => (data + addArray[index]))
return newTestArray
})
setTestTag(prevTag => {
return (!prevTag)
})
}
const renderArray = (currentTestArray) => {
return currentTestArray.map((data, index) => (
<div>
testArray[{index}]={data}
</div>
))
}
function TestPage(props) {
const [testArray, setTestArray] = useState([])
const [testTag, setTestTag] = useState(false)
useEffect(() => {
let samples = 3
let initArray= []
for (let i=0; i<samples;i++) initArray[i] = Math.random()
setTestArray(initArray)
// In real code: setup socket here...
setInterval(() => {
let addArray= []
for (let i=0; i<samples;i++) addArray[i] = Math.random()
updateArray(setTestArray, setTestTag, addArray)
}, 1000)
return (() => {
// In real code, disconnect socket here...
})
}, []);
return (
<Paper>
Array content:
{renderArray(testArray)}
<br/>
Tag: {(testTag)? 'true' : 'false'}
</Paper>
)
}
This works just fine. But, in my full app, if I comment out everything concerning "testTag", then my array content never displays. testArray's content is as expected, updates just fine, but placing a debugger inside the map section show that array as empty.
Thus my questions:
is my updateArray function a bad idea? From what I read, my prevTestArray input will always reflect the latest state value, and setTestArray is never supposed to change... This is the only way I see to handle the async calls my socket connection generate, without placing "testArray" in my useEffect dependencies (thus avoiding continuously connecting/disconnecting the socket?)
rendering outside the component, in renderArray, doesn't affect my tests (same result if I move the code inside my component), but is there a problem with this?
As a side note, my array's content is actually more complex is the real app (array of objects), I have tried placing this in this test code, it works just fine too...
Thank you!
Edit: Note that moving updateArray inside the useEffect seems to be the recommended pattern. I did that in my full app. The hook linter does not complain about any missing dependency, yet this still doesn't work in my full app.
But the question is still whether what I am doing here is wrong or not: I know it goes against the guidelines as it prevents the linter from doing its job, but it looks to me like this will still work, the previous state being accessible by default in the setter functions.
Edit #2: Shame on me... silly mistake in my real app code, the equivalent of updateArray had a shallow array copy at some place instead of a deep copy.
Why adding the flipping tag made it work is beyond me (knowing the data was then indeed properly displayed and updated), but getting rid of this mistake solved it all.
I will leave this question on, as the question still stand: is placing the state update, and part of the rendering outside the component a functional problem, or just something which might mater on hide dependencies (preventing the react hooks linter from doing its job) and thus simply bad practice?
The fact is that things work just fine now with both functions outside the component function, which makes sense based on what I understand from hooks at this point.

React stackable snackbars/toasts

I'm creating my own simple snackbar/toast stacker. However, I'm having problems with queing them in an orderly manner. Removing a snackbar from the snackbar que causes re-render and odd behavior.
The basic flow:
Click a button which causes the addSnack function to fire which is provided by the withSnackbar HOC.
Take the parameters from the fired function, and create a snack accordingly and add it to the snackbar list.
At the end, we render the snackbar list.
Each snackbar controls it's own appearance and disappearance, and is controlled by a time out. After the timeout is fired, it calls removeSnack function which is suppose to remove the first snack from the list.
codesandbox
If you click the button for example, four times in a short amount of time. They render nicely, but when the first one is to be deleted, they all disappear and reappear abnormally.
I understand that it's partially the state re-renderings fault, however, I'm not sure how to handle it in a way that the removal is handled gracefully without affecting the rendering of other snacks.
So, after many hours of trial and error, I found a solution that works so far. Moving and reading the snacks outside of the state helped with the bizarre rendering problems, and with it, I was able to create a message que which works well.
Working example
Codesandbox
If you look at splice document, you will notice that it's returning an array of deleted elements and not the initial array.
You can correct it by splicing then updating:
snacks.splice(-1, 1);
addSnacks(snacks);
However you are still going to have some weird behavior and you might need to use a keyed list to fix that.
i had the same issue and i saw your solution, but i was really trying to find out why it happens - here is why:
when u call a useState hook from an async function's callback, you should use the callback format of the hook to make sure that you are working with the latest value. example:
const [messages, setMessages] = useState([]);
const addMessage = ( message ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
return [ ...prevMessages, message ];
});
};
const removeMessage = ( index ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
let newMessages = [...prevMessages];
newMessages.splice( index, 1 );
return newMessages;
});
};

Resources