Tam Coton
Tam Coton

Reputation: 864

Component not re-rendering when useState hook updates

export default function SearchPage() {
    const [searchString, setSearchString] = React.useState("");
    const [apiCall, setApiCall] = React.useState<() => Promise<Collection>>();
    const {isIdle, isLoading, isError, error, data} = useApi(apiCall);
    const api = useContext(ApiContext);

    useEffect(()=>console.log("APICall changed to", apiCall), [apiCall]);

    const doSearch = (event: React.FormEvent) => {
        event.preventDefault();
        setApiCall(() => () => api.search(searchString));
    };

    const doNext = () => {
        var next = api.next;
        if (next) {
            setApiCall(()=>(() => next)());
        }
        window.scrollTo(0, 0);
    }

    const doPrev = () => {
        if (api.prev) {
            setApiCall(() => api.prev);
        }
        window.scrollTo(0, 0);
    }

    return (
        <>
            <form className={"searchBoxContainer"} onSubmit={doSearch}>
                <TextField
                    label={"Search"}
                    variant={"filled"}
                    value={searchString}
                    onChange={handleChange}
                    className={"searchBox"}
                    InputProps={{
                        endAdornment: (
                            <IconButton onClick={() => setSearchString("")}>
                                <ClearIcon/>
                            </IconButton>
                        )
                    }}
                />
                <Button type={"submit"} variant={"contained"} className={"searchButton"}>Go</Button>
            </form>

            {
                (isIdle) ? (
                    <span/>
                ) : isLoading ? (
                    <span>Loading...</span>
                ) : isError ? (
                    <span>Error: {error}</span>
                ) : (
                    <Paper className={"searchResultsContainer"}>
                        <Box className={"navButtonContainer"}>
                            <Button variant={"contained"}
                                    disabled={!api.prev}
                                    onClick={doPrev}
                                    className={"navButton"}>
                                {"< Prev"}
                            </Button>
                            <Button variant={"contained"}
                                    disabled={!api.next}
                                    onClick={doNext}
                                    className={"navButton"}>
                                {"Next >"}
                            </Button>
                        </Box>
                        <Box className={"searchResults"}>
                            {
                                data && data.items().all().map(item => (
                                    <span className={"thumbnailWrapper"}>
                                    <img className={"thumbnail"}
                                         src={item.link("preview")?.href}
                                         alt={(Array.from(item.allData())[0].object as SearchResponseDataModel).title}/>
                                </span>
                                ))
                            }
                        </Box>
                        <Box className={"navButtonContainer"}>
                            <Button variant={"contained"}
                                    disabled={!api.prev}
                                    onClick={doPrev}
                                    className={"navButton"}>
                                {"< Prev"}
                            </Button>
                            <Button variant={"contained"}
                                    disabled={!api.next}
                                    onClick={doNext}
                                    className={"navButton"}>
                                {"Next >"}
                            </Button>
                        </Box>
                    </Paper>
                )
            }
        </>
    )
}

For various reasons, I've got a function stored in my state (it's for use with the react-query library). I'm seeing very odd behaviour when I try and update it, though. When any of doSearch, doNext, or doPrev are called, it successfully updates the state - the useEffect hook is firing properly and I can see the message in console - but it's not triggering a re-render until the window loses and regains focus.

Most of the other people I've seen with this problem have been storing an array in their state, and updating the array rather than creating a new one - so the hooks don't treat it as a new object, and the re-render doesn't happen. I'm not using an array, though, I'm using a function, and passing it different function objects. I'm absolutely stumped and have no idea what's going on.

EDIT: It seems it might not be the rendering failing to fire, but the query hook not noticing that its input has changed? I've edited the code above to show the whole function, and my custom hook is below.

function useApi(func?: () => Promise<Collection>) {
    return useQuery(
        ["doApiCall", func],
        func || (async () => await undefined),
        {
            enabled: !!func,
            keepPreviousData: true
        }
    )
}

Upvotes: 1

Views: 327

Answers (1)

TkDodo
TkDodo

Reputation: 28793

You can’t put a function into the queryKey. Keys need to be serializable. See: https://react-query.tanstack.com/guides/query-keys#array-keys

Upvotes: 1

Related Questions