I am digging graphql so I followed a tutorial, And I stucked in this part.
Home.js
function Home() {
const {
loading,
data: { getPosts: posts } // <===## Here ##
} = useQuery(FETCH_POSTS_QUERY);
return (
<div>
{loading ? (
<h1>Loading posts..</h1>
) : (
posts &&
posts.map((post) => (
<p>
{post.content}
</p>
))
)}
</div>
);
}
const FETCH_POSTS_QUERY = gql`
{
getPosts {
id
content
}
}
`;
export default Home;
resolver
Query: {
async getPosts() {
try {
const posts = await Post.find().sort({ createdAt: -1 });
return posts;
} catch (err) {
throw new Error(err);
}
}
},
Whole code: https://github.com/hidjou/classsed-graphql-mern-apollo/tree/react10
In above example is working well, and it use it use data: { getPosts: posts } for deconstruction of returned data. but In my code, I followed it but I got an error
TypeError: Cannot read property 'getPosts' of undefined
Instead, If I code like below,
function Home() {
const {
loading,
data // <===## Here ##
} = useQuery(FETCH_POSTS_QUERY);
if(loading) return <h1>Loading...</h1>
const { getPosts: posts } = data // <===## Here ##
return (
<div>
{loading ? (
<h1>Loading posts..</h1>
) : (
posts &&
posts.map((post) => (
<p>
{post.content}
</p>
))
)}
</div>
);
}
It working well. Seems like my code try to reference data before it loaded. But I don't know why this happen. Code is almost same. Different things are 1. my code is on nextjs, 2. my code is on apollo-server-express. Other things are almost same, my resolver use async/await, and will return posts. Am I miss something?
my resolver is like below.
Query: {
async getPosts(_, { pageNum, searchQuery }) {
try {
const perPage = 5
const posts =
await Post
.find(searchQuery ? { $or: search } : {})
.sort('-_id')
.limit(perPage)
.skip((pageNum - 1) * perPage)
return posts
} catch (err) {
throw new Error(err)
}
},
Your tutorial may be out of date. In older versions of Apollo Client, data was initially set to an empty object. This way, if your code accessed some property on it, it wouldn't blow up. While this was convenient, it also wasn't particularly accurate (there is no data, so why are we providing an object?). Now, data is simply undefined until your operation completes. This is why the latter code is working -- you don't access any properties on data until after loading is false, which means the query is done and data is no longer undefined.
If you want to destructure data when your hook is declared, you can utilize a default value like this:
const {
loading,
data: { getPosts: posts } = {}
} = useQuery(FETCH_POSTS_QUERY)
You could even assign a default value to posts as well if you like.
Just keep in mind two other things: One, data will remain undefined if a network error occurs, even after loading is changed to true, so make sure your code accounts for this scenario. Two, depending on your schema, if there's errors in your response, it's possible for your entire data object to end up null. In this case, you'll still hit an issue with destructuring because default values only work with undefined, not null.
Related
I have a custom hook that looks something like this:
import { useQuery, useQueryClient } from 'react-query'
import { get } from '#/util/api' // Custom API utility
import produce from 'immer' // Using immer for deep object mutation
export function useData() {
const queryClient = useQueryClient()
const { data, isSuccess } = useQuery(
'myData', () => get('data')
)
function addData(moreData) {
const updatedData = produce(data.results, (draft) => {
draft.push(moreData)
})
setData(updatedData)
}
function setData(newData) {
queryClient.setQueryData('myData', newData)
}
return {
data: data && data.results,
setData,
addData,
}
}
My data in data.results is an array of objects. When I call addData it creates a copy of my current data, mutates it, then calls setData where queryClient.setQueryData is called with a new array of objects passed in as my second argument. But my cached data either doesn't update or becomes undefined in the component hooked up to the useData() hook. Does anyone know what I'm doing wrong?
code looks good from react-query perspective, but I'm not sure if that's how immer works. I think with your code, you will get back the same data instance with just a new data.results object on it. I would do:
const updatedData = produce(data, (draft) => {
draft.results.push(moreData)
})
I am sending a bulk data request to Shopify graphql to get all the products in the store, which may take a while depending on how many products the store has, so instead of using useQuery, Shopify recommends to send a bulk request using useMutation more on that here , anyway here is my code.
const BULK_INIT_MUTATION = gql`
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
images{
edges{
node{
id
originalSrc
altText
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
`;
const Index = () => {
const [createBulkRequest, { data }] = useMutation(BULK_INIT_MUTATION);
createBulkRequest();
return <Page>Hi Index</Page>;
};
Above should initiate a bulk request to shopify and as per Shopify docs it will take them anything between couple seconds to couples minutes to have the data ready for me to fetch using useQuery with below query
query {
currentBulkOperation {
id
status
errorCode
createdAt
completedAt
objectCount
fileSize
url
partialDataUrl
}
}
When everything is all good above query will return something like this
{
"data": {
"currentBulkOperation": {
"id": "gid:\/\/shopify\/BulkOperation\/720918",
"status": "COMPLETED",
"errorCode": null,
"createdAt": "2019-08-29T17:16:35Z",
"completedAt": "2019-08-29T17:23:25Z",
"objectCount": "57",
"fileSize": "358",
"url": "https:\/\/storage.googleapis.com\/shopify\/dyfkl3g72empyyoenvmtidlm9o4g?<params>",
"partialDataUrl": null
}
},
...
}
now all I am interested at is the url value, which should link to an address where I can get the actual data.
Now the issue is unless the Shopify Server had all the time it needed to process my request, it will always return the url value as undefined, what I need is a way to maybe constantly request the data after x time until it finally returns the url hence the operation status's completed and then re-render.
I am not sure what's the best way to approach this is, I am thinking something like setInterval, but I'm really not sure. so any suggestion would be quite helpful as I've been stuck with this for couple days now?
I have already tried useEffect like below
const Index = () => {
const [response, setData] = useState({});
const [createBulkRequest, { data }] = useMutation(BULK_INIT_MUTATION);
createBulkRequest();
useEffect(() => {
const timer = setInterval(() => {
const { loading, error, data } = useQuery(BULK_STATUS_QUERY);
console.log('returned data', data);
setData(data);
}, 5000);
// clearing interval
return () => clearInterval(timer);
});
return <Page>Hi Index</Page>;
};
However doing this throws this error.
Unhandled Runtime Error Error: Invalid hook call. Hooks can only be
called inside of the body of a function component. This could happen
for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug
and fix this problem.
If you need to schedule some task after x amount of time in order to retrieve data and update some state if defined, then you can use the setInterval() method calls a function in a useEffect hook (make sure to clear your interval in the return of that useEffect)
countSubcategories() function returns [object Promise] where it should return row counts of mapped subcategories.
This code is in vue.js & Laravel, Any suggestions on this?
<div v-for="(cat,index) in cats.data" :key="cat.id">
{{ countSubcategories(cat.id) }} // Here subcategories row counts should be displayed.
</div>
<script>
export default {
data() {
return {
cats: {},
childcounts: ""
};
},
created() {
this.getCategories();
},
methods: {
countSubcategories(id) {
return axios
.get("/api/user-permission-child-count/" + `${id}`)
.then(response => {
this.childcounts = response.data;
return response.data;
});
},
getCategories(page) {
if (typeof page === "undefined") {
page = 1;
}
let url = helper.getFilterURL(this.filterpartnerForm);
axios
.get("/api/get-user-permission-categories?page=" + page + url)
.then(response => (this.cats = response.data));
}
}
};
</script>
As Aron stated in the previous answer as you are calling direct from the template the information is not ready when the template is rendered.
As far as I understood you need to run getCategories first so then you can fetch the rest of your data, right?
If that's the case I have a suggestion:
Send an array of cat ids to your back-end and there you could send back the list of subcategories you need, this and this one are good resources so read.
And instead of having 2 getCategories and countSubcategories you could "merge" then like this:
fetchCategoriesAndSubcategories(page) {
if (typeof page === "undefined") {
page = 1;
}
let url = helper.getFilterURL(this.filterpartnerForm);
axios
.get("/api/get-user-permission-categories?page=" + page + url)
.then(response => {
this.cats = response.data;
let catIds = this.cats.map(cat => (cat.id));
return this.countSubcategories(catIds) // dont forget to change your REST endpoint to manage receiving an array of ids
})
.then(response => {
this.childcounts = response.data
});
}
Promises allow you to return promises within and chain .then methods
So in your created() you could just call this.fetchCategoriesAndSubcategories passing the data you need. Also you can update your template by adding a v-if so it doesn't throw an error while the promise didn't finish loading. something like this:
<div v-if="childCounts" v-for="(subcategorie, index) in childCounts" :key="subcategorie.id">
{{ subcategorie }} // Here subcategories row counts should be displayed.
</div>
Hello!
Based on the provided information, it could be 2 things. First of all, you may try replacing:
return response.data;
with:
console.log(this.childcounts)
and look in the console if you have the correct information logged. If not, it may be the way you send the information from Laravel.
PS: More information may be needed to solve this. When are you triggering the 'countSubcategories' method?
I would do all the intial login in the component itself, and not call a function in template like that. It can drastically affect the performance of the app, since the function would be called on change detection. But first, you are getting [object Promise], since that is exactly what you return, a Promise.
So as already mentioned, I would do the login in the component and then display a property in template. So I suggest the following:
methods: {
countSubcategories(id) {
return axios.get("..." + id);
},
getCategories(page) {
if (typeof page === "undefined") {
page = 1;
}
// or use async await pattern
axios.get("...").then(response => {
this.cats = response.data;
// gather all nested requests and perform in parallel
const reqs = this.cats.map(y => this.countSubcategories(y.id));
axios.all(reqs).then(y => {
// merge data
this.cats = this.cats.map((item, i) => {
return {...item, count: y[i].data}
})
});
});
}
}
Now you can display {{cat.count}} in template.
Here's a sample SANDBOX with similar setup.
This is happen 'cause you're trying to render a information who doesn't comeback yet...
Try to change this method inside created, make it async and don't call directly your method on HTML. Them you can render your variable this.childcounts.
How to use 2 graphql queries with react-apollo-hooks where the 2nd query depends on a parameter retrieved from the 1st query?
I try to use 2 queries which looks like this:
const [o, setO] = useState()
const { loading: loadingO, error: errorO, data: dataO } = useQuery(Q_GET_O, { onCompleted: d => setO(d.getO[0].id) });
if (loadingO) { return "error" }
const { loading: loadingOP, error: errorOP, data: dataOP } = useQuery(Q_GET_OP, { variables: { o } })
However, when I run my project, react-hooks gives me the following message:
"index.js:1437 Warning: React has detected a change in the order of Hooks called by Upgrade. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks"
I would like to know how I can use react-apollo-hooks in order to run a query that depends on another query. It works great if the graphql query variables are known in advance. However, I did not find a solution for variables that come from other query.
The problem here is that you are short circuit returning before all of your hooks have a chance to run.
React will complain if you exit a render function before all of the hooks have a chance to be called.
For example:
function BrokenFoo () {
const query = useSomeQuery();
if (query.loading) return <Loading />
// This will cause some issues because
// it's possible that we return before our useState hook gets called
const [bar, setBar] = useState();
return <SomeComponent bar={bar} setBar={setBar} data={query.data} />
}
To fix:
function FixedFoo () {
// This will be fine because
// all of the hooks have a chance to be called before a return
const query = useSomeQuery();
const [bar, setBar] = useState();
if (query.loading) return <Loading />
return <SomeComponent bar={bar} setBar={setBar} data={query.data} />
}
You can add the skip option to the second query and lose the if condition:
const { loading: loadingOP, error: errorOP, data: dataOP }
= useQuery(Q_GET_OP, { variables: { o }, skip: !o })
from the docs:
If skip is true, the query will be skipped entirely
I've followed the documentation about using graphql-tools to mock a GraphQL server, however this throws an error for custom types, such as:
Expected a value of type "JSON" but received: [object Object]
The graphql-tools documentation about mocking explicitly states that they support custom types, and even provide an example of using the GraphQLJSON custom type from the graphql-type-json project.
I've provided a demo of a solution on github which uses graphql-tools to successfully mock a GraphQL server, but this relies on monkey-patching the built schema:
// Here we Monkey-patch the schema, as otherwise it will fall back
// to the default serialize which simply returns null.
schema._typeMap.JSON._scalarConfig.serialize = () => {
return { result: 'mocking JSON monkey-patched' }
}
schema._typeMap.MyCustomScalar._scalarConfig.serialize = () => {
return mocks.MyCustomScalar()
}
Possibly I'm doing something wrong in my demo, but without the monkey-patched code above I get the error regarding custom types mentioned above.
Does anyone have a better solution than my demo, or any clues as to what I might be doing wrong, and how I can change the code so that the demo works without monkey-patching the schema?
The relevant code in the demo index.js is as follows:
/*
** As per:
** http://dev.apollodata.com/tools/graphql-tools/mocking.html
** Note that there are references on the web to graphql-tools.mockServer,
** but these seem to be out of date.
*/
const { graphql, GraphQLScalarType } = require('graphql');
const { makeExecutableSchema, addMockFunctionsToSchema } = require('graphql-tools');
const GraphQLJSON = require('graphql-type-json');
const myCustomScalarType = new GraphQLScalarType({
name: 'MyCustomScalar',
description: 'Description of my custom scalar type',
serialize(value) {
let result;
// Implement your own behavior here by setting the 'result' variable
result = value || "I am the results of myCustomScalarType.serialize";
return result;
},
parseValue(value) {
let result;
// Implement your own behavior here by setting the 'result' variable
result = value || "I am the results of myCustomScalarType.parseValue";
return result;
},
parseLiteral(ast) {
switch (ast.kind) {
// Implement your own behavior here by returning what suits your needs
// depending on ast.kind
}
}
});
const schemaString = `
scalar MyCustomScalar
scalar JSON
type Foo {
aField: MyCustomScalar
bField: JSON
cField: String
}
type Query {
foo: Foo
}
`;
const resolverFunctions = {
Query: {
foo: {
aField: () => {
return 'I am the result of resolverFunctions.Query.foo.aField'
},
bField: () => ({ result: 'of resolverFunctions.Query.foo.bField' }),
cField: () => {
return 'I am the result of resolverFunctions.Query.foo.cField'
}
},
},
};
const mocks = {
Foo: () => ({
// aField: () => mocks.MyCustomScalar(),
// bField: () => ({ result: 'of mocks.foo.bField' }),
cField: () => {
return 'I am the result of mocks.foo.cField'
}
}),
cField: () => {
return 'mocking cField'
},
MyCustomScalar: () => {
return 'mocking MyCustomScalar'
},
JSON: () => {
return { result: 'mocking JSON'}
}
}
const query = `
{
foo {
aField
bField
cField
}
}
`;
const schema = makeExecutableSchema({
typeDefs: schemaString,
resolvers: resolverFunctions
})
addMockFunctionsToSchema({
schema,
mocks
});
// Here we Monkey-patch the schema, as otherwise it will fall back
// to the default serialize which simply returns null.
schema._typeMap.JSON._scalarConfig.serialize = () => {
return { result: 'mocking JSON monkey-patched' }
}
schema._typeMap.MyCustomScalar._scalarConfig.serialize = () => {
return mocks.MyCustomScalar()
}
graphql(schema, query).then((result) => console.log('Got result', JSON.stringify(result, null, 4)));
I and a few others are seeing a similar issue with live data sources (in my case MongoDB/Mongoose). I suspect it is something internal to the graphql-tools makeExecutableSchema and the way it ingests text-based schemas with custom types.
Here's another post on the issue: How to use graphql-type-json package with GraphQl
I haven't tried the suggestion to build the schema in code, so can't confirm whether it works or not.
My current workaround is to stringify the JSON fields (in the connector) when serving them to the client (and parsing on the client side) and vice-versa. A little clunky but I'm not really using GraphQL to query and/or selectively extract the properties within the JSON object. This wouldn't be optimal for large JSON objects I suspect.
If anyone else comes here from Google results, the solution for me was to add the JSON resolver as parameter to the makeExecutableSchema call. It's described here:
https://github.com/apollographql/apollo-test-utils/issues/28#issuecomment-377794825
That made the mocking work for me.