How to use RTK query selector with an argument? - reactjs

I use RTK Query to consume an /items/filter API. In the codes below, the debouncedQuery holds a value of query string parameter to be used to call a filter API endpoint. e.g.: In the /items/filter?item_name=pencil and return the matched results. When it's empty, then /items/filter is called and returns a limited number of results (20 items).
So far, /items/filter returns the results and are displayed as expected while the application is started.
When I passed a filter param /items/filter?item_name={debouncedQuery}, it returned the results. But, it was not shown because, in the Item Detail Component, the selectItemById does not return any result with the provided ids.
Bellow are sample code:
Search Item Component:
export function SearchItem(props: SearchItemProps) {
const {onSelectedItem} = props;
const [itemName, setItemName] = useState<string|undefined>(undefined);
const debouncedQuery = useDebounce(itemName, 500);
const {currentData: items, refetch, isLoading, isFetching, isSuccess} = useFilterItemsQuery(debouncedQuery, {
refetchOnFocus: true,
refetchOnMountOrArgChange: true,
skip: false,
selectFromResult: ({data, error, isLoading, isFetching, isSuccess}) => ({
currentData: data,
error,
isLoading,
isFetching,
isSuccess
}),
});
const ids = items?.ids
useEffect(() => {
refetch();
}, []);
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.toLowerCase();
setItemName(value);
}
let content;
if (isLoading || isFetching) {
content = <div style={{display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: 50}}>
<Spinner animation="grow" variant="dark"/>
</div>;
}
if (!ids?.length) {
content = <Alert variant="dark">
<Alert.Heading>Oh snap! What happened?</Alert.Heading>
<p>The item: {itemName} is not found!</p>
</Alert>;
}
if (isSuccess) {
content = ids?.length ? <ListGroup>
{ids.map((itemId: EntityId, index: number) => {
return <ItemDetail key={index} index={index} id={itemId} onSelectedItem={onSelectedItem}/>
})}
</ListGroup> : null;
}
return (
<>
<Card className="bg-secondary bg-opacity-10 pt-3">
<Card.Header>
<SearchForm name="item_name" placeholder="Search Item" onChange={handleOnChange}/>
</Card.Header>
<Card.Body style={{minHeight: 544, maxHeight: 544, overflowY: "auto"}}>
{content}
</Card.Body>
</Card>
</>
)
}
Item Detail Component
export function ItemDetail(props: ItemProps) {
const {index, id, onSelectedItem} = props;
const item = useAppSelector(state => {
return selectItemById(state, id);
});
console.log("item: ", item);
const handleOnClickedItem = (selectedItem: Item) => {
onSelectedItem(selectedItem);
}
return <ListGroup.Item
action
onClick={() => handleOnClickedItem(item!)}
className={"d-flex justify-content-between align-items-start"}
key={item?.item_uuid}
variant={index % 2 === 0 ? "light" : "dark"}
>
<div className="ms-2 me-auto">
<div>{item?.item_name}</div>
</div>
<Badge bg="dark" className={"bg-opacity-50"} style={{minWidth: 100}}>
<NumberFormat
value={item?.price}
displayType={'text'}
thousandSeparator={true}
prefix={''}
renderText={(formattedValue: string) => <div>{formattedValue}</div>}
/>
</Badge>
</ListGroup.Item>
}
Item ApiSlice
const itemsAdapter = createEntityAdapter<Item>()
const initialState = itemsAdapter.getInitialState();
export const itemApiSlice = catalogApiSlice.injectEndpoints({
endpoints: builder => ({
filterItems: builder.query({
query: (arg) => {
const url = CATALOG_FILTER_ITEMS
if (arg) {
return {
url,
params: {item_name: arg},
};
} else {
return {url};
}
},
transformResponse(response: { data: Item[] }) {
return itemsAdapter.setAll(initialState, response.data)
},
providesTags: (result: Item[] | any) => {
if (result.ids.length) {
// #ts-ignore
return [...result.ids.map(({id}) => ({type: 'Items' as const, id})), {
type: 'Items',
id: 'FILTER_LIST'
}];
} else return [{type: 'Items', id: 'FILTER_LIST'}];
},
}),
getItems: builder.query({
query: () => CATALOG_ITEMS,
transformResponse(response: { data: Item[] }) {
return response.data;
},
providesTags: (result, error, arg) => {
// #ts-ignore
return result
? [
...result.map(({id}) => ({type: 'Items' as const, id})),
{type: 'Items', id: 'LIST'},
]
: [{type: 'Items', id: 'LIST'}]
},
}),
getItem: builder.query({
query: id => {
return {
url: `${CATALOG_ITEMS}/${id}`,
};
},
transformResponse(response: { data: Item }) {
return response.data;
},
providesTags: (result, error, arg) => {
// #ts-ignore
return result
? [
{type: 'Items' as const, id: result.id},
{type: 'Items', id: 'DETAIL'},
]
: [{type: 'Items', id: 'DETAIL'}]
},
}),
})
})
export const {
useGetItemsQuery,
useFilterItemsQuery,
useGetItemQuery
} = itemApiSlice
export const selectItemsResult = itemApiSlice.endpoints.filterItems.select();
const selectItemsData = createDraftSafeSelector(
selectItemsResult,
itemsResult => {
return itemsResult.data
}
)
export const {
selectAll: selectAllItems,
selectById: selectItemById,
selectIds: selectItemIds
} = itemsAdapter.getSelectors((state: any) => selectItemsData(state) ?? initialState);
I am wondering how I can get that debouncedQuery in select() or how to update the memoized select in each /items/filter?item_name={debouncedQuery}.
Thank you

This is a pattern you should not use - for the reason you found here.
export const selectItemsResult = itemApiSlice.endpoints.filterItems.select();
is the same as
export const selectItemsResult = itemApiSlice.endpoints.filterItems.select(undefined);
and will always give you the result of useFilterItemsQuery()/useFilterItemsQuery(undefined).
If you call useFilterItemsQuery(5), you also have to create a selector using
export const selectItemsResult = itemApiSlice.endpoints.filterItems.select(5);
.
and all other selectors would have to depend on that.
Of course, that doesn't scale.
Good thing: it's also absolutely unneccessary.
Instead of calling
const item = useAppSelector(state => {
return selectItemById(state, id);
});
in your component, call useFilterItemsQuery with a selectFromResult method and directly use the selectById selector within that selectFromResults function - assuming you did get it by just calling itemsAdapter.getSelectors() and are passing result.data into the selectById selector as state argument.

Related

Cannot pass selected date from MUI DateTimePicker into SWR hook

I have a component that holds a MUI DataGrid.
Now, for any row of the DataGrid I need to render a DateTimePicker column. There is data coming
in with a SWR call which data could contain a datetime for the column or not.
mainPage/sdk.js
export const useSomeData = (
params: TUseFetchOptions['params']
) => {
const { data, isLoading, mutate } = useFetch<TPagination<TSomeData>>('/some-data/');
const approve = useCallback(
(someData: TSomeData) => {
const data = { published_at: someData.published_at }
return requestHandler(
post(`/some-data/update/`, data)
)
.then((someData) => {
mutate((prev) => {
if (!prev) {
return prev;
}
// some irrelevant mutation for swr caching happens here
return prev;
}
return prev;
}, false);
})
.catch((error) =>
// some irrelevant alerting happens here
);
},
[mutate]
);
return useMemo(
() => ({ someData: data, isLoading, approve,mutate }),
[data, isLoading, mutate, approve]
);
};
mainPage/index.tsx
import {useSomeData} from './sdk'
const SomeDataPublish = () => {
// const {params} = ....
// const dataGridProps = ...
const { someData, isLoading, approve } = useSomeData(params);
return (
<Stack>
{someData && (
<SomeDataDataGrid
someData={someData}
params={params}
DataGridProps={dataGridProps}
handleApprove={approve}
/>
)}
</Stack>
);
};
export default SomeDataPublish;
mainPage/componenets/someDataDataGrid.tsx
export const columns: GridColumns = [
{
// some field
},
{
// some field
},
{
// some field
},
// ...
];
const buildColumnsData = (
handleApprove: ReturnType<typeof useSomeData>['approve'],
): GridColumns => {
return [
...columns,
{
field: 'published_at',
headerName: 'Publish at',
flex: 0.75,
renderCell: (params: any) => <RowDatePicker params={params} />
},
{
field: '',
type: 'actions',
flex: 0.4,
getActions: (params: any) => [
<RowActions
params={params}
handleApprove={handleApprove}
/>
]
}
];
};
const buildRows = (someData: TSomeData[]): GridRowsProp => {
return someData.map((row) => ({
id: row.id,
// ...
published_at: _.get(row, 'published_at'),
}));
};
const SomeDataDataGrid: FC<{
someData: TPagination<TSomeData>;
params: TUseFetchOptions['params'];
DataGridProps: Partial<MuiDataGridProps>;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ someData, params, DataGridProps, handleApprove }) => {
return (
<Paper>
<DataGrid
// ...
columns={buildColumnsData(handleApprove)}
rows={someData ? buildRows(someData.results) : []}
// ...
{...DataGridProps}
/>
</Paper>
);
};
export default SomeDataDataGrid;
mainPage/componenets/rowDatePicker.tsx
const RowDatePicker: React.FC<{
params: GridRowParams;
}> = ({ params }) => {
const [publishedAt, setPublishedAt] = React.useState(params.row.published_at);
return (
<>
<DateTimeField
label={'Pick Date'}
value={publishedAt}
onChange={setPublishedAt}
/>
</>
);
};
export default RowDatePicker;
mainPage/componenets/rowAction.tsx
const RowActions: React.FC<{
params: GridRowParams;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ params, handleApprove }) => {
return (
<>
<Tooltip title="Approve">
<IconButton
color="success"
disabled={false}
onClick={(e) => {
console.log(params.row)}
handleApprove(params.row)
>
<AppIcons.CheckCircle />
</IconButton>
</Tooltip>
</>
);
};
export default RowActions;
The problem that I have - if I change the date from the date picker, on clicking the <AppIcons.CheckCircle /> in the RowActions component I expect the row.published_at to be updated with the new value. Then I pass the new updated object (with the updated published_at attribute) to the handleApprove hook so I can make some mutations and pass the updated object (with new published_at value) to the back end.
However, on examining the someData object that is passed to the approve hook the published_at field has its old value ( the one that came from the SWR fetcher).
I know that I need to mutate somehow params.row.published_at = publishedAt in the onChange callback of the RowDatePicker.DateTimePicker, but I am not sure how to do it. Any help would be appreciated.

Why element is not getting rendered and how can I fix it?

I am trying to render some dynamic data in an element using useEffect Hook, which is not working.
Below is abc.tsx which has byTestOne(globalThis.test) which is responsible to send data back to us using globalThis.test value
const abc = () => {
const [data, setData] = useState<any>([]);
// globalThis.test is something that changes and when it changes, byTestOne triggers and get the data based on globalThis.test
useEffect(() => {
async function fetchData() {
const data = byTestOne(globalThis.test).then((data: any) => {
return data
})
setData(await data)
}
fetchData();
}, [globalThis.test]);
return (
<ImageBackground
source={require("../assets/images/abc.png")}
style={styles.bg}>
<View style={styles.containerHome}>
<CardStack>
{
// Below Element is not getting rendered
data.map((item: any) => (
<Card key={item.id}>
<CardItemForSwiper
name={item.name}
description={item.description}
/>
</Card>
)
)
}
</CardStack>
</View>
</ImageBackground>
);
};
export default ABC;
byTestOne() looks like this:
let data = [{
name: "test1",
description: "desc",
test: "one"
},
{
name: "test2",
description: "desc",
test: "two"
}]
export const byTestOne = (test: string) => {
const dataA = new Promise((resolve) => {
const filData = data.filter((rawData) => {
return rawData.test == test
})
if (filData.length > 0) {
globalThis.test = test
}
return resolve(filData)
})
return dataA
}

Unable to update nested state react

I have a reusable drop down menu component and i render it twice with two different lists and it should update the state with the id of the first element.
the first drop down of the layout update the state without any issue but the second one does not(i switched the order and it always seems the first one updates the state the second doesn't).
please see code
dashbord
const initializeData = {
actionStatuses: [],
actionCategories: [],
actionGroups: [],
actionEvents: [],
actionEventsWithFilter: [],
selectedFilters: {actionStatusId: "", actionCategoryId:""},
};
const Dashboard = ({ selectedPracticeAndFy }) => {
const [data, setData] = useState(initializeData);
const getSelectedStatus = ({ key }) => {
const actionStatusId = key;
const selectedFilters = { ...data.selectedFilters, actionStatusId };
setData((prevState) => {
return { ...prevState, selectedFilters }
});
};
const getSelectedCategory = ({ key }) => {
const actionCategoryId = key;
const selectedFilters = { ...data.selectedFilters, actionCategoryId };
setData((prevState) => {
return { ...prevState, selectedFilters }
});
};
}
result filter:
const ResultFilter = ({actionStatuses, actionCategories, getSelectedStatus, getSelectedCategory}) => {
return (
<Grid
justify="flex-start"
container
>
<Grid item >
<Typography component="div" style={{padding:"3px 9px 0px 0px"}}>
<Box fontWeight="fontWeightBold" m={1}>
Result Filter:
</Box>
</Typography>
</Grid>
<Grid >
<DropdownList payload={actionCategories} onChange={getSelectedCategory} widthSize= {dropdownStyle.medium}/>
<DropdownList payload={actionStatuses} onChange={getSelectedStatus} widthSize= {dropdownStyle.medium}/>
</Grid>
</Grid>
);
}
DropdownList:
const DropdownList = ({ label, payload, onChange, widthSize, heightSize, withBorders, initialData }) => {
const { selectedData, setSelectedData, handelInputChange } = useForm(
payload
);
useEffect(() => {
if (Object.entries(selectedData).length === 0 && payload.length !== 0) {
setSelectedData(payload[0]);
}
}, [payload]);
useEffect(() => {
if (Object.entries(selectedData).length !== 0) {
onChange(selectedData);
}
}, [selectedData]);
return (
<div style={widthSize}>
<div className="ct-select-group ct-js-select-group" style={heightSize}>
<select
className="ct-select ct-js-select"
id={label}
value={JSON.stringify(selectedData)}
onChange={handelInputChange}
style={withBorders}
>
{label && <option value={JSON.stringify({key: "", value: ""})}>{label}</option>}
{payload.map((item, i) => (
<option key={i} value={JSON.stringify(item)} title={item.value}>
{item.value}
</option>
))}
</select>
</div>
</div>
);
};
It may be a stale closure issue, could you try the following:
const getSelectedStatus = ({ key }) => {
setData((data) => {
const actionStatusId = key;
const selectedFilters = {
...data.selectedFilters,
actionStatusId,
};
return { ...data, selectedFilters };
});
};
const getSelectedCategory = ({ key }) => {
setData((data) => {
const actionCategoryId = key;
const selectedFilters = {
...data.selectedFilters,
actionCategoryId,
};
return { ...data, selectedFilters };
});
};

Interaction with Apollo GraphQL Store not Working

I'm Trying to Learn GraphQL by Developing a Simple To-do List App Using React for the FrontEnd with Material-UI. I Need to Now Update the Information on the Web App in Real-time After the Query Gets Executed. I've Written the Code to Update the Store, But for Some Reason it Doesn't Work. This is the Code for App.js.
const TodosQuery = gql`{
todos {
id
text
complete
}
}`;
const UpdateMutation = gql`mutation($id: ID!, $complete: Boolean!) {
updateTodo(id: $id, complete: $complete)
}`;
const RemoveMutation = gql`mutation($id: ID!) {
removeTodo(id: $id)
}`;
const CreateMutation = gql`mutation($text: String!) {
createTodo(text: $text) {
id
text
complete
}
}`;
class App extends Component {
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.map(existingTodo => existingTodo.id === todo.id ? {
...todo,
complete: !todo.complete,
} : existingTodo);
store.writeQuery({ query: TodosQuery, data })
}
});
};
removeTodo = async todo => {
await this.props.removeTodo({
variables: {
id: todo.id,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.filter(existingTodo => existingTodo.id !== todo.id);
store.writeQuery({ query: TodosQuery, data })
}
});
};
createTodo = async (text) => {
await this.props.createTodo({
variables: {
text,
},
update: (store, { data: { createTodo } }) => {
const data = store.readQuery({ query: TodosQuery });
data.todos.unshift(createTodo);
store.writeQuery({ query: TodosQuery, data })
},
});
}
render() {
const { data: { loading, error, todos } } = this.props;
if(loading) return <p>Loading...</p>;
if(error) return <p>Error...</p>;
return(
<div style={{ display: 'flex' }}>
<div style={{ margin: 'auto', width: 400 }}>
<Paper elevation={3}>
<Form submit={this.createTodo} />
<List>
{todos.map(todo =>
<ListItem key={todo.id} role={undefined} dense button onClick={() => this.updateTodo(todo)}>
<ListItemIcon>
<Checkbox checked={todo.complete} tabIndex={-1} disableRipple />
</ListItemIcon>
<ListItemText primary={todo.text} />
<ListItemSecondaryAction>
<IconButton onClick={() => this.removeTodo(todo)}>
<CloseIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)}
</List>
</Paper>
</div>
</div>
);
}
}
export default compose(
graphql(CreateMutation, { name: 'createTodo' }),
graphql(UpdateMutation, { name: 'updateTodo' }),
graphql(RemoveMutation, { name: 'removeTodo' }),
graphql(TodosQuery)
)(App);
Also, i Want to Create Some List Items but that Doesn't Work Either. I'm Trying to get the Text Entered in the Input Field in Real-time Using a Handler Function handleOnKeyDown() in onKeyDown of the Input Field. I Pass in a event e as a Parameter to handleOnKeyDown(e) and when i console.log(e) it, instead of logging the Text Entered, it Returns a Weird Object that i Do Not Need. This is the Code that Handles Form Actions:
export default class Form extends React.Component{
state = {
text: '',
}
handleChange = (e) => {
const newText = e.target.value;
this.setState({
text: newText,
});
};
handleKeyDown = (e) => {
console.log(e);
if(e.key === 'enter') {
this.props.submit(this.state.text);
this.setState({ text: '' });
}
};
render() {
const { text } = this.state;
return (<TextField onChange={this.handleChange} onKeyDown={this.handleKeyDown} label="To-Do" margin='normal' value={text} fullWidth />);
}
}
This above Code File Gets Included in my App.js.
I Cannot Figure out the Issues. Please Help.
I was stuck with a similar problem. What resolved it for me was replacing the update with refetchQueries as:
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete
},
refetchQueries: [{
query: TodosQuery,
variables: {
id: todo.id,
complete: !todo.complete
}
}]
});
};
For your second problem, try capitalizing the 'e' in 'enter' as 'Enter'.
Hope this helps!

How data.refetch() function from react-apollo works

On the frontend, I am using ReactJS and trying to build-in a filtering option to a list view. The list view correctly getting data from graphql endpoint by issuing this graphql query:
query getVideos($filterByBook: ID, $limit: Int!, $after: ID) {
videosQuery(filterByBook: $filterByBook, limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
id
title
ytDefaultThumbnail
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
On the initial load $filterByBook variable is set to null, so the query correctly returns all pages for all nodes (query returns a paginated result). Then, by clicking on the filter (filter by book) another graphql query is issuing, but it always returns the same data. Here is a code snippet for filtering component
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
And, here is complete code without imports for the list view component
class VideoList extends React.Component {
constructor(props) {
super(props);
this.subscription = null;
}
componentWillUnmount() {
if (this.subscription) {
// unsubscribe
this.subscription();
}
}
renderVideos() {
const { videosQuery } = this.props;
return videosQuery.edges.map(({ node: { id, title, ytDefaultThumbnail } }) => {
return (
<Col sm="4" key={id}>
<Card>
<CardImg top width="100%" src={ytDefaultThumbnail} alt="video image" />
<CardBlock>
<CardTitle>
<Link
className="post-link"
to={`/video/${id}`}>
{title}
</Link>
</CardTitle>
</CardBlock>
</Card>
</Col>
);
});
}
renderLoadMore() {
const { videosQuery, loadMoreRows } = this.props;
if (videosQuery.pageInfo.hasNextPage) {
return (
<Button id="load-more" color="primary" onClick={loadMoreRows}>
Load more ...
</Button>
);
}
}
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
render() {
const { loading, videosQuery } = this.props;
if (loading && !videosQuery) {
return (
<div>{ /* loading... */}</div>
);
} else {
return (
<div>
<Helmet
title="Videos list"
meta={[{
name: 'description',
content: 'List of all videos'
}]} />
<h2>Videos</h2>
{this.renderFilters()}
<Row>
{this.renderVideos()}
</Row>
<div>
<small>({videosQuery.edges.length} / {videosQuery.totalCount})</small>
</div>
{this.renderLoadMore()}
</div>
);
}
}
}
export default compose(
graphql(VIDEOS_QUERY, {
options: () => {
return {
variables: {
limit: 3,
after: 0,
filterByBook: null
},
};
},
props: ({ data }) => {
const { loading, videosQuery, fetchMore, subscribeToMore, refetch } = data;
const loadMoreRows = () => {
return fetchMore({
variables: {
after: videosQuery.pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const totalCount = fetchMoreResult.videosQuery.totalCount;
const newEdges = fetchMoreResult.videosQuery.edges;
const pageInfo = fetchMoreResult.videosQuery.pageInfo;
return {
videosQuery: {
totalCount,
edges: [...previousResult.videosQuery.edges, ...newEdges],
pageInfo,
__typename: "VideosQuery"
}
};
}
});
};
return { loading, videosQuery, subscribeToMore, loadMoreRows, refetch };
}
}),
graphql(LIST_BOOKS_QUERY, {
props: ({ data }) => {
const { listOfBooksWithChapters } = data;
return { listOfBooksWithChapters };
}
}),
)(VideoList);
Question:
Why refetch function returns data without taking into account new variable filterByBook? How to check which variables object I supplied to the refetch function? Do I need to remap data that I receive from refetch function back to the component props?
EDIT:
I found the way to find what variable object I supplied to the query and found that variable object on filtering event returns this data
variables:Object
after:0
limit:3
variables:Object
after:0
filterByBook:"2"
limit:3
you can do it by adding refetch in useQuery
const {loading, data, error,refetch} = useQuery(GET_ALL_PROJECTS,
{
variables: {id: JSON.parse(localStorage.getItem("user")).id}
});
then call function refetch()
const saveChange = input => {
refetch();
};
OR
const saveChange = input => {
setIsOpen(false);
addProject({
variables: {
createBacklogInput: {
backlogTitle: backlogInput,
project:id
}
}
}).then(refetch);
It seem that refetch function is not meant to refetch data with different variables set (see this discussion).
I finally and successfully solved my issue with the help from this article

Resources