reactjsreact-hooksreact-query

React Query changes behavior after 'useState' variable is updated


I have a very simple react application using React Query with two react components: App and BotConfig. I am experiencing React Query functions within a component change their behavior from working as expected to not working after updating a useState variable.

App -- Contains ReactQuery setup and maintains a single useState React variable called currentBotId, which is passed into BotConfig. currentBotId is client-side state.

...
function App() {
const [currentBotId, setCurrentBotId ] = useState("Bot#0")
    const bots = [...]
    const queryClient = new QueryClient()

    return (
        <QueryClientProvider client={queryClient}>
            <div id="app">
                <BotConfig bots={botsList} setCurrentBotId={setCurrentBotId} currentBotId={currentBotId}/>
            </div>
        </QueryClientProvider>
    )

export default App

BotConfig -- Updates currentBotId and also performs create/delete actions on a Bot entities managed by React Query. bots and their content are server-side state.

type Props = {
    bots: Bot[]
    currentBotId: string,
    setCurrentBotId: (botId: string) => void
}

function BotConfig({bots, currentBotId, setCurrentBotId}: Props) {
    const queryClient = useQueryClient()

// Create a new bot for the current bot id
    const mutateCreateBot = useMutation({
        mutationFn: (botId: string) => {
            const bot = createBot(botId)
            return Promise.resolve(bot)
        },
        onSuccess: (data) => {
            queryClient.setQueryData(['getNewBot'], data)
        },
    })

// Delete the current bot
    const mutateDeleteCurrentBot = useMutation({
        mutationFn: (botId: string) => {
            deleteBot(botId)
            return Promise.resolve(botId)
        },
        onSuccess: (data) => {
            queryClient.invalidateQueries({queryKey: ['getNewBot']})
        },
    })

    const getNewBotQuery = useQuery({
        queryKey: ['getNewBot'],
        queryFn: () => {
            const bot = getBot(currentBotId)
            if (bot == undefined) { return null } else { return bot }
        }
    })

    const handleClick = (botId: string) => {
        setCurrentBotId(botId)
    };

    return (
        <div>
            <div>
                <div>{getNewBotQuery.data != null ? <h1>{getNewBotQuery.data.id}</h1> : "No Bot Created"}</div>
                    <button
                    onClick={() => {mutateCreateBot.mutate(currentBotId)}}>
                    Create Bot
                </button>
                <button
                    onClick={() => {mutateDeleteCurrentBot.mutate(currentBotId)}}>
                    Delete Bot
                </button>
            </div>
            <List>
                {bots.map((bot) => (
                    <ListItem disablePadding>
                        <ListItemButton
                            selected={currentBotId == bot.id}
                            onClick={() => handleClick(bot.id)}
                            style={{ backgroundColor: bot.id == currentBotId ? 'lightblue' : 'green', }}>
                            <ListItemText primary={bot.id} />
                        </ListItemButton>
                    </ListItem>
                ))}
            </List>
        </div>
    );
}

export default BotConfig;

This generates a UI with 3 bots that can either have content populated or not.

Populated Bot Content

Deleted Bot Content

The problem I am facing is straight forward.

If I load the page and do not modify currentBotId, everything works as expected. The create and delete buttons both modify the server state AND cause the client UI to update after the server updates.

If I modify currentBotId by selecting another bot identifier, both buttons continue to update the server state as expected for the appropriate Bot, but no longer cause UI updates, even if I navigate back to the first bot that was originally working.

I have found that moving the useState variable currentBotId inside of the BotConfig component resolves the issues, so the root cause appears to be related to the fact that it is outside the component. In my full application moving currentBotId into this component is not possible.

I am new to React and React Query, so any insight into why this would be failing is much appreciated.


Solution

  • Every time you use setCurrentBotId it causes a rerender for App component and it recreates a new instance of query client. To fix this, define the query client like below

    const [queryClient] = useState(new QueryClient())