Load suggestions in Autocomplete component only on input change - material-ui

I fetch my autocomplete suggestions from my db, and I do that whenever the input changes, the problem is my function triggers every time my value changes as well (because that triggers an input change I guess), which is really inefficient and causes a lot of network requests on rerenders, how can I prevent that ?
My code looks something like this.
const handleValueChange = (event, newValue) =>{
setValue(newValue);
}
const handleInputChange = (event, newInput) =>{
// Fetching and setting suggestions here
...
setInput(newInput);
}
<Autocomplete
value={value}
onChange={handleValueChange}
inputValue={input}
onInputChange={handleInputChange}
....
/>

I would say look into using the useEffect hook in combination with state. Something along the line of
const [suggestions,setSuggestions] = useState();
useEffect(()=>{ //fetch and set suggestions },[]}
then set the options in the autocomplete to the state you just set. Making sure you conditional render the autocomplete to not display before fetching is done.
{suggestions? (<AutoComplete.../>):("")}
I don't know if you can use this line for line, but at least in the right direction.

Related

When to set the initial value of the form?

I have these codes, if the user opens the form dialog for the first time, it works well.
function PostFormDialog({ id }) {
const queryClient = useQueryClient()
const post = useQuery(['post', id], () => fetchPost(id))
const update = useMutation(() => updatePost(formValue), {
onSuccess: () => {
queryClient.invalidateQueries(['post', id])
},
})
if (post.isLoading) {
return 'loading...'
}
return (
<Dialog {...dialogProps}>
<Form initialValue={post} onSubmit={update.mutate} />
</Dialog>
)
}
But when I submit the form once, I quickly open the dialog box again, and it will display the last data. The data is being retrieved at this time, but isLoading is false.
I want:
After opening the form dialog box, if the data is out of date, wait for the data to be loaded and display loading...
If you are editing the form, switching tabs may cause data to be retrieved, but loading... is not displayed at this time
This is hard for me. I can avoid it by using optimistic updates, but is there a better way?
Try playing around with react query options. For example:
useQuery(
["post", id],
() => fetchPost(id),
{ refetchOnMount: true }
);
After opening the form dialog box, if the data is out of date, wait for the data to be loaded and display loading...
if your PostFormDialog unmounts after it is closed, you can set cacheTime for your query to a "smaller time". It defaults to 5 minutes so that data can be re-used. If you set cacheTime: 0, the cached data will be removed immediately when you unmount the component (= when you close the dialog).
Every new open of the dialog will result in a hard loading state.
If you are editing the form, switching tabs may cause data to be retrieved, but loading... is not displayed at this time
loading... is not displayed because the query is no longer in loading state. The extra isFetching boolean will be true though, which can be used for background updates.
However, when populating forms with "initial data", background updates don't much sense, do they? What if the user has changed data already?
You can:
turn off background refetches with staleTime: Infinity to just load initial data once.
Keep the background refetches, and then maybe display a "data has changed" notification to the user?
Do not initialize the form with initialValue, but keep it undefined and fall back to the server value during rendering. I've written about this approach here

My useQuery hook is refetching everytime its called. I thought it is suppose to hand back the cache?

I'm a little confused here. I thought react-query, when using useQuery will hand back 'cache' n subsequent calls to the same "useQuery". But everytime I call it it, it refetches and makes the network call.
Is this the "proper way" to do this? I figured it would just auto hand me the "cache" versions. I tried extending staleTime and cacheTime, neither worked. Always made a network call. I also tried initialData with the cache there.. didn't work.
SO, I am doing the following, but seems dirty.
Here is the what I have for the hook:
export default function useProducts ({
queryKey="someDefaultKey", id
}){
const queryClient = useQueryClient();
return useQuery(
[queryKey, id],
async () => {
const cachedData = await queryClient.getQueryData([queryKey, id]);
if (cachedData) return cachedData;
return await products.getOne({ id })
}, {
enabled: !!id
}
);
}
This is initiated like so:
const { refetch, data } = useProducts(
{
id
}
}
);
I call "refetch" with an onclick in two diff locations.. I'd assume after I retrieve the data.. then subsequent clicks will hand back cache?
I’m afraid there are multiple misconceptions here:
react query operates on stale-while-revalidate, so it will give you data from the cache and then refetch in the background. You can customize this behavior by setting staleTime, which will tell the library how long the data can be considered fresh. No background updates will happen.
when you call refetch, it will refetch. It’s an imperative action. If you don’t want it, don’t call refetch.
you don’t need to manually read from the cache in the queryFn - the library will do that for you.

Best practice for testing for data-testid in a nested component with React Testing Library?

I'm trying to write a test to check if my app is rendering correctly. On the initial page Ive added a data-testid of "start". So my top level test checks that the initial component has been rendered.
import React from "react";
import { render } from "react-testing-library";
import App from "../App";
test("App - Check the choose form is rendered", () => {
const wrapper = render(<App />);
const start = wrapper.getByTestId("start");
// console.log(start)
// start.debug();
});
If I console.log(start) the I can see all the properties of the node. However if I try and debug() then it errors saying it's not a function.
My test above does seem to work. If I change the getByTestId from start to anything else then it does error. But I'm not using the expect function so am I violating best practices?
There are two parts to this question -
Why console.log(start) works and why not start.debug()?
getByTestId returns an HTMLElement. When you use console.log(start), the HTMLElement details are logged. But an HTMLElement does not have debug function. Instead, react-testing-library provides you with a debug function when you use render to render a component. So instead of using start.debug(), you should use wrapper.debug().
Because you don't have an expect function, is it a good practice to write such tests ?
I am not sure about what could be a great answer to this, but I will tell the way I use it. There are two variants for getting an element using data-testid - getByTestId and queryByTestId. The difference is that getByTestId throws error if an element with the test id is not found whereas queryByTestId returns null in such case. This means that getByTestId in itself is an assertion for presence of element. So having another expect which checks if the element was found or not will be redundant in case you are using getByTestId. I would rather use queryByTestId if I am to assert the presence/absence of an element. Example below -
test("App - Check the "Submit" button is rendered", () => {
const { queryByTestId } = render(<App />)
expect(queryByTestId('submit')).toBeTruthy()
});
I would use getByTestId in such tests where I know that the element is present and we have expects for the element's properties (not on the element's presence/absence). Example below -
test("App - Check the "Submit" button is disabled by default", () => {
const { getByTestId } = render(<App />)
expect(getByTestId('submit')).toHaveClass('disabled')
});
In the above test, if getByTestId is not able to find the submit button, it fails by throwing an error, and does not execute the toHaveClass. Here we don't need to test for presence/absence of the element, as this test is concerned only with the "disabled" state of the button.

Set initial value to select within custom component in Angular 4

As you can see in this plunkr (https://plnkr.co/edit/3EDk5xxSLRolv2t9br84?p=preview) I have two selects: one in the main component behaving as usual, and one in a custom component, inheriting the ngModel settings.
The following code links the innerNgModel to the component ngModel.
ngAfterViewInit() {
//First set the valueAccessor of the outerNgModel
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
//Set the innerNgModel to the outerNgModel
//This will copy all properties like validators, change-events etc.
this.innerNgModel = this.ngModel;
}
It works, since the name property is updated by both selects.
However when it first loads the second select has no selection.
I guess I'm missing something, a way to initialize the innerNgModel with the initial value.
This is a weird situation to do something like this, but I believe to get this working they need to implement another life-cycle hook. AfterModelSet or something like that :)
Anyways, you can solve this with a simple setTimeout and a setValue:
ngAfterViewInit() {
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
this.innerNgModel = this.ngModel;
setTimeout(() => {
this.innerNgModel.control.setValue(this.ngModel.model);
})
}
plunkr

Capturing changes to model using angular-google-places-autocomplete

I'm using angular-google-places-autocomplete (https://github.com/kuhnza/angular-google-places-autocomplete) with Ionic but having problems capturing the selected option when using this directive.
I have the directive set up like this:
<!-- template -->
<input type="text" placeholder="Place search" g-places-autocomplete ng-model="locationSearchResult"/>
<h5>Result</h5>
<pre ng-bind="locationSearchResult | json"></pre>
My controller code is set up to watch for changes to the locationSearchResult model, and if it does change to save the new location to local storage:
// Controller
$scope.locationSearchResult = {};
$scope.$watch('locationSearchResult', function(newVal, oldVal) {
if (angular.equals(newVal, oldVal)) { return; }
$scope.$storage.loc = newVal;
$state.go('new-page');
});
When using the autocomplete it seems to work as expected - I get a list of predictions, and selecting a prediction from the list of predictions updates the text input with the name of the selected place, and the JSON data for the selected place displays under the result heading. But, the change doesn't seem to be picked up by the $scope.$watch in the controller.
As a result, I can't seem to be able to capture the search result data and do anything with it - like add it to the user session.
Maybe I'm just going about it the wrong way (though I used the same approach with ngAutocomplete and it worked ok).
Use the event that gets emitted in your controller.
$scope.$on('g-places-autocomplete:select', function (event, param) {
console.log(event);
console.log(param);
});