Unexpected result from apollo react's useQuery method - reactjs

For a project, where i've implemented authentication by running a GraphQL query inside a AuthenticationProvider from a context, I noticed the query is fetching data twice.
const AuthenticationProvider: FC = props => {
const {
loading,
data
} = useQuery(MeQuery)
if (loading) return null
return <AuthenticationContext.Provider value={{user: data?.me || null}} {...props} />
}
However the query runs perfect, I still wanted to know why it fetches the data twice. I did some googling, and came across this issue, where this answer was provided. I tried the same thing, with the skip option, based if the data is loaded.
const [skip, setSkip] = useState(false)
const {
loading,
data
} = useQuery(MeQuery, { skip })
useEffect(() => {
if (!loading && data?.me) {
setSkip(true)
}
}, [loading, data])
// ...
But when logging in, it stopped working.
const useLoginMutation = () => useMutation(LOGIN_QUERY, { update: (cache, { data }) => {
if (!data) {
return null
}
cache.writeQuery({ query: MeQuery, data: { me: data.login } })
}
})
The cache still get's updated with the right values, but doesn't retrieve the user anymore (null).
const { user } = useContext(AuthenticationContext)
What am I missing here? It seems the query did run and fetched the correct data.

You don't need to use context when you are using apollo useQuery. If you make a query first, then the data fetched will be stored in the cache. You can directly access the data from the cache for the second you run the query. Since useQuery has default fetchPolicy cache-first. Mean its check in the cache first, if no query made before it makes a network request.
If you want to avoid network loading. You can make a top-level component AuthWrapper.
const useUserQuery = () => useQuery(ME_QUERY);
const AuthWrapper = ({children}) => {
const {loading, data} = useUserQuery();
if(loading) return null
return children;
}
export GetUsetInThisComponent = ({}) => {
// Since we have fetched the user in AuthWrapper, the user will be fetched from the cache.
const {data} = useUserQuery();
// No you can access user from data?.user
// Rest of the application logic
}
// Wrap the component like this to avoid loading in the children components
<AuthWrapper>
<GetUserInThisComponent />
</AuthWrapper>

Related

How do I use the get request of axios by sending a query from the user and getting the values from the database for that particular query?

function AdminMemberSearchFirstName({ name }) {
const [data, setData] = useState([]);
const [query, setQ] = useState("");
function queryGiven(query) {
setQ(query);
}
async function getFilteredData() {
axios.get(`admin/member/firstname/${query}`).then((response) => {
console.log(response.data);
setData(data);
});
}
useEffect(() => {
getFilteredData(name);
}, []);
return (
<div>
<FirstNameForm queryGiven={queryGiven} />
<h1>{query}</h1>
</div>
);
}
I am using axios to get the data from the database using the given API. Instead of using the query variable, if I use the actual name, then it works. When I use the query variable, I think it passes an empty string when the page loads because of which I get 400 error code in the console. The value of query comes from the FirstNameForm component, and that one works. How can I fix this issue?
name is not defined anywhere. If you want to pass a value, pass query
If you want to make the request when query changes, list it in the effect hook dependencies
If you don't want to make requests for empty query, then wrap the call in a condition
// this could even be defined in another module for easier testing
const getFilteredData = async (name) =>
(await axios.get(
`admin/member/firstname/${encodeUriComponent(name)}`
)).data;
useEffect(() => {
// check for empty query
if (query.trim().length > 0) {
getFilteredData(query)
.then(setData)
.catch(console.error);
} else {
setData([]); // clear data for empty query
}
}, [ query ]); // this effect hook will run when query changes

React Recoil: State not being saved across navigation to different URLs within App

I'm getting started with Recoil for a React App, but running into some issues, or at least some behavior I'm not expecting.
I'd like to be able to use one component to render many different "views" based on the URL. I have a useEffect in this component that switches based on the location.pathname and based on that pathname, it'll make an API call. But before it makes the API call, it checks the length of the atom to see if it's empty or not, then will call the API and set the atom based on the API call.
However, when I navigate to a different URL and come back to one I've already visited, the API is called again, even though I've previously set the state for that URL.
The behavior I'm expecting is that once a URL has been visited and the return from the API is stored in an Atom, the API call isn't made again when leaving the URL and coming back.
Relevant code below:
Atom.js
export const reports = atom({ key: "reports", default: { country: [], network: [], }, });
the one component that will render different data based on the reports atom.
import { useRecoilState } from "recoil";
import { reports } from "../globalState/atom";
const TableView = ({ columns, }) => {
const location = useLocation();
const [report, setReport] = useRecoilState(reports);
const currentView = location.pathname.split("/")[1];
useEffect(() => {
const getReportsData = async () => {
switch (location.pathname) {
case "/network":
if (report[currentView].length === 0) {
const response = await fetch("/api");
const body = await response.json();
setReport(
Object.assign({}, report, {
[currentView]: body,
})
);
console.log('ran')
break;
}
getReportsData();
}, [])
As previously mentioned, that console.log is ran every time I navigate to /network, even if I've already visited that URL.
I've also tried doing this with selectors.
Atom.js
export const networkState = atom({
key: "networkState",
default: networkSelector,
});
export const networkSelector = selector({
key: "networkSelector",
get: async ({ get }) => {
try {
const body = await fetch("/api/network").then((r) => r.json());
return body;
} catch (error) {
console.log(error);
return [];
}
}
Component
import {useRecoilStateLoadable} from "recoil"
import {networkState} from "../globalState/atom";
const Table = ({columns}) => {
const [networkData, setNetworkData] =
useRecoilStateLoadable(networkState);
And then a switch statement based on networkData.state
}
Any help would be greatly appreciated, thank you!

React Firebase read data results in too many renders

I'm using Firebase realtime database for my react project. I try to follow the firebase documentation and use "onValue()" to retrieve data. Here is my code:
export default function Home() {
const {currentUser} = useAuth();
const [userinfo,setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
setUserinfo(data);
})
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
This would result in an error of too many re-renders. I don't know how to retrieve the data. If I use
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
console.log(data);
})
then the proper data would print out in the console perfectly fine. I also tried the following:
let info;
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
info = data;
})
console.log(info)
but info would just be undefined. I can't seem to figure out the problem here. How can I use the data?
It throws error too many re-renders because you are not using any lifecycle hook or function to update/change state value and once you update your state it will again re-render your whole component and then again you update the state and the same thing happens in the loop causing too many re-renders.
So to avoid this you need to put code that is responsible for listening to changes from DB and changing state inside a block which will only get called on specific events or function calls or etc.
In your case, I suggest using useEffect hook. see below code -
export default function Home() {
const { currentUser } = useAuth();
const [userinfo, setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
// this useEffect will get called only
// when component gets mounted first time
useEffect(() => {
// here onValue will get initialized once
// and on db changes its callback will get invoked
// resulting in changing your state value
onValue(ref(db, `users/${uid}`), snapshot => {
const data = snapshot.val();
setUserinfo(data);
})
return () => {
// this is cleanup function, will call just on component will unmount
// you can clear your events listeners or any async calls here
}
}, [])
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
Note - I have not worked with firebase real-time DB recently but by looking at the code and error I have added this answer, let me know if anything needs correction.

react apollo - clear data object before refetch

When using refetch of useQuery hook the data object is still defined. And when using infinite scrolling, only the first page will be refetched.
Is it possible to clear the data object before calling the refech so that we can start fresh?
const { data, loading, error, fetchMore, refetch } = useQuery(GET_ALL_ITEMS, {variables});
getNextPage = async () => { // merges results for infinite scrolling
await fetchMore({ variables,
updateQuery: (previousResult, { fetchMoreResult }) => {
const oldEntries = previousResult.items;
const newEntries = fetchMoreResult.items;
fetchMoreResult.items = [...oldEntries, ...newEntries];
return fetchMoreResult;
},
)
}
Can I do something like refresh = () => { data = null; refetch(); } but without directly mutating state?
I couldn't find a way to clear data but found a different approach in Apollo Client v3 docs.
Basically it's to ignore loaded data when loading or in case of error:
const { data: loadedData, loading, error, fetchMore, refetch } = useQuery(
GET_ALL_ITEMS,
{
variables,
notifyOnNetworkStatusChange: true, // important for adequate loading state
}
);
const data = (loading || error) ? undefined : loadedData;
Note, that in order to make loading work for refetch, you need to pass notifyOnNetworkStatusChange: true option.
It's also possible to get networkStatus from useQuery result and check it like this:
networkStatus === NetworkStatus.refetch
but it seems to work for me with loading and error only.

Can't perform a React state update on an unmounted component using context provider

For a React project, I'm using multiple context providers running GraphQL queries, providing all, or some components with my needed data. For this example, I have a BookProvider that queries all the books for a specific user.
export const BookProvider = ({ children }) => {
// GraphQL query for retrieving the user-belonged books
const { loading, data } = useQuery(gql`{
books {
id,
name,
description
}
}
`, {
pollInterval: 500
})
if(!data?.books) return null
return (
<BookContext.Provider value={data?.books}>
{!loading && children}
</BookContext.Provider>
)
}
I made sure the query wasn't loading and is available in the data object. For retreiving my data, I have a dashboard component wrapped in this provider. In here, there are other components loading the corresponding data.
const Dashboard = () => {
return (
<BookProvider>
// More components for loading data
</BookProvider>
)
}
The data is loaded like this:
const { id, name } = useContext(BookContext)
In the same way, I have a AuthenticationProvider, wrapping the whole application.
export const MeQuery = gql`{ me { id, email } }`
export const AuthenticationProvider = (props) => {
const { loading, data } = useQuery(MeQuery)
if(loading) return null
const value = {
user: (data && data.me) || null
}
return <AuthenticationContext.Provider value={value} {...props} />
}
For my application routes, checking if the user is authenticated, I use a PrivateRoute component. The Dashboard component is loaded by this PrivateRoute.
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user } = useAuthentication()
return user ? <Route {...rest} render={props => <Component {...props} />} /> : <Redirect to={{ pathname: '/login' }} />
}
When logging in, and setting the user object, there is no problem, however, when logging out, which is a component on the Dashboard, it re-directs me to the login page, but I receive the following error:
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at BookProvider (http://localhost:3000/static/js/main.chunk.js:2869:3)
at Dashboard
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:85417:29)
at PrivateRoute (http://localhost:3000/static/js/main.chunk.js:4180:14)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:85619:29)
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:85052:30)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:84672:35)
at Router
For logging out, I'm using the following code:
export function useLogout() {
const client = useApolloClient()
const logout = useMutation(gql`mutation Logout { logout }`, { update: async cache => {
cache.writeQuery({ query: MeQuery, data: { me: null }})
await client.resetStore()
}})
return logout
}
export default useLogout
This gets called like const [logout] = useLogout()
Questions
Why is this error occuring?
How can this be fixed?
Is it a good practice running queries in context providers?
I'll give it another try ;-)
You would use useLazyQuery instead, that seem to allow avoiding the bug that plagues useQuery:
const [ executeQuery, { data, loading } ] = useLazyQuery(gql`${QUERY}`);
And then manage the polling manually:
useEffect(function managePolling() {
const timeout = null
const schedulePolling = () => {
timeout = setTimeout(() => {
executeQuery()
schedulePolling()
}, 500)
}
executeQuery()
schedulePolling()
return () => clearTimeout(timeout)
}, [executeQuery])
(untested code, but you get the idea)
This was suggested on the same thread (https://github.com/apollographql/apollo-client/issues/6209#issuecomment-676373050) so maybe you already tried it...
It seems that your graphql query hook is polling data, and this will update its internal state, or try to do so, even if the component is unmounted.
I've read similar issues in that thread (and it looks like a bug because useQuery hook should detect when component is unmounted and stop polling)
One of the solutions that seems to fit your issue, is to control the polling so that you can stop it once the component unmounts.
It would look like so:
export const BookProvider = ({ children }) => {
// GraphQL query for retrieving the user-belonged books
const { loading, data, stopPolling, startPolling } = useQuery(gql`{
books {
id,
name,
description
}
}`, {fetchPolicy: "network-only"})
React.useEffect(function managePolling() {
startPolling(500)
return () => stopPolling()
})
if(!data?.books) return null
return (
<BookContext.Provider value={data?.books}>
{!loading && children}
</BookContext.Provider>
)
}
Does it suppress the warning?
Note: not sure if the {fetchPolicy: "network-only"} option is needed

Resources