React query refetch even if the key already exist in cache - reactjs

I built a form using react,react-query, material ui, formik and yup (for schema validation).
link to the code
I built custom fields:
SelectField
AutocompleteField
CacheAutocompleteField - cache field using react-query
queryAsyncFunc props - get async function and cache the data using react-query
I have 3 fields:
Type - Select field
Country - CacheAutocompleteField
City - CacheAutocompleteField
My scenario:
I select any type from my hardcoded list (Type A, Type B , Type C),
I search any country, then I search any city
What I'm trying to do?
every time I select a new type (from type options) - I want the country and city fields to be reset.
every time I already search the same key (the queryKey is combined of cacheKey+inputValue) , it will not call to api, it will get the results from cache (that's what I chose to use from react-query to do).
What I'm getting when I run my code?
When I select a type A, enter a country “Island” it will fetch data from api and get the data.
Then when I select a type B, enter a country “Island” - It will fetch data from api and get the data.
But when I select a type A and and same country “Island” again - I don’t want it to fetch data from api - I want it to get data from cache (that’s the reason I chose to work with react-query too) because it already search for this data with the same type. The queryKey is depended of other type field.
when I search anything from autocomplete and it not find it, then I try to reset it by select any other type, it will kind of reset the value of the input but it still exist in inputValue of the country.
for example I select type C, then enter "lakd" in country, then select any other type, it not reset it. reset works for me only when anything find in autocomplete and I select it. I guess it's because the autocomplete component not have inputValue props, but when I use it it make me other issues.

For your first issue, I think it would be better to use enable property of react-query(to handle it with debounceInputValue).
so you can use boolean state for enable and disable it and in useEffect for debounceInputValue make it true but when data arrived make it false again
sth like this :
const [queryStatus, setQueryStatus] = useState(false);
const { data, isFetching, refetch } = useQueryData(
cacheKey,
inputValue,
queryAsyncFunc,
queryStatus
);
useEffect(() => {
if (debounceInputValue.length > 1) {
setQueryStatus(true);
}
}, [debounceInputValue]);
useEffect(() => {
setDataOptions(data ? [...data] : []);
setQueryStatus(false);
}, [inputValue, data]);
and in useQuery:
enabled: enabled,
staleTime: Infinity,
And for your second issue, I fixed it by setting state for inputValue and onChange for it
like this :
const onInputChange = (_,newInputValue)=>{
setInputValue(newInputValue);
}
and in autoComplete :
inputValue={inputValue}
onInputChange={onInputChange}
Codesandbox

You needn't call refetch. It call the API regardless of the cache.
Comment/Remove this code
// useEffect(() => {
// if (debounceInputValue.length > 1) {
// refetch();
// }
// }, [debounceInputValue, refetch]);
And you should enable the useQuery
enabled: true,
And use debounceInputValue instead of inputValue for useQueryData
https://codesandbox.io/s/react-query-autocomplete-forked-d84rf4?file=/src/components/FormFields/CacheAutocompleteField.tsx:1255-1263

Related

Use useEffect or not

Is it bad practice to use useEffect?
And should useEffect be avoided if possible due to re-renders?
This question arised yesterday when a colleague asked for a code review and we had different opinions on how to solve this.
We are creating an app that shows some kind of documentation which could be sorted in chronological or reversed chronological order. This is decided by a button in the apps top bar with a default value of chronological order, this value is stored in a global redux state and will be used in every call to fetch documentation.
In this example we update sortOrder on button click and as an effect of that we fetch data.
If I understand this correctly, we render once when sortOrder state change, and once after data is fetched.
Pseudo code ish
interface AppState = {
sortOrder: SortOrder:
documentation: Documentation[];
}
reducer(){
case toggleSortOrder:
const order = state.sortOrder === 'asc' ? 'desc' : 'asc';
return {
....state,
sortOrder: order;
}
}
const AppBar = () => {
const dispatch = useDispatch();
return <div><button onClick={dispatch(toggleSortOrder)}>Change sort order</button>
</div>;
}
const DocumentationList = (type: DocumentationType) => {
const dispatch = useDispatch();
const sortOrder = useSelector((state) => state.appState.sortOrder);
const documentation = useSelector((state) => state.appState.documentation);
useEffect(() => {
// action is caught by redux-saga and a call to docApi is made through axios
dispatch(getDocumentation.request(type, sortOrder)
},[sortOrder]);
return documentation.map((doc) => <Documentation data={doc} />);
}
Is this bad practice?
Should we avoid useEffect and fetch data on click and update sortOrder in saga instead?
Reading docs and blogs I mostly see examples of how en when to use them.
In my opinion, I would go with solution more-less like yours, with splitting responsibilities between element which externally changes query params, and element which is displaying data based on current query params. If you decide to put all logic in button click handler then you are kinda coupling too much list and button, because in order to delegate all work to button click you must dig into DocumentationList fetch-data implementation and to copy it to another place(button related saga or in button click handler) in order to fetch data from another place in the app, and not just from the DocumentationList itslef.
From my perspective, only DocumentationList should be responsible to fetch data, and noone else. But you should provide way to subscribe, from documentation list, to some external query params(sort, filters etc) and when they change(if they exist) data should be loaded.
Right now you only have sort, but in case when you can potentially have more params that can be externally modified, then I would dedicate more complex redux part to query params, something like documentationQueryParams: { sort: "asc", filters: { name: "doc a", type: "type b" } }, and then inside DocumentationList I would use custom hook, for example const queryParms = useDocumentationQueryParams(); which will return standardized query params, and in useEffect I would subscribe to those queryParms change - whenever they change I will easily fetch new data since you know what is the structure of the queryParms(they must be standardized is some way). Like this you coupled them but in very flexible way, whenever you need new param you will update only filter/query-related component, because in DocumentationList you relay on standardized hook output and you can easily create generic mechanism to output query string, or body data, in order to make new request and to fetch new data.
In terms of performance, there is really no difference between hooks-based approach and moving all to click handlers, because in DocumentationList your render part should rerender only when list change, no matter how list is being changed.

Apollo useLazyQuery hook uses old variables when refetching (instead of new vars)

My database stores a list of items. I've written a query to randomly return one item from the list each time a button is clicked. I use useLazyQuery and call the returned function to execute the query and it works fine. I can call the refetch function on subsequent button presses and it returns another random item correctly.
The problem comes when i try to pass variables to the query. I want to provide different criteria to the query to tailor how the random choice is made. Whatever variables I pass to the first call are repeated on each refetch, even though they have changed. I can trace that they are different on the client but the resolver traces the previous variables.
// My query
const PICK = gql`
query Pick($options: PickOptionsInput) {
pick(options: $options) {
title
}
}
`;
// My lazy hook
const [pick, { data, refetch }] = useLazyQuery(PICK, {
fetchPolicy: "no-cache",
});
// My button
<MyButton
onPick={(options) =>
(refetch || pick)({ // Refetch unless this is the first click, then pick
variables: {
options // Options is a custom object of data used to control the pick
},
})
}
/>
Some things I've tried:
Various cache policies
Not using an object for the options and defining each possible option as a different variable
I'm really stumped. It seems like the docs say new variables are supposed to be used if they are provided on refetch. I don't think the cache policy is relevant...I'm getting fresh results on each call, it's just the input variables that are stale.
I guess only pick is called ...
... because for <MyBytton/> there is no props change, no onPick redefined - always the first handler definition used (from first render) and options state from the same time ...
Try to pass all (options, pick and refetch) as direct props to force rerendering and handler redefinition:
<MyButton
options={options}
pick={pick}
refetch={refetch}
onPick={(options) =>
(refetch || pick)({ // Refetch unless this is the first click, then pick
variables: {
options // Options is a custom object of data used to control the pick
},
})
}
/>
... or [better] define handler before passing it into <MyButton/>:
const pickHandler = useCallback(
(options) => {
if(refetch) {
console.log('refetch', options);
refetch( { variables: { options }});
} else {
console.log('pick', options);
pick( { variables: { options }});
}
},
[options, pick, refetch]
);
<MyButton onPick={pickHandler} />

GraphQL Automatic refetch on empty responses

I want to randomize movies from theMovieDB API. First I send a request to access the ID of the latest entry:
const { loading: loadingLatest, error: errorLatest, data: latestData, refetch: refetchLatest } = useQuery(
LATEST_MOVIE_QUERY
);
Then I want to fetch data from a randomly selected ID between 1 and the number of the latest id. Using a variable dependant on the first query seems to break the app, so for now I'm just using the same movie every time upon mounting the component:
const [
movieState,
setMovieState
] = useState(120);
const { loading, error, data, refetch } = useQuery(ONE_MOVIE_BY_ID_QUERY, {
variables : { movieId: movieState },
skip : !latestData
});
I want to press a button to fetch a new random movie, but the problem is that many of the IDs in the API lead to deleted entries and then I get an error back. I want to keep refetching until I get a good response back but I have no idea to implement it. Right now my randomize function just looks like this:
const randomizeClick = () => {
let mostRecentID = latestData.latestMovie.id;
setMovieState(Math.floor(Math.random() * mostRecentID));
};
I'd be grateful if someone can help me how to implement this.
I think what you needs is the "useLazyQuery" functionality of Apollo. You can find more information about it here: https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually
With useLazyQuery you can change your variables easily and this is meant to be fired after a certain event (click or something similar). The useQuery functionality will be loaded when the component is mounted.

Getting current table data from Ant Design Table

I'm using Ant design table for displaying some data. New data arrives every one second. When user clicks on button, I need to export current table data to XSL.
I'm getting current data in this way:
onChange={(pagination, filter, sorter, currentTable) => this.onTableChange(filter, sorter, currentTable)}.
This thing works and gets me good and filtered data, but when I get new data, I can't get those new data because this function is not triggered while none of the filters or sort settings didn't change. Is there any way to get current table data without dummy changing filter to trigger onChange function?
Title and footer return only current page data.
Code show here
{ <Table
dataSource={this.props.data}
rowKey={(record) => { return record.id}}
columns={this.state.Columns}
pagination={this.state.pageSizeOptions}
rowClassName={(record) => { return
this.getRowClassNames(record) }}
expandedRowKeys={ this.state.expandedRowKeys }
expandedRowRender={record => { return
this.getEventsRows(record) }}
onExpand={(expanded, record) => {
this.onExpandRow(expanded, record.id) }}
expandRowByClick={false}
onChange={(pagination, filter, sorter, currentTable)
=> this.onTableChange(filter, sorter, currentTable)}
/>
}
You can store pagination, sorter and filter data in your component state. Also you can pass these params to your redux state and store there. If you can share your code, i can provide more specific answers.
Below you can find my solution for permanent filter. I was sending filter params to API and get filtered data. If you want to filter it in the component, you can use component lifecycle or render method.
onTableChange(pagination, filter, sorter){
const { params, columns } = this.state;
/** You can do any custom thing you want here. Below i update sort type in my state and pass it to my redux function */
if(sorter.order != null)
params.ordering = (sorter.order === 'descend' ? "-" : "")+ sorter.columnKey.toString();
this.setState({params});
this.props.listUsers(this.state.params);
}
This is because you have taken either columns, datasource varibale as object instead of array.
write
this.state.someVariable=[]
instead of
this.state.someVariable = {}

Don't loadOptions on initial render

I have a ton of selects on my page and I don't want to fire off API calls for all of them every time the page loads. How can I skip loadOptions when async react-select components initialize?
According to the docs, it looks like you can specify autoload={false}. From the docs:
Unless you specify the property autoload={false} the control will automatically load the default set of options (i.e. for input: '') when it is mounted.
Updated:
To get around the issue where you have to type something, you could add an onFocus callback. This will fire when the react-select component is clicked on or tabbed to, instead of waiting for the user to type something. It might look something like this:
<ReactSelect
...otherProps
onFocus = { this.fetchData }
/>
To prevent constantly re-fetching the data, you might want to keep track of whether or not the data has been fetched in state somewhere. You could declare the initial state as fetched: false, then in the function that loads the options, set this.setState({ fetched: true }). Finally, your fetchData function can check this.state.fetched before requesting new data.
state= {option1:'', option2:'', option3:''}
If option 1 is filled out render select #2's values
this.state.option1 && apicall for option 2().
If option 2 is filled out render select #3's values
this.state.option2 && apicall for option 3().
All of this can be in componentDidMount()
This is assuming when the user selects a value you are storing that value in the state.

Resources