refetching a query with react-apollo: why is `loading` not true? - reactjs

I'm trying Apollo and using the following relevant code:
const withQuery = graphql(gql`
query ApolloQuery {
apolloQuery {
data
}
}
`);
export default withQuery(props => {
const {
data: { refetch, loading, apolloQuery },
} = props;
return (
<p>
<Button
variant="contained"
color="primary"
onClick={async () => { await refetch(); }}
>
Refresh
</Button>
{loading ? 'Loading...' : apolloQuery.data}
</p>
);
});
The server delays for 500ms before sending a response with { data: `Hello ${new Date()}` } as the payload. When I'm clicking the button, I expect to see Loading..., but instead the component still says Hello [date] and rerenders half a second later.
According to this, the networkStatus should be 4 (refetch), and thus loading should be true. Is my expectation wrong? Or is something regarding caching going on that is not mentioned in the React Apollo docs?
The project template I'm using uses SSR, so the initial query happens on the server; only refetching happens in the browser - just if that could make a difference.

I think that you need to specify notifyOnNetworkStatusChange: true in the query options (it's false by default).

Related

Next.js refresh values without reloading page in props list

probably I got a little bit lost on the following task. I have an admin page in my application where you can see all the posts from the plattform. I'm requesting the posts from an api and Im displaying them on the page as a list. I Inserted two buttons to enable/disable the post by calling a function which does tag the post to be enabled/disabled.
Everything works fine, but I want to change the state of the button without reloading the page. Im passing disable parameter threw the button tag. I don't know why its not working, if I console.log the values its already changed there. Is this a proper way to use useeffect? I tried to use it but I failed using it correct.
Somebody can help please?
Simplified Code ( I removed the enable function, since its nearly the same like disable)
export default function Feed(props) {
const [postStatus, setPostStatus] = useState(props.posts)
async function disablePost(id, e){
e.preventDefault();
const userInput = { postId: id }
try{
const res = await axios.post(`${baseurl}/api/auth/admin/disable-post`, userInput, {
})
var currentPostStatus = postStatus;
currentPostStatus.map((el) => {
if(el.id === id){
el.disabled = true;
}
console.log(el.id)
});
setPostStatus(currentPostStatus)
console.log(postStatus)
}catch(error){
console.log(error)
return
}
}
return (
<>
<HeaderAdmin></HeaderAdmin>
<div>
{/* {console.log(props.userCount)} */}
<p>Alle Posts</p>
{postStatus.map((post) =>
<React.Fragment key={post.id}>
<p>{post.id}</p>
<p>{post.email}</p>
<p>{post.desc}</p>
<p>{post.disabled == true ? 'Post deaktviert' : 'Post aktiviert'}</p>
<button disabled={ post.disabled } onClick={(e) => disablePost(post.id, e)}>Post deaktivieren</button>
<button disabled={ !post.disabled } onClick={(e) => enablePost(post.id, e)}>Post aktivieren</button>
</React.Fragment>
)}
</div>
</>
);
}
Your screen can't refresh to the new version after you clicked the disable or enable.
var currentPostStatus = postStatus;
currentPostStatus.map((el) => {
if(el.id === id){
el.disabled = true;
}
});
In your code, you are only changing the internal property of postStatus, but React only cares about the reference of the object. so you need to do
setPostStatus([...currentPostStatus])
The above line create a new array. I personally believe this is something React should improve in the future, because half of the question about React in stackoverflow is talking about this :)

FetchMore : Request executed twice every time

I am trying to implement pagination in a comment section.
I have normal visual behavior on the website. When I click the get more button, 10 new comments are added.
My problem is the request is executed twice every time. I have no idea why. The first time, it is executed with a cursor value, the second time without it. It seems that the useQuery hook is executed after each fetchMore.
Any help would be appreciated. Thanks!
component :
export default ({ event }) => {
const { data: moreCommentsData, fetchMore } = useQuery(getMoreCommentsQuery, {
variables: {
id: event.id,
},
fetchPolicy: "cache-and-network",
});
const getMoreComments = () => {
const cursor =
moreCommentsData.event.comments[
moreCommentsData.event.comments.length - 1
];
fetchMore({
variables: {
id: event.id,
cursor: cursor.id,
},
updateQuery: (prev, { fetchMoreResult, ...rest }) => {
return {
...fetchMoreResult,
event: {
...fetchMoreResult.event,
comments: [
...prev.event.comments,
...fetchMoreResult.event.comments,
],
commentCount: fetchMoreResult.event.commentCount,
},
};
},
});
};
return (
<Container>
{moreCommentsData &&
moreCommentsData.event &&
moreCommentsData.event.comments
? moreCommentsData.event.comments.map((c) => c.text + " ")
: ""}
<Button content="Load More" basic onClick={() => getMoreComments()} />
</Container>
);
};
query :
const getMoreCommentsQuery = gql`
query($id: ID, $cursor: ID) {
event(id: $id) {
id
comments(cursor: $cursor) {
id
text
author {
id
displayName
photoURL
}
}
}
}
`;
Adding
nextFetchPolicy: "cache-first"
to the useQuery hook prevents making a server call when the component re renders.
This solved my problem.
Could it be that the second request you are seeing is just because of refetchOnWindowFocus, because that happens a lot...
I had a similar problem and I solved it by changing the fetchPolicy to network-only.
Please note, if you are hitting this issue with #apollo/client v3.5.x there was a bug related to this which was fixed from v3.6.0+. That commit solved the duplicated execution issue in my case.
It only makes one request if you are worried about that but the react component refreshes but not rerender each time an item in the useQuery hook changes.
Take for instance it would refresh the component when loading changes, making it seem like it sends multiple requests.

useQuery not returning up to date data

i have a homepage (/home) with a list of products as cards (retrieved
via useQuery) each of which has an upvote button
when I click upvote,
i trigger a mutation to upvote + a UI change to update the vote
count
when i go to another page, and then go back to /home,
useQuery doesn’t retrieve the products with the correct vote count
however, when I check my DB, the products all have the correct vote
count.
Why doesuseQuery not return the right amount until i do another page
refresh?
for reference, here it is below:
const Home = props => {
const {data, loading, error} = useQuery(GET_PRODUCTS_LOGGED_IN, {
variables: {
userid: props.userid
}
});
console.log(
'data', data.products // this data is outdated after I go from /home -> /profile -> /home
);
return (
<Container>
{_.map(data.products, product => (
<VoteButton
hasVoted={product.hasVoted}
likes={product.likes}
productid={product.productid}
/>
))}
</Container>
);
}
const VoteButton = ({likes, hasVoted, productid}) => {
const [localHasVoted, updateLocalHasVoted] = useState(hasVoted);
const [likesCount, updateLikesCount] = useState(likes);
const [onVote] = useMutation(VOTE_PRODUCT);
const onClickUpvote = (event) => {
onVote({
variables: {
productid
}
})
updateLocalHasVoted(!localHasVoted);
updateLikesCount(localHasVoted ? likesCount - 1 : likesCount + 1);
}
return (
<VoteContainer onClick={onClickUpvote}>
<VoteCount >{likesCount}</VoteCount>
</VoteContainer>
);
};
On your useQuery call, you can actually pass it a config option called 'fetch-policy' which tells Apollo how you want the query to execute between making the call or using the cache. You can find more information here, Apollo fetch policy options.
A quick solution would be be setting fetch-policy to cache and network like the the example below.
const {data, loading, error} = useQuery(GET_PRODUCTS_LOGGED_IN, {
variables: {
userid: props.userid
},
fetchPolicy: 'cache-and-network',
});
You can also make it so that when your mutation happens, it will run your query again by setting the 'refetch-queries' option on useMutation like the code below.
This will cause your query to trigger right after the mutation happens.
You can read more about it here Apollo mutation options
const [onVote] = useMutation(VOTE_PRODUCT, {
refetchQueries: [ {query: GET_PRODUCTS_LOGGED_IN } ],
});

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

Apollo React Nested Query & Mutation

I have a component that I'd like to fetch data for a profile and allow posting messages on the same view. I've created a simple profile component here, and have the mutation set within the query, similar to the Apollo React tutorial.
When I run this, I get a properly rendered query with data. When the button is pressed, the mutation occurs but the page re-renders with an empty object in the Query's data parameter and the page errors with Cannot read property 'name' of undefined (which is expected given a blank data object).
Is there a better approach here?
const GET_PROFILE = gql`
query GetProfileQuery($profileId: ID!) {
getProfileInfo(profileId: $profileId) {
name
}
}
`;
const ADD_ENDORSEMENT = gql`
mutation AddEndorsement($profileId: ID!, $body: String!) {
addEndorsement(profileId: $profileId, body: $body) {
endorsementId
}
}
`;
const Profile = props => {
const profileId = props.match.params.profileId;
return (
<Query query={GET_PROFILE} variables={{ profileId: profileId }}>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<Fragment>
<h1>Welcome {data.getProfileInfo.name}</h1>
<Mutation mutation={ADD_ENDORSEMENT}>
{(addEndorsement, { loading, error }) => (
<div>
<button
onClick={e => {
addEndorsement({
variables: {
profileId: profileId,
body: "This is a test."
}
});
}}
>
Save
</button>
{loading && <p>Loading...</p>}
{error && <p>Error :(</p>}
</div>
)}
</Mutation>
</Fragment>
);
}}
</Query>
);
};
I had a similar issue were data turned up undefined. In my case the problem was caused by a Query subscribeToMore option I was using invalidating the cache by silently failing to merge the updated data into the original Query results (it did not contain exactly identical data fields).
The question doesn't mention subscriptions, but it will be a similar cache invalidation problem.
To fix I'd first try making sure the Query and Mutation's return identical data.
If that doesn't resolve, then depending on responsiveness needs either look at the "refetchQueries/awaitRefetchQueries" to refresh the Query, or manually poking the updated data into the local cache with "update" options on the Mutation.
Good luck.
Try adding the following to the onClick mutation, it should help avoid needing to update the cache manually, although that would be the next step. You can pass just the string name of the query to refetchQueries, you don't need to pass the actual query there. Another trick is to ensure you are getting the same fields back so the cache object matches, (not sure what type you are returning on the query/mutation.
onClick={e => {
addEndorsement({
variables: {
profileId: profileId,
body: 'This is a test.',
},
refetchQueries: ['GetProfileQuery']
})
}}

Resources