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

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.