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 } ],
});
Related
I am using graphQl with react with apollo client and mongoose. Sometimes when I click on a component. rand om data will return from this useQuery as null.
// must define novel as a state to use useEffect correctly
const [novel, setNovel] = useState({});
const { loading, data } = useQuery(GET_NOVEL, {
variables: { _id: novelId }
});
// use effect ensures that all novel data is completely loaded
// before rendering the SingleNovel page
useEffect(() => {
console.log(data?.novel);
// if there's data to be stored
if (data) {
setNovel(data.novel)
}
}, [data, loading, novel]);
export const GET_NOVEL = gql`
query getNovel($_id: ID!) {
novel(_id: $_id) {
_id
title
description
penName
user {
_id
username
email
}
favorites{
_id
}
createdAt
reviews {
_id
reviewText
rating
createdAt
user{
_id
username
}
}
chapterCount
reviewCount
}
}
`
Specifically, the novel.user.username and reviews.rating property come back as null. On reload of the page however, the data seems to populate the fields normally.
How can I fix this?
Heres the resolver
novel: async (parent, { _id }) => {
// returns single novel from the novel id given
const novel = await Novel.findOne({ _id })
.populate('user')
// populate the reviews for the novel but also populate
// the info within the reviews of the user who made each review.
.populate({
path: 'reviews',
populate: {path: 'user'}
})
.exec();
console.log(novel)
return novel;
},
I'm new to React Query and am wondering what that's about--forestalling execution of my GraphQL query until I click anywhere within the browser's viewport. Shouldn't the query just execute straight away?
Here's my code:
import { useUrlHashParameters } from "../src/hooks";
import { useQuery } from "react-query";
import { gql, request } from "graphql-request";
export default function Home() {
const parameters = useUrlHashParameters();
const { data } = useQuery("test", async () => {
if (parameters) {
localStorage.setItem("accessToken", parameters.accessToken);
const url = "http://localhost:3001/graphql";
const document = gql`
query {
products {
id
title
description
totalInventory
__typename
}
}
`;
const requestHeaders = {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
};
return await request({
url,
document,
requestHeaders,
});
}
});
console.log(data);
return (
<div className={styles.container}>
<main className={styles.main}>
<Image
src="/blah-logo.png"
alt="Blah"
width={300}
height={75}
/>
{data?.products.map(({ title }, index: number) => (
<div key={index}>{title}</div>
))}
</main>
</div>
);
}
Shouldn't the query just execute straight away?
Yes, it should, and it likely does. My best guess is that on the first render, the parameters are undefined, and since you check for if (parameters) in the queryFn without doing anything in an else branch, the queryFn will just return a Promise that resolves to undefined (which will be illegal in v4 btw).
Then, when you focus the window, you get a refetch, in which case parameters likely exist.
You can verify this by putting a log statement into the queryFn, but outside the if statement.
Further, all parameters that your query uses should be part of the query key. That makes sure that you get automatic refetches when the parameters are changing. So changing your query key to:
["test", parameters]
and setting: enabled: !!parameters instead of the if to stop the query from running if there are no parameters is likely the best course of action:
const parameters = useUrlHashParameters();
const { data } = useQuery(
["test", parameters],
() => request({...}),
{
enabled: !!parameters
}
)
The objective is to, by pressing a button, delete the object from the database.
To do that, I have to pass the ID of the object I want to delete from the database to the axios query. But I'm stuck trying to do it.
In my opinion the problem is I am not passing the ID to erase to the query, since the query seems right to me.
File: persons.js
import axios from 'axios'
const baseUrl = 'http://localhost:3001/persons'
const deleteContact = (id) =>{
const request = axios.delete('{$baseUrl}/${id}')
request.then(response =>response.data)
}
export default {
deleteContact: deleteContact,
}
The button that should call the function to delete:
File: person.js
import React from 'react'
const Person = ({ person, deleteContact }) => {
return (
<li>
{person.name} {person.number}
<button onClick={deleteContact(person.id)}>Delete {person.id} </button>
</li>
)
}
export default Person
So, by pressing the button I execute the deleteContact funtion and I pass to that function the person.id so it sends the id to delete.
Here is waht's wrong. I don't know how to make the function deleteContact.
I have tried this, but of course I am not sending any props. It's wrong and does nothing. I get the error TypeError: deleteContact is not a function.
const deleteContact = (id) => {
}
The deleteContact funtion I try to implement is on the file App.js
It is something obvious I am missing here. But I can't figure out what is.
Likely something basic, but I have been stuck here for a while, as silly this may seem to be.
File: App.js
import React, { useState, useEffect } from 'react'
import Person from './components/Person'
import Form from './components/Form'
import Filter from './components/Filter'
import FilterResults from './components/FilterResults'
import contactService from './services/persons'
//npm run server
const App = () => {
//Reminder: current state, function that updates it, initial state.
const [ newName, setNewName ] = useState('')
const [ newNumber, setNewNumber ] = useState('')
const [contacts, setContacts] = useState([])
//Filter
const [ filter, setFilter ] = useState('')
//contactService is importer from /services/persons.
//.getAll is like typing: axios.get('http://localhost:3001/persons')
//Effect hooks used to fetch data from the server. The data fetched is saved
//into the contacts state variable
useEffect(() => {
contactService
.getAll()
.then(response => {
setContacts(response.data)
console.log(contacts)
})
}, [])
/*
second parameter of useEffect is used to specify how often the effect
is run. If the second parameter is an empty array [],
then the effect is only run along with the first render of
the component. */
console.log('render', contacts.length, 'contacts')
//adding new persons
const addPerson = (event) => {
event.preventDefault()
/* complete the addPerson function for creating new persons */
const personObject = {
name: newName,
number: newNumber,
//The server will create the id
//id: persons.length + 1,
}
//Adding the data to the server
/*
using separate server comunication module from persons.js
"create" instead of previous code:
axios
.post('http://localhost:3001/persons', personObject)
replaced by:
contactService
.create(personObject)
*/
contactService
//Passing personObject to create
.create(personObject)
.then(response => {
console.log(response)
//After concat, the fiel is set to blank again ('').
//Updating state after creating, to display created contact.
setContacts(contacts.concat(personObject))
setNewName('')
setNewNumber('')
})
}
//Delete contacts
const deleteContact = (personObject) => {
}
const handlePersonChange = (event) => {
console.log(event.target.value)
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
console.log(event.target.value)
setNewNumber(event.target.value)
}
const handleFilterChange = (event) => {
setFilter(event.target.value)
}
const personsToShow = filter === ''
? contacts
: contacts.filter(person =>
person.name.toLowerCase().includes(filter.toLowerCase()))
const row_names = () => personsToShow.map(person =>
<p key={person.name}>{person.name} {person.number} </p>
)
return (
<div>
<Filter value={filter} onChange={handleFilterChange} />
<Form
onSubmit={addPerson}
name={{value: newName, onChange: handlePersonChange}}
number={{value: newNumber, onChange: handleNumberChange}}
deleteContacts={() => deleteContact()}
/>
<h2>Numbers from database</h2>
{/* The contents of the database are stored on the variable contacts.
I map through the array. Person.js component used. */}
<ul>
{contacts.map(person =>
//Pass all the props from person to Person.js
<Person
key={person.id}
person={person}
/>
)}
</ul>
<h2>Filter results</h2>
<FilterResults persons={row_names()} />
</div>
)
}
export default App
The dabatbase is hardcoded json.
file db.json
"persons": [
{
"name": "ss",
"number": "ssssd",
"id": 17
},
{
"name": "ddd",
"number": "6tyhhth",
"id": 18
},
{
"name": "almejas",
"number": "1234",
"id": 19
},
{
"name": "pailo",
"number": "244",
"id": 20
}
]
}
OK. Once again. To handle API requests you have to build API server first. When you send ajax reqests they must go somewhere, and there must be a program listening requests and doing something what depends on request params and body. The repository you show contains only frontend logic and it is OK because server side logic and front-end can be kept separately and run indepentendly. In the same git account you may find some other reps like this one https://github.com/inci-august/fullstackopen/tree/d6680a40d03536e20ee9537cc64e1cb57dd6b74a/part3/phonebook-backend containing back-end implementation. So you build API server, accepting requests, doing something (create/delete posts, users, auth etc) and sending something back, and AFTER you find it working you can send API requests from frontend. Before front part is created you may use apps like Postman to test your API server.
UPDATE:
You mentioned the following link https://fullstackopen.com/en/part2/altering_data_in_server containing the same I have already said - the server side logic does not suppose to be implemented on this step.
In the next part of the course we will learn to implement our own
logic in the backend. We will then take a closer look at tools like
Postman that helps us to debug our server applications
As for you question - the "props" in requests to server can be sent by params in address string like:
axios.delete("api/person/2")
In the example above we say to server that we want the person with id=2 to be deleted. On the server side it will be catched with instruction like:
router.delete("api/person/:id", (req, res) => {
const id = req.params.id
// here you delete the person information and send response back
})
ID here will be handled by server as parameter and you will have an access to its value for further actions.
The exercise expected me to remove objects from the json database just using axios. Perhaps I did not express myself clarly enough, but I finally solved it.
I just wanted to pass the id to delete of each object to the axios request.
On the return sttement of App.js, I pass the person object as props to deleteContactOf
<ul>
{contacts.map(person =>
//Pass all the props from person to Person.js
//Here you pass the props
<Person
key={person.id}
person={person}
deleteContact={() => deleteContctOf(person)}
/>
)}
</ul>
This is how the function deleteContactOf looks like
//Delete contacts
const deleteContctOf = (person) => {
console.log(person)
console.log('delete contact ' + person.id + ' ????')
if (window.confirm("Do you really want to delete this person")) {
contactService
.remove(person.id)
} else {
return
}
}
deleteContactOf passes the id (person.id) to the axios request on file perons.js
Now the id is passed. That's were I was failing.
const remove = (id) => {
const req = axios.delete(`${baseUrl}/${id}`)
return req.then((res) => res.data)
}
Now by clicking the delete button, the contacts that belong to that id are deleted
<button onClick={deleteContact}>Delete</button>
Thanks for your time effort. Of course this is implemented with APIs in real life. This was just an specific exercise I had to solve.
Thanks
Suppose data - is data from a parent query.
Child react-component:
const ShowDetails = ({data}) => {
const { loading, error, data_details } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache -- asks for additional fields that are missing in data.
When (!loading && !error) data_details will have requested fields.
Issue: data_details will have only requested fields.
Question: Is there a way to use parent data with merged-additional-requested fields in ShowDetails and ignore data_details?
In Chrome with help of Apollo devtools I see that apollo-cache has one entry from merged data and data_details.
I do not want to re-fetch all existed entries in data.
Example:
Parent component query:
const bookQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
author
}
}
`
Details query:
const bookEditionsQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
editions {
publisher
year
}
}
}
`
const bookReviewQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
review {
user
score
date
}
}
}
`
All this queries will populate the same bucket in Apollo cache: book with id.
What is necessary to achieve: in react component BookDetails:
have 1 object with:
data.author
data.editions[0].year
data.review[0].user
Logically - this is one entry in cache.
Thank you for your help.
Almost nothing to save by using already fetched [and passed from parent] data ... only author ... all review and edition must be fetched, no cache usage at all.
... fetching review and editions by book resolver helps apollo cache to keep relation but also requires API to use additional ('book') resolver [level] while it is not required ... review and editions resolvers should be callable directly with book id ... and f.e. can be used by separate <Review /> sub component ... or review and editions called within one request using the same id parameter.
Just use data and dataDetails separately in component - avoid code complications, keep it simply readable:
const ShowDetails = ({data}) => {
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
if(loading) return "loading...";
return (
<div>
<div>author: {data.author}</div>
{dataDetails.review.map(...
... if you really want to join data
const ShowDetails = ({data}) => {
const [bookData, setBookData] = useState(null);
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache, {
onCompleted: (newData) => {
setBookData( {...data, ...newData } );
}
});
if(bookData) return ...
// {bookData.author}
// bookData.review.map(...
I have mutation as follows:
<Mutation
mutation={ADD_NEW_SLOT}
refetchQueries={() => [{ query: GET_COMPANY_ADDRESSES, variables: { companyId: this.props.session.company.id } }]}
awaitRefetchQueries={true}
>
.......
</Mutation>
Where GET_COMPANY_ADDRESSES is exported from a parent component.
But it doesn't refresh after the mutation is done.
What am I doing wrong?
UPDATE
The return of the render function in the parent component is as follows:
<Query query={GET_COMPANY_ADDRESSES} variables={{companyId: session.company.id}} notifyOnNetworkStatusChange={true} fetchPolicy={'cache-and-network'}>
{({loading, error, refetch, data}) => {
if (loading) return <LoadingIndicator/>;
if (error) return <ErrorIndicator description={error.message}/>;
const treeNodes = convertSlotsToTree(data);
const address = data.companyAddresses[1];
return (
<AddSlot address={address}
toggleSlotForm={this.props.togglePanel}
session={this.props.session}/>
)
}}
</Query>
The graphql query is in the same file and it is as follows:
export const GET_COMPANY_ADDRESSES = gql`
query CompanyAddresses($companyId: Int!) {
companyAddresses(companyId: $companyId) {
id
name
default
compound
address {
id
addressFull
countryCode
city
postCode
slotSet{
id
area
zone
aisle
side
level
position
disabled
col
printEntry
fullName
}
}
}
}
`;
It still does not work with react-apollo 3.1.x w/o workaround.
It seems that if you use these two things, updates will be sent to Query:
Set option for Query: fetchPolicy="cache-and-network" (you use this already).
Comment refetchQueries and use Query's refetch instead. Attach function to Mutation's onCompleted, and call refetch from the Query directly. You should see graphql query in HTTP requests, and UI should updates itself as well.
Note. "cache-first" as fetchPolicy did not work, although I guess cache is updated, but UI does not.
With Angular I found this working, as expected (note the "no-cache" flag):
this.postsQuery = this.apollo.watchQuery<any>({
query: GET_USER_ON_ROLE,
fetchPolicy: "no-cache",
variables: {
roleId : this.roleId
}
});
this.querySubscription = this.postsQuery
.valueChanges
.subscribe(({ data, loading }) => {
this.loading = loading;
this.allUsers = data.getAllUsersPerRoleUI;
});
An you need to reload once the mutation is over using refetch():
public reloadFunction()
{
this.postsQuery.refetch()
}