Render Contentful Reference (many) Array on Gatsby - graphql

I am pretty new to using Contentful and their Reference (many) field type. I have one reference type that pulls in many product names. in GraphQL I can see all my product name displaying, but when I try and render it on Gatsby I am not seeing anything display (productName:array). Here is my GraphQL
{
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
and here is my code...
import React from 'react';
import { graphql, StaticQuery } from 'gatsby';
const Products = () => (
<StaticQuery
query={graphql`
query MyQuery {
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
`}
render={data => (
<div>
{data.allContentfulAppetizerMenuSection.nodes.map(({ menuItemReferences }, i) => (
<div key={i}>
{menuItemReferences.productName}
</div>
))}
</div>
)}
/>
)
export default Products;
Any help will be much appreciated.

Try:
const Products = () => {
const data = useStaticQuery(graphql`
query {
allContentfulAppetizerMenuSection {
nodes {
menuItemReferences {
productName
}
}
}
}
`);
return <div>
{data.allContentfulAppetizerMenuSection.nodes.map(({ menuItemReferences }, i) => {
return <div key={i}>
{menuItemReferences.productName}
</div>;
})}
</div>;
};
export default Products;
Note the usage of useStaticQuery hook and the return statement in your loop.
The refactor upon useStaticQuery results in a cleaner and reusable code but the idea it's exactly the same, if you are more familiar with the old StaticQuery, you can keep it.
If the error persists, try to debug inside the loop.

Related

Gatsby Contentful embedded image

As I see there is no json option anymore when querying the contentfulBlogPost only raw. I was able to make some changes to get everything from the body, except the image from that post.
If I made a test in GraphQL Playground I can get the image id and url but that's it.
query {
allContentfulAsset {
edges {
node {
id
file {
url
}
}
}
}
}
I tried to find an example how to get embedded images but no luck....
import React from 'react'
import { graphql } from 'gatsby'
import { documentToReactComponents } from '#contentful/rich-text-react-renderer'
import Layout from '../components/layout'
export const query = graphql`
query($slug: String!) {
contentfulBlogPost(slug: {eq: $slug}) {
title
publishedDate(formatString: "MMMM Do, YYYY")
body {
raw
}
}
allContentfulAsset {
edges {
node {
id
file {
url
}
}
}
}
}
`
const Blog = (props) => {
const options = {
renderNode: {
"embedded-asset-block": (node) => {
const alt = node.data.title
const url = node.file.url
return <img alt={alt} src={url}/>
}
}
}
return (
<Layout>
<h1>{props.data.contentfulBlogPost.title}</h1>
<p>{props.data.contentfulBlogPost.publishedDate}</p>
{documentToReactComponents(JSON.parse(props.data.contentfulBlogPost.body.raw, options))}
</Layout>
)
}
export default Blog
Plugins:
...
'gatsby-plugin-sharp',
{
resolve: 'gatsby-transformer-remark',
options: {
plugins: [
'gatsby-remark-relative-images',
{
resolve: 'gatsby-remark-images-contentful',
options: {
maxWidth: 750,
linkImagesToOriginal: false
}
}
]
}
}
],
}
Hi I saw this solution in a Youtube comment. First thing you have to do is change your Graphql query to something like this:
query ($slug: String!) {
contentfulBlogPost(slug: {eq: $slug}) {
id
title
publishedDate(formatString: "MMMM Do, YYYY")
body {
raw
references {
... on ContentfulAsset {
contentful_id
title
file {
url
}
}
}
}
}
}
Then change your options constant to:
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: node => {
console.log(node);
const imageID = node.data.target.sys.id;
const {
file: {url},
title
} = props.data.contentfulBlogPost.body.references.find(({contentful_id: id}) => id === imageID);
return <img src={url} alt={title} />
}
}
}
Use something like:
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/
The return statement in BLOCKS.EMBEDDED_ASSET entry will contain your data, adapt to your needs. If you go inside the dependency, you'll see all the exposed methods, so you will have also a BLOCKS.EMBEDDED_ENTRY entry for your embedded entries. Apply it like:
[BLOCKS.EMBEDDED_ENTRY]: node => {
// your logic to manipulate the entry here
return (
<>
<div>whatever</div>
</>
)
},
For anyone that is still struggling to find the "references" field in graphql, remember that you HAVE TO first create an entry in contentful by adding at least one image. Otherwise, the references field will not show up in graphql, hence you can not query it.

Apollo GraphQL pass object

In GraphQL, how do I pass an object instead of a string?
Take this code from Apollo's website as an example, with my minor change:
import React, { useState } from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [dog, setDog] = useState(null);
const [getDog, { loading, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (data && data.dog) {
setDog(data.dog);
}
const myObject = {
type: {
favors: [
tom: true,
bill: false
]
}
}
return (
<div>
{dog && <img src={dog.displayImage} />}
<button onClick={() => getDog({ variables: { theObject: myObject } })}>
Click me!
</button>
</div>
);
}
I believe React is trying to parse the object into a string, but (as the error message explains) JSON.stringify cannot serialize cyclic structures.
What do I do?

I keep getting errors when getting image with gatsby-image and graphql

I am trying to add an image sourced from graphql. I've run into this problem a few times now and always end up lucking out into a fix.
When querying the image I get this response:
The "path" argument must be one of type string, Buffer, or URL.
Received type undefined
The code is as follows:
import React from 'react'
import { Link, graphql, useStaticQuery, StaticQuery } from 'gatsby'
import Img from 'gatsby-image'
import './longCard.css';
const CardData = props => {
const slug = props.slug;
return (
<StaticQuery
query={
graphql`
query($slug: String) {
sanityProduct(slug: {current: {eq: $slug}}) {
slug{
current
}
title
featured_image {
asset {
childImageSharp {
fixed {
...GatsbyImageSharpFixed
}
}
}
}
}
}
`}
render={data => <LongCard />}
/>
)
}
export default CardData
export const LongCard = ({ data }) => {
return (
<div className="long-card">
<div className="long-card-inner">
<Link to={data.sanityProduct.slug.current}>{data.sanityProduct.title}</Link>
{/* Add image */}
<Img fixed={data.featured_image.asset.childImageSharp.fixed} />
</div>
</div>
)
}
I didn't need the ChildImageSharp section, I think this is only for querying the file-system.

apollo-client: How to get inverse relation from cache?

I have a graphql query response of the shape
{
table {
id
legs {
id
}
}
This normalizes to table and leg entries in my InMemoryCache.
But then if my application retrieves a leg from cache and needs to know the corresponding table, how would I find this?
The two ideas I had are
adding a table prop to each leg when the query response comes in - not sure if/how that would work (I have multiple queries and mutations containing the graphql fragment with above shape)
having a suitable cache redirect, but I don't know how to do this without searching all tables for the leg.
Does apollo provide any features suitable to achieve this inverse lookup?
Update: to clarify, the leg has a table prop, but I since I already have the info in the client before resolving that prop, I'd like to resolve that prop client-side instead of server-side.
You should be adding a table prop to each leg. According to the graphql.org documentation you should be thinking in graphs:
With GraphQL, you model your business domain as a graph by defining a schema; within your schema, you define different types of nodes and how they connect/relate to one another.
In your model, tables and legs are nodes in your business model graph. When you add a table prop to each leg you are creating a new edge in this graph that your client-side code can traverse to get the relevant data.
Edit after clarification:
You can use writeFragment and to gain fine grained control of the Apollo cache. Once the cache filling query is done, compute the inverse relationship and write it to the cache like so:
fetchTables = async () => {
const client = this.props.client
const result = await client.query({
query: ALL_TABLES_QUERY,
variables: {}
})
// compute the reverse link
const tablesByLeg = {}
for (const table of result.data.table) {
for (const leg of table.legs) {
if (!tablesByLeg[leg.id]) {
tablesByLeg[leg.id] = {
leg: leg,
tables: []
}
}
tablesByLeg[leg.id].tables.push(table)
}
}
// write to the Apollo cache
for (const { leg, tables } of Object.values(tablesByLeg)) {
client.writeFragment({
id: dataIdFromObject(leg),
fragment: gql`
fragment reverseLink from Leg {
id
tables {
id
}
}
`,
data: {
...leg,
tables
}
})
}
// update component state
this.setState(state => ({
...state,
tables: Object.values(result)
}))
}
Demo
I put up a complete exemple here: https://codesandbox.io/s/6vx0m346z
I also put it below just for completeness sake.
index.js
import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from "react-apollo";
import { createClient } from "./client";
import { Films } from "./Films";
const client = createClient();
function App() {
return (
<ApolloProvider client={client}>
<Films />
</ApolloProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
client.js
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
export function dataIdFromObject(object) {
return object.id ? object.__typename + ":" + object.id : null;
}
export function createClient() {
return new ApolloClient({
connectToDevTools: true,
ssrMode: false,
link: new HttpLink({
uri: "https://prevostc-swapi-graphql.herokuapp.com"
}),
cache: new InMemoryCache({
dataIdFromObject,
cacheRedirects: {
Query: {
planet: (_, args, { getCacheKey }) =>
getCacheKey({ __typename: "Planet", id: args.id })
}
}
})
});
}
Films.js
import React from "react";
import gql from "graphql-tag";
import { withApollo } from "react-apollo";
import { dataIdFromObject } from "../src/client";
import { Planet } from "./Planet";
const ALL_FILMS_QUERY = gql`
query {
allFilms {
films {
id
title
planetConnection {
planets {
id
name
}
}
}
}
}
`;
const REVERSE_LINK_FRAGMENT = gql`
fragment reverseLink on Planet {
id
name
filmConnection {
films {
id
title
}
}
}
`;
class FilmsComponent extends React.Component {
constructor() {
super();
this.state = { films: [], selectedPlanetId: null };
}
componentDidMount() {
this.fetchFilms();
}
fetchFilms = async () => {
const result = await this.props.client.query({
query: ALL_FILMS_QUERY,
variables: {}
});
// compute the reverse link
const filmByPlanet = {};
for (const film of result.data.allFilms.films) {
for (const planet of film.planetConnection.planets) {
if (!filmByPlanet[planet.id]) {
filmByPlanet[planet.id] = {
planet: planet,
films: []
};
}
filmByPlanet[planet.id].films.push(film);
}
}
// write to the apollo cache
for (const { planet, films } of Object.values(filmByPlanet)) {
this.props.client.writeFragment({
id: dataIdFromObject(planet),
fragment: REVERSE_LINK_FRAGMENT,
data: {
...planet,
filmConnection: {
films,
__typename: "PlanetsFilmsConnection"
}
}
});
}
// update component state at last
this.setState(state => ({
...state,
films: Object.values(result.data.allFilms.films)
}));
};
render() {
return (
<div>
{this.state.selectedPlanetId && (
<div>
<h1>Planet query result</h1>
<Planet id={this.state.selectedPlanetId} />
</div>
)}
<h1>All films</h1>
{this.state.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li>
planets:
{f.planetConnection.planets.map(p => {
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
<li>
<button
onClick={() =>
this.setState(state => ({
...state,
selectedPlanetId: p.id
}))
}
>
select
</button>
</li>
<li> </li>
</ul>
);
})}
</li>
</ul>
);
})}
<h1>The current cache is:</h1>
<pre>{JSON.stringify(this.props.client.extract(), null, 2)}</pre>
</div>
);
}
}
export const Films = withApollo(FilmsComponent);
Planet.js
import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";
const PLANET_QUERY = gql`
query ($id: ID!) {
planet(id: $id) {
id
name
filmConnection {
films {
id
title
}
}
}
}
`;
export function Planet({ id }) {
return (
<Query query={PLANET_QUERY} variables={{ id }}>
{({ loading, error, data }) => {
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
const p = data.planet;
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
{p.filmConnection.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li> </li>
</ul>
);
})}
</ul>
);
}}
</Query>
);
}

relay refetch doesn't show the result

I'm trying to create a live search-result component(lazy load one). It works perfectly for the first time but refetch doesn't update the data. I see the request and respoonse in Network tab! so it does get the data, but it doesn't supply it to the component!
any idea why?
import React, { Component } from 'react';
import {
createRefetchContainer,
graphql,
} from 'react-relay';
import ProfileShow from './ProfileShow';
class ProfileList extends Component {
render() {
console.log("rendering....", this.props)
return (
<div className="row">
<input type="text" onClick={this._loadMe.bind(this)} />
{this.props.persons.map((person) => {
return (
<div className="col-md-3">
<ProfileShow person={person} />
</div>
);
})}
</div>
);
}
_loadMe(e) {
const refetchVariables = fragmentVariables => ({
queryStr: e.target.value,
});
this.props.relay.refetch(refetchVariables, null, (...data) => {
console.log(data)
});
}
}
const FragmentContainer = createRefetchContainer(
ProfileList,
{
persons: graphql.experimental`
fragment ProfileList_persons on Person #relay(plural: true) {
fullname
number
email
pic
}
`
},
graphql.experimental`
query ProfileListRefetchQuery($queryStr: String!) {
talentList(query: $queryStr) {
...ProfileList_persons
}
}
`,
);
export default FragmentContainer;

Resources