react-admin calls getList twice on sorting - reactjs

I have a Datagrid inside a List with custom sorting and filtering. Everything works fine except that when the function that updates the sorting is called, the getList method is called twice (the server is called twice, which is very time consuming).
Here is the fucntion responsible of updating sorting field and order:
const handleSort = (key) => {
console.log("key for sorting :", key);
const {field, order} = sort;
console.log("field and order: ", field +" "+order);
if(field === key) {
sort.order = order === "ASC" ? "DESC" : "ASC";
} else {
sort.field = key;
}
setSort(sort);
refresh();
}
What could probably be the cause of this ?
Edit:
Actually I found that every simple click anywhere on the web page causes react-admin to call getList with the current url (to refresh the page). Is there a way to disable this behaviour or to control it ?

The first request was actually done on windows focus as dictated by React pareameter refetchOnWindowFocus, which has the value true by default.
I changed its value to false in the queryClient prop of the Admin object like the following:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 10000,
retry: false,
refetchOnWindowFocus: false,
},
},
});
return <Admin
queryClient={queryClient} />
...
</Admin >

Related

Redux-form fires an extra CHANGE action when deleting an element in complex fieldArray

I have a form built with reduxForm which has a nested fieldArray component.
In this component, I have two dropdowns. The first determines what will be in the second dropdown.
Here's my solution for that
// Set initial list values when hydrating form
const setInitialQualifiantTypeList = () => {
const list: (string | undefined)[] = []
fields.forEach((qualifiant: any, index: number) => {
const qualifiantType = fields.get(index).qualifiantType
list.push(qualifiantType ? `B0${qualifiantType}` : undefined)
})
return list
}
let qualifiantTypeList = setInitialQualifiantTypeList()
// Change list parameter when changing the type
const onChangeQualifiantType = (
qualifiantType: string,
qualifiant: string,
name: string,
index: number
) => {
let newQualifiantTypeList = [...qualifiantTypeList]
if (qualifiantType === '') {
newQualifiantTypeList[index] = undefined
} else {
newQualifiantTypeList[index] = `B0${qualifiantType}`
// setQualifiantTypeList(newQualifiantTypeList)
qualifiantTypeList = newQualifiantTypeList
}
// If it doesn't match previous type reset the value
if (qualifiantType !== qualifiant.substring(0, 2)) {
resetValue(`${name}[${index}].qualifiant`)
}
}
qualifiantTypeList gets passed as a prop to my dropdown component which gets the appropriate list.
However, when it comes to the fieldArray part, I lose some data if deleting any of my fields except the last. I've tried fields.remove(index) and even change(form, field, newArray) replacing the entire value, but redux-form fires an extra CHANGE no matter what, making my data go from
[
{
"qualifiantType": "94",
"qualifiant": "1KBC-CA-Q"
},
{
"qualifiantType": "95",
"qualifiant": "2A"
},
{
"qualifiantType": "96",
"qualifiant": "3B"
}
]
to
[
{
"qualifiantType": "94",
"qualifiant": "1KBC-CA-Q"
},
{
"qualifiantType": "96"
}
]
The state I'm looking for does appear in the redux devtools, but an extra CHANGE action gets dispatched and clears it.
I've logged my onChangeQualifiantType function and it's not firing during this process, so that's not the cause.
How I can I prevent this behavior so as to simply delete the array element and keep everything else?
After some digging, turns out it's because the sub-list is an async DB fetch, it renders first with the new answer, the component clears it because it's a dropdown and the value is replaced by the time the data is in place.

React state value is not retained when value is set using useState within useEffect with dep []

I am using Fluent UI Details List and trying to make row editable on icon click.
My code is as below. On first run, it shows grid with empty data. Then it goes to useEffect (I tried both useEffect and useLayoutEffect, same behaviour) and data is fetched and stored in state. It also fires the render and the grid shows all the rows per the data. All good so far.
When row > cell 1 is double-clicked on the grid, I turned the row to editable mode. That is also working.
For each editable column, I have a different onChange event attached. So, when any input text box/dropdown cell value changes, it fires the respective onChange event callback function.
Within this cell change callback event, it gets item id and final changed value as input aurguments. And using them, data array will be updated and stored in state. This is my expectation.
But when I read the current data array from state within the callback function, it is empty.
So, basically, problem is that state value stored from useEffect is not retained.
There is no other code line where data array state is updated. So, no chance that the data array is reset by code.
If anyone has any idea or faced, solved such issue, let me know. Thanks in advance.
Adding few things which I tried,
I tried using the class component and it worked. only difference is that instead of useEffect, I used componentDidMount and instead of useState, I used this.setState. Not sure what is other difference within class and function component?
The same code works in class component but not in function component.
I tried using the same function component and instead of async call within useEffect, I made direct sync fetch call before render and loaded data in state as initial value. Then, it worked.
So, it fails only when the data is fetched async mode within useEffect and set to state from there.
My problem is resolved after converting to class component.
but want to understand what is the issue within my function component code.
/** function component */
const [dataItems, setDataItems] = useState<IScriptStep[]>([]);
const [groups, setGroups] = useState<IGroup[]>([]);
/** Component Did Mount */
useLayoutEffect(() => {
props.telemetry.log(`useEffect - []`, LogLevel.Debug);
(async () => {
let scriptSteps = await props.dataHelper.retrieveScriptSteps();
setDataItems(scriptSteps);
let groups = getGroups(scriptSteps);
setGroups(groups);
props.telemetry.log(`Data updated in state`, LogLevel.Debug);
})();
}, []);
/** Render */
return (
<div className="SubgridMain">
{props.telemetry.log(`render`, LogLevel.Debug)}
<div className="List">
<DetailsList
componentRef={_root}
items={dataItems}
groups={groups}
columns={columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
checkButtonAriaLabel="select row"
checkButtonGroupAriaLabel="select section"
onRenderDetailsHeader={_onRenderDetailsHeader}
onRenderRow={_onRenderRow}
groupProps={{ showEmptyGroups: true }}
onRenderItemColumn={_onRenderItemColumn}
onItemInvoked={_onItemInvoked}
compact={false}
/>
</div>
</div>
);
/** on change cell value callback function */
const _onChangeCellName = (entityId : string, fieldName:string, finalValue:string) => {
let currentItems = dataItems;
// create new data array
let toUpdateState: boolean = false;
let newItems = currentItems.map((item) => {
if (item.key === entityId) {
if (item.name !== finalValue) {
toUpdateState = true;
item.name = finalValue ?? null;
}
}
return item;
});
if (toUpdateState) setDataItems(newItems);
};
/** columns configuration is set as below */
let columns : IColumn[] = [
{
key: 'name',
name: 'Name',
fieldName: 'name',
minWidth: 300,
isResizable: true,
onRender: this._onRenderCellName,
},
..
..
]
/** Render Name cell */
private _onRenderCellName(item?: IScriptStep, index?: number, column?: IColumn) {
if (item) {
let stepName = item?.name ?? '';
if (item.isEditable) {
let propsNameTextEditor: ITextEditorWrapperProps = {
entityId: item.key,
fieldName: 'name',
initialText: stepName,
multiline: true,
required: true,
setFinalValue: this._onChangeCellName,
};
return <TextEditorWrapper {...propsNameTextEditor} />;
} else {
return (
<div className="ReadOnly">
<p>{stepName}</p>
</div>
);
}
} else return <></>;
};

How to make an infinite scroll with react-query?

I'm trying to use react-query useInfiniteScroll with a basic API, such as the cocktaildb or pokeapi.
useInfiniteQuery takes two parameters: a unique key for the cache and a function it has to run.
It returns a data object, and also a fetchMore function. If fetchMore is called - through an intersection observer for exemple -, useInfiniteQuery call its parameter function again, but with an updated payload thanks to a native callback getFetchMore().
In the official documentation, getFetchMore automatically takes two argument: the last value returned, and all the values returned.
Based on this, their demo takes the value of the previous page number sent by getFetchMore, and performs a new call with an updated page number.
But how can I perform the same kind of thing with a basic api that only return a json?
Here is the official demo code:
function Projects() {
const fetchProjects = (key, cursor = 0) =>
fetch('/api/projects?cursor=' + cursor)
const {
status,
data,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery('projects', fetchProjects, {
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
})
infinite scrolling relies on pagination, so to utilize this component, you'd need to somehow track what page you are on, and if there are more pages. If you're working with a list of elements, you could check to see if less elements where returned in your last query. For example, if you get 5 new items on each new fetch, and on the last fetch you got only 4, you've probably reached the edge of the list.
so in that case you'd check if lastGroup.length < 5, and if that returns true, return false (stop fetching more pages).
In case there are more pages to fetch, you'd need to return the number of the current page from getFetchMore, so that the query uses it as a parameter. One way of measuring what page you might be on would be to count how many array exist inside the data object, since infiniteQuery places each new page into a separate array inside data. so if the length of data array is 1, it means you have fetched only page 1, in which case you'd want to return the number 2.
final result:
getFetchMore: (lastGroup, allGroups) => {
const morePagesExist = lastGroup?.length === 5
if (!morePagesExist) return false;
return allGroups.length+1
}
now you just need to use getMore to fetch more pages.
The steps are:
Waiting for useInfiniteQuery to request the first group of data by default.
Returning the information for the next query in getNextPageParam.
Calling fetchNextPage function.
Reference https://react-query.tanstack.com/guides/infinite-queries
Example 1 with rest api
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage) => {
// lastPage signature depends on your api respond, below is a pseudocode
if (lastPage.hasNextPage) {
return lastPage.nextCursor;
}
return undefined;
},
})
Example 2 with graphql query (pseudocode)
const {
data,
fetchNextPage,
isLoading,
} = useInfiniteQuery(
['GetProjectsKeyQuery'],
async ({ pageParam }) => {
return graphqlClient.request(GetProjectsQuery, {
isPublic: true, // some condition/variables if you have
first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
cursor: pageParam,
});
},
{
getNextPageParam: (lastPage) => {
// pseudocode, lastPage depends on your api respond
if (lastPage.projects.pageInfo.hasNextPage) {
return lastPage.projects.pageInfo.endCursor;
}
return undefined;
},
},
);
react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:
const projects = data.pages.flatMap((p) => p.projects.nodes)
Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:
const handleEndReached = () => {
fetchNextPage();
};
Graphql example query:
add to your query after: cursor:
query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
projects(
condition: {isPublic: $isPublic}
first: $first
after: $cursor
) ...

Conditional dropdowns with react-select in react-final-form initialized from the state

I'm using react-select and react-final-form for conditional dropdowns, where options for the second select are provided by a <PickOptions/> component based on the value of the first select (thanks to this SO answer).
Here is the component:
/** Changes options and clears field B when field A changes */
const PickOptions = ({ a, b, optionsMap, children }) => {
const aField = useField(a, { subscription: { value: 1 } });
const bField = useField(b, { subscription: {} });
const aValue = aField.input.value.value;
const changeB = bField.input.onChange;
const [options, setOptions] = React.useState(optionsMap[aValue]);
React.useEffect(() => {
changeB(undefined); // clear B
setOptions(optionsMap[aValue]);
}, [aValue, changeB, optionsMap]);
return children(options || []);
};
It clears the second select when the value of the first one changes by changeB(undefined). I've also set the second select to the first option in an array by passing initialValue. As I need to initialize the values from the state, I ended up with the following code:
initialValue={
this.state.data.options[index] &&
this.state.data.options[index].secondOption
? this.state.data.options[index]
.secondOption
: options.filter(
option => option.type === "option"
)[0]
}
But it doesn't work. Initial values from the state are not being passed to the fields rendered by <PickOptions/>. If I delete changeB(undefined) from the component, the values are passed but then the input value of the second select is not updated, when the value of the first select changes (even though the options have been updated). Here is the link to my codesandbox.
How can I fix it?
I was able to get this to work by taking everything that is mapped by the fields.map() section and wrapping it in it's own component to ensure that each of them have separate states. Then I just put the changeB(undefined) function in the return call of the useEffect hook to clear the secondary selects after the user selects a different option for the first select like so:
React.useEffect(() => {
setOptions(optionsMap[aValue]);
return function cleanup() {
changeB(undefined) // clear B
};
}, [aValue, changeB, optionsMap]);
You can see how it works in this sandbox: React Final Form - Clear Secondary Selects.
To change the secondary select fields, you will need to pass an extra prop to PickOptions for the type of option the array corresponds to. I also subscribe and keep track of the previous bValue to check if it exists in the current bValueSet array. If it exists, we leave it alone, otherwise we update it with the first value in its corresponding optionType array.
// subscibe to keep track of previous bValue
const bFieldSubscription = useField(b, { subscription: { value: 1 } })
const bValue = bFieldSubscription.input.value.value
React.useEffect(() => {
setOptions(optionsMap[aValue]);
if (optionsMap[aValue]) {
// set of bValues defined in array
const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);
// if the previous bValue does not exist in the current bValueSet then changeB
if (!bValueSet.some(x => x.value === bValue)) {
changeB(bValueSet[0]); // change B
}
}
}, [aValue, changeB, optionsMap]);
Here is the sandbox for that method: React Final Form - Update Secondary Selects.
I also changed your class component into a functional because it was easier for me to see and test what was going on but it this method should also work with your class component.
Based on the previous answer I ended up with the following code in my component:
// subscibe to keep track of aField has been changed
const aFieldSubscription = useField(a, { subscription: { dirty: 1 } });
React.useEffect(() => {
setOptions(optionsMap[aValue]);
if (optionsMap[aValue]) {
// set of bValues defined in array
const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);
if (aFieldSubscription.meta.dirty) {
changeB(bValueSet[0]); // change B
}
}
}, [aValue, changeB, optionsMap]);
This way it checks whether the aField has been changed by the user, and if it's true it sets the value of the bField to the first option in an array.

React, dynamically add text to ref span

I'm trying to render a message to a span tag specific to an item in a list. I've read a lot about React 'refs', but can't figure out how to populate the span with the message after it's been referenced.
So there's a list of items and each item row has their own button which triggers an API with the id associated with that item. Depending on the API response, i want to update the span tag with the response message, but only for that item
When the list is created the items are looped thru and each item includes this
<span ref={'msg' + data.id}></span><Button onClick={() => this.handleResend(data.id)}>Resend Email</Button>
After the API call, I want to reference the specific span and render the correct message inside of it. But I can't figure out how to render to the span at this point of the code. I know this doesn't work, but it's essentially what I am trying to do. Any ideas?
if (response.status === 200) {
this.refs['msg' + id] = "Email sent";
I recommand using state. because string refs legacy (https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs)
const msgs = [
{ id:1, send:false },
{ id:2, send:false },
{ id:3, send:false },
];
this.state = {
msgs
};
return this.state.msgs.map((msg, index) => {
const status = msg.send ? "Email Sent" : "";
<span>{ status }</span><Button onClick={() => this.handleResend(index)}>Resend Email</Button>
});
async handleResend (index) {
const response = await callAPI(...);
if(reponse.status !== 200) return;
const newMsgs = _.cloneDeep(this.state.msgs);
newMsgs[index].send = true;
this.setState({
msgs: newMsgs
})
}
The workaround is set innerText
this.refs['msg' + id].innerText = "Email sent";
But rather than using ref try to use state to update elements inside render.
i was facing with this issue right now and i figured it out this way:
// currentQuestion is a dynamic Object that comes from somewhere and type is a value
const _target = `${currentQuestion.type}_01`
const _val = this[`${_target}`].current.clientHeight // here is the magic
please note that we don't use . after this to call the ref and not using refs to achieve what we want.
i just guessed that this should be an Object that would hold inner variables of the current object. then since ref is inside of that object then we should be able to call it using dynamic values like above...
i can say that it worked automagically!

Resources