How to use RTK query selector with an argument?
Asked Answered
M

1

1

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

Megillah answered 14/9, 2022 at 23:54 Comment(0)
S
4

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.

Senaidasenalda answered 15/9, 2022 at 6:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.