I'm using Reactjs and Relay. I want to create something if "this" item is not created yet / array is empty. somehow, the relay query returns an empty array if the data has not been loaded, therefore it keeps created "this item" since it's considered empty. how do we determine if relay query data is actually empty or not loaded / pending?
i.e query:
QueryRenderer(MyComponent, {
query: graphql`
query MyComponentQuery($id: ID!) {
items(containerId: $id) {
id
title
}
}
`,
vars: ({ containerId }) => ({
id: containerId
})
})
handle create "this" item:
useEffect(() => {
if(!props.items){
// Create "this" item
}
}, [props.items]);
I am guessing from your snippet that QueryRenderer is an HOC you are using wrapping relay's QueryRender component?
If so, this is how you normally determine if a query is in loading state:
// Example.js
import React from 'react';
import { QueryRenderer, graphql } from 'react-relay';
const renderQuery = ({error, props}) => {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return <div>{props.page.name} is great!</div>;
}
return <div>Loading</div>;
}
const Example = (props) => {
return (
<QueryRenderer
environment={environment}
query={graphql`
query ExampleQuery($pageID: ID!) {
page(id: $pageID) {
name
}
}
`}
variables={{
pageID: '110798995619330',
}}
render={renderQuery}
/>
);
}
This snippet is taken from the relay.dev docs.
Notice how the render renderProp is getting the props field once the query is no longer loading.
If you are using the new hooks-api, it will be different based on which of the query-hooks you are using.
Related
I'm trying to log out the raw and so far I can't get anything in the console. It just shows up as an empty array. There must be something wrong with my Query as all i can find on the docs is how to render ContentfulAsset but not actual text which i can format with CSS
/* eslint-disable */
import React from 'react';
import { Helmet } from 'react-helmet';
import { graphql } from 'gatsby';
import Layout from '../components/layout';
import { documentToReactComponents } from '#contentful/rich-text-react-renderer';
export const query = graphql`
query MyQuery($slug: String) {
contentfulLongPost(Slug: { eq: $slug }) {
title
updatedAt(formatString: "MMMM Do, YYYY")
body {
raw
references {
... on ContentfulAsset {
contentful_id
__typename
}
}
}
free
contentType
}
}
`;
const caseStudy = (props) => {
console.log(props);
return (
<Layout>
</Layout>
);
};
export default caseStudy;
What do I put after __typename to get the raw?
query MyQuery($slug: String) {
contentfulLongPost(Slug: {eq: $slug}) {
title
updatedAt(formatString: "MMMM Do, YYYY")
body {
raw
}
}
}
`;
There are a few things wrong there... caseStudy must be CaseStudy since it's a React component, otherwise, it will be interpreted as an HTML element which obviously will break your code (<caseStudy> doesn't exist... yet).
Even if ContentfulAsset fragment is wrong, if your query is correct, you should get something inside the raw field (located at props.data.contentfulLongPost.raw) so check it again. If you are just trying to print the rich text, while your query doesn't break, you can print what's inside raw without data in the ContentfulAsset fragment.
Maybe what it's wrong if the filter that is hold by the $slug variable, so even if the query is correct, you are not able to fetch the data because there's not any data to fetch.
Once you ensure that your data is being fetched properly (you have data inside props.data) you can customize the output by lifting your data to:
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{children}</p>
const options = {
renderMark: {
[MARKS.BOLD]: text => <Bold>{text}</Bold>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
[BLOCKS.EMBEDDED_ASSET]: node => {
return (
<>
<h2>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
},
}
renderRichText(node.bodyRichText, options)
Source: https://www.contentful.com/developers/docs/tutorials/general/rich-text-and-gatsby/
A full customizable example can be checked (almost copy/pasted) at: https://github.com/contentful/rich-text/tree/master/packages/rich-text-react-renderer
The idea is to create a custom object that will parse your fetched data with a custom HTML/React component:
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{children}</p>
const options = {
renderMark: {
[MARKS.BOLD]: text => <Bold>{text}</Bold>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
[BLOCKS.EMBEDDED_ASSET]: node => {
return (
<>
<h2>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
},
}
function BlogPostTemplate({ data }) {
return <div>{data.contentfulBlogPost && renderRichText(data.contentfulBlogPost, options)}</div>
}
Where BlogPostTemplate stands for your CaseStudy and contentfulBlogPost for your contentfulLongPost.
Summarizing:
Fix your component naming
Check what's inside props.data.contentfulLongPost.raw
Check that you have any data with the provided slug. You can force the value of the slug in localhost:8000/___graphql to check what data is fetching your query.
Customize the output using the options object and the renderRichText helper
I'm trying to generate Gatsby pages based on data in my CMS (Sanity).
I have created three pages in my CMS under the umbrella term "Second page", I use createPage in gatsby-node to generetate pages with the correct slugs.
All the pages are generated according to their slug from the CMS, but in my template component I cannot filter out data. I get the result for all three pages when I only need the result for the one page that matches the slug. My console.log in secondPage.js shows three arrays corresponding to the three items in my CMS
gatsby-node.js
// Create pages for docs
exports.createPages = ({ actions, graphql }) => {
const path = require(`path`);
const { createPage } = actions;
//const docTemplate = path.resolve("src/templates/docTemplate.js");
const secondPageTemplate = path.resolve("src/templates/secondPage.js");
return graphql(`
{
allSanitySecondPage {
edges {
node {
slug
}
}
}
}
`).then((result) => {
if (result.errors) {
Promise.reject(result.errors);
}
result.data.allSanitySecondPage.edges.forEach(({ node }) => {
createPage({
path: node.slug,
component: secondPageTemplate,
context: {
slug: node.slug,
},
});
});
});
};
secondPage.js (template)
import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";
const BlockContent = require("#sanity/block-content-to-react");
const secondPage = ({ data }) => {
// const pageData = data.sanitySecondPage.edges.node;
return (
<Layout>
<h1>Hello from the second page!</h1>
{console.log(data.sanitySecondPage)}
{/* <BlockContent blocks={pageData._rawBlockContent} /> */}
</Layout>
);
};
export const query = graphql`
query($slug: String!) {
sanitySecondPage(slug: { eq: $slug }) {
_rawBlockContent
}
}
`;
export default secondPage;
It turns out that all I needed was to write a stack overflow post to solve my own issue. Everything was correct, I miss understood the return of block content, it was supposed to be three array elements.
Solved!
I'm trying to combine two dependent GraphQL queries.
The first one should get an ID and the second one should take that ID. I read that compose behaves like flowRight(), but no matter in what order I put the queries, if queryId is below queryDetails, queryDetail's is always skipped (as expected). No matter how I put my code together the variable is undefined.
import { graphql, compose } from 'react-apollo'
import gql from 'graphql-tag'
class Home extends Component {
constructor(props) {
super(props)
console.log("Where's my data?")
console.log(props)
}
render() {
return(
<div />
)
}
}
export const queryIdConst = gql`
query IdQuery {
account(name:"SuperAccount")
{
lists {
edges {
id
}
}
}
}
`
export const queryDataConst = gql`
query DataQuery($id: ID!) {
account(name:"SuperAccount")
{
list(id: $id) {
displayTitle
}
}
}
`
export default compose(
graphql(queryIdConst, {
name: 'listId',
}),
graphql(queryDataConst, {
name: 'data',
skip: ({ listId }) => !listId.data,
options: ({ listId }) => ({
variables: {
id: list.data.account.lists.edges[0].id
}
})
})
)(Home)
I have already tried to change the compose functions order, but anyway this is not working, as I expected it to work.
Thanks for any help!
Edit: Switched the two graphql() in compose() to be inline with AbsoluteSith's comment link
Solution
With hints and help from Daniel Rearden and AbsoluteSith I implemented the following solution:
Changed the compose():
export default compose(
graphql(queryIdConst, {
name: 'listId',
}),
graphql(queryDataConst, {
name: 'dataHome', // changed to 'dataHome' to avoid confusion
skip: ({ listId }) => !listId.account,
options: ({ listId }) => ({
variables: {
id: listId.account.lists.edges[0].id
}
})
})
)(Home)
And my render():
return(
<div>
{ dataHome && !dataHome.loading &&
<div>{dataHome.account.list.displayTitle}</div>
}
</div>
)
When using the graphql HOC, by default, the wrapped component receives a prop called data (or mutate if passing in a mutation). Given a query like
query IdQuery {
account(name:"SuperAccount") {
lists {
edges {
id
}
}
}
}
once the query loads, the query result is available under this.props.data.account. When you use the name configuration option, you're telling the HOC to use something other than data for the prop name. So if you set name to listId, then your query result will be available at
this.props.listId.account
That means the second HOC inside of compose should look more like this:
graphql(queryDataConst, {
skip: ({ listId }) => !listId.account, // <--
options: ({ listId }) => ({
variables: {
id: listId.account.lists.edges[0].id // <--
}
})
})
As shown below I'm getting my data in my nextJS application in the pages/article.js using a graphQL query.
This data is passed down to another react component, which gives me a list of checkboxes.
Selecting a checkbox is calling a mutation to store the ID of the selected checkboxes in the DB.
To get the content updated, I'm using refetchQueries to call the main query again, which will pass the data down to the current component.
So far everything is working. Now I would like to get this stuff realtime using optimistic UI - which makes me some problems...
Replacing the refetchQueries with
update: (store, { data: { getArticle } }) => {
const data = store.readQuery({
query: getArticle,
variables: {
id: mainID
}
})
console.log(data)
}
runs me to the error TypeError: Cannot read property 'kind' of undefined which comes from readQuery.
I don't see what I'm doing wrong. And this is just the first part to get optimisic UI..
pages/article.js
import Article from '../components/Article'
class ArticlePage extends Component {
static async getInitialProps (context, apolloClient) {
const { query: { id }, req } = context
const initProps = { }
// ...
return { id, ...initProps }
}
render () {
const { id, data } = this.props
const { list } = data
return (
<Article
mainID={id}
list={list}
/>
)
}
}
export default compose(
withData,
graphql(getArticle, {
options: props => ({
variables: {
id: props.id
}
})
})
)(ExtendedArticlePage)
components/Article.js
import { getArticle } from '../graphql/article'
import { selectMutation } from '../graphql/selection'
export class Article extends Component {
checkboxToggle (id) {
const { mainID, checkboxSelect } = this.props
checkboxSelect({
variables: {
id
},
refetchQueries: [{
query: getArticle,
variables: {
id: mainID
}
}],
})
}
render () {
const { list } = this.props
return (
list.map(l => {
return (<Checkbox onClick={this.checkboxToggle.bind(this, l.id)} label={l.content} />)
}
)
}
}
export default compose(
graphql(selectMutation, { name: 'checkboxSelect' })
)(Article)
You have a variable shadowing issue in your update code, it seems that you're using the same name getArticle for both your query and the mutation result nested in data.
This is why your call to readQuery fails, the query params you need to provide resolves to the mutation result and not the actual query, hence the TypeError: Cannot read property 'kind' of undefined.
You just need to name your query with another identifier like getQueryArticle.
We are building an offline first React Native Application with Apollo Client. Currently I am trying to update the Apollo Cache directly when offline to update the UI optimistically. Since we offline we do not attempt to fire the mutation until connect is "Online" but would like the UI to reflect these changes prior to the mutation being fired while still offline. We are using the readQuery / writeQuery API functions from http://dev.apollodata.com/core/read-and-write.html#writequery-and-writefragment. and are able to view the cache being updated via Reacotron, however, the UI does not update with the result of this cache update.
const newItemQuantity = existingItemQty + 1;
const data = this.props.client.readQuery({ query: getCart, variables: { referenceNumber: this.props.activeCartId } });
data.cart.items[itemIndex].quantity = newItemQuantity;
this.props.client.writeQuery({ query: getCart, data });
If you look at the documentation examples, you will see that they use the data in an immutable way. The data attribute passed to the write query is not the same object as the one that is read. Mutating this object is unlikely to be supported by Apollo because it would not be very efficient for it to detect which attributes you modified, without doing deep copies and comparisons of data before/after.
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
const data = client.readQuery({ query });
const myNewTodo = {
id: '6',
text: 'Start using Apollo Client.',
completed: false,
};
client.writeQuery({
query,
data: {
todos: [...data.todos, myNewTodo],
},
});
So you should try the same code without mutating the data. You can use for example set of lodash/fp to help you
const data = client.readQuery({...});
const newData = set("cart.items["+itemIndex+"].quantity",newItemQuantity,data);
this.props.client.writeQuery({ ..., data: newData });
It recommend ImmerJS for more complex mutations
Just to save someones time. Using the data in an immutable way was the solution. Agree totally with this answer, but for me I did something else wrong and will show it here. I followed this tutorial and updating the cache worked fine as I finished the tutorial. So I tried to apply the knowledge in my own app, but there the update didn’t work even I did everything similar as showed in the tutorial.
Here was my approach to update the data using the state to access it in the render method:
// ... imports
export const GET_POSTS = gql`
query getPosts {
posts {
id
title
}
}
`
class PostList extends Component {
constructor(props) {
super(props)
this.state = {
posts: props.posts
}
}
render() {
const postItems = this.state.posts.map(item => <PostItem key={item.id} post={item} />)
return (
<div className="post-list">
{postItems}
</div>
)
}
}
const PostListQuery = () => {
return (
<Query query={GET_POSTS}>
{({ loading, error, data }) => {
if (loading) {
return (<div>Loading...</div>)
}
if (error) {
console.error(error)
}
return (<PostList posts={data.posts} />)
}}
</Query>
)
}
export default PostListQuery
The solution was just to access the date directly and not using the state at all. See here:
class PostList extends Component {
render() {
// use posts directly here in render to make `cache.writeQuery` work. Don't set it via state
const { posts } = this.props
const postItems = posts.map(item => <PostItem key={item.id} post={item} />)
return (
<div className="post-list">
{postItems}
</div>
)
}
}
Just for completeness here is the input I used to add a new post and update the cache:
import React, { useState, useRef } from 'react'
import gql from 'graphql-tag'
import { Mutation } from 'react-apollo'
import { GET_POSTS } from './PostList'
const ADD_POST = gql`
mutation ($post: String!) {
insert_posts(objects:{title: $post}) {
affected_rows
returning {
id
title
}
}
}
`
const PostInput = () => {
const input = useRef(null)
const [postInput, setPostInput] = useState('')
const updateCache = (cache, {data}) => {
// Fetch the posts from the cache
const existingPosts = cache.readQuery({
query: GET_POSTS
})
// Add the new post to the cache
const newPost = data.insert_posts.returning[0]
// Use writeQuery to update the cache and update ui
cache.writeQuery({
query: GET_POSTS,
data: {
posts: [
newPost, ...existingPosts.posts
]
}
})
}
const resetInput = () => {
setPostInput('')
input.current.focus()
}
return (
<Mutation mutation={ADD_POST} update={updateCache} onCompleted={resetInput}>
{(addPost, { loading, data }) => {
return (
<form onSubmit={(e) => {
e.preventDefault()
addPost({variables: { post: postInput }})
}}>
<input
value={postInput}
placeholder="Enter a new post"
disabled={loading}
ref={input}
onChange={e => (setPostInput(e.target.value))}
/>
</form>
)
}}
</Mutation>
)
}
export default PostInput