Suppose I have 4 files in React JS, which are: 1 external file, 2 pages and 1 component:
- Login.js (1 page)
- counter.cache.js (1 file)
- Landing.js(1 page)
- Counter.js (1 component)
1- In Login.js, I have 3 buttons:
1-1. A button to login in using email and password.
1-2. A button to sign in with third party.
1-3. A button to navigate from Login.js to Landing.js.
Here is the code of Login.js:
import { useNavigate } from "react-router-dom";
import { queryClient } from "../App";
const Login = () => {
const navigate = useNavigate();
return(
<div
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<button
onClick={() => {
// Save 0 into query with queryKey: 'counter'
queryClient.setQueryData(["counter"], {
counter: 0,
});
}}
>
Login with email and password
</button>
<button
style={{ marginTop: "20px" }}
onClick={() => {
// Save 0 into query with queryKey: 'counter'
queryClient.setQueryData(["counter"], {
counter: 0,
});
}}
>
Sign in with third party
</button>
<button
style={{ marginTop: "20px" }}
onClick={() => {
// Navigate to Landing page
navigate("/landing");
}}
>
Go to landing page
</button>
</div>
);
}
export default Login;
2- In counter.cache.js, I have only one ‘useQuery’ which is useCounter:
Here is the code of counter.cache.js:
// Here I don't need to fetch anything, just to get data from this **useQuery**
export const useCounter = () =>
useQuery({
queryKey: ["counter"],
queryFn: () => null,
});
3- In Counter.js, I’m increasing the counter,then, updating the value using useState hook and into cache. In addition, to implement useEffect on the counter from useCounter if it’s changing.
Here is the code of Counter.js:
import { useEffect, useState } from "react";
import { useCounter } from "../cache/counter.cache";
const Counter = () => {
const { data, isLoading } = useCounter();
const [counter, setCounter] = useState(-1);
// Put the value of initial counter from cache which is 0
useEffect(() => {
if (data && !isLoading) {
setCounter(data.counter);
}
}, [data]);
// Implement useEffect on the counter from 'cache':
useEffect(() => {
if (data && data.counter !== 0) {
console.log(`Rendering Counter component`);
}
}, [data.counter]);
return (
<div
style={{
width: "200px",
height: "300px",
position: "fixed",
right: "0",
bottom: "0",
marginBottom: "30px",
border: "3px solid black",
}}
>
<h2>{`Counter is ${counter}`}</h2>
<button
onClick={() => {
data.counter += 1;
setCounter(data.counter);
}}
>
Increase
</button>
</div>
);
}
export default Counter;
4- In Landing.js, I’m just using the component Counter.js and implementing useEffect on the counter from useCounter if it’s changing (the same as implementing useEffect on the counter in Counter.js component above).
import { useCounter } from "../cache/counter.cache";
const Landing = () => {
const { data, isLoading } = useCounter();
useEffect(() => {
if (data && data.counter !== 0) {
console.log(`Rendering landing page`);
}
}, [data.counter]);
if(data && !isLoading)
return <Counter/>
}
export default Landing;
My problem is console.log(Rendering landing page
); is not being called, while ONLY console.log(Rendering Counter component
); is being called.
So, as conclusion, the useEffect is being called only in Counter.js component, but not in Landing.js page.
Note that I’m using the following react-query and react-query dev tools version:
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-query-devtools": "^4.29.12",
What I have tried (but in different scenario), I have implemented the following:
In the Counter.js component:
useEffect(()=>{
const event = new CustomEvent("new_event", { detail: SomeDataHere });
window.dispatchEvent(event);
},[counter])
In the Landing.js page:
useEffect(()=>{
const handleNewData = (data) => {
console.log("Rendering Landing component");
}
window.addEventListener("new_event", handleNewData);
return () => {
window.removeEventListener("new_event", handleNewData);
};
}, []);
Small hint: in the dev tools, there is a number ‘2’ beside the query key ‘counter’, and that’s because I have used the useCounter inside Landing.js and inside Counter.js component that is inside Landing.js page.
2
Answers
You should not be storing the resulting data in local state in your
Counter.js
file. Instead you should be deriving the state, and updating your queryCache. e.g.That said, it’s very unsual to use react-query for state that is not stored server side. Usually for a global state/store (like what you’re demonstrating) you’d use one of the various global state solutions like Redux, Zustand, or Jotai.
In your example, you do this:
That’s not allowed. Data in the React Query cache is immutable. When you directly modify it like that, React Query doesn’t know about the changes you made, so it can’t tell other components that things have been updated (which explains why one of the console.logs isn’t happening). In fact, that doesn’t tell either component to re-render — the only reason the re-render happens is because you also have the
(which you shouldn’t have because you’re duplicating the state between React Query and vanilla React state, and any time you duplicate state, you’re asking for problems — that’s why caching is one of the famously hard problems in computer science).