using renderRichText - receiving an error Node is not defined - reactjs

I am having an error "TypeError: Cannot read property 'map' of undefined" when I tried to use documentToReactComponents to output the body of contentful post. If I comment it out, and try to console log, it displays documents etc.
Having edited the code to the suggested answer, I am receiving another error "Node is not defined" using renderRichText
import React from 'react'
import Layout from '../components/layout'
import { graphql } from 'gatsby'
import Head from '../components/head'
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
export const query = graphql`
query($slug: String!) {
contentfulBlogPost(
slug: {
eq: $slug
}
) {
title
slug
publishedDate(formatString: "MMM Do, YYYY")
body {
raw
}
}
}
`
const Blog = (props) => {
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 => {
const alt= node.data.target.fields.title['en-US']
const url= node.data.target.fields.file['en-US'].url
return (
<>
<img src={url} alt={alt} />
</>
)
},
[BLOCKS.EMBEDDED_ENTRY]: node => {
return (
<Layout>
<Head home_title="Blog Post" />
<h1>{props.data.contentfulBlogPost.title}</h1>
<p>{props.data.contentfulBlogPost.publishedDate}</p>
{props.data.contentfulBlogPost.body}
</Layout>
)
},
},
}
renderRichText(node.bodyRichText, options)
}
export default Blog
I have tried to read through from Github but still receiving the same error. I need help.

All you need to do is to add references to the graphql query, unlike with json, the body has raw and references. With that being said, you need to append this to the body after the raw;
references{
contentful_id
title
fixed{
src
}
}
Then you can you id to search for the title and src as you can see above.
import React from 'react'
import Layout from '../components/layout'
import { graphql } from 'gatsby'
import Head from '../components/head'
import { documentToReactComponents } from '#contentful/rich-text-react-renderer'
import { BLOCKS } from "#contentful/rich-text-types"
export const query = graphql`
query($slug: String!){
post: contentfulBlogPost(slug:{eq:$slug}){
title
publishedDate(formatString: "MMMM Do, YYYY")
body{
raw
references{
contentful_id
title
fixed{
src
}
}
}
}
}
`
const Blog = (props) => {
const assets = new Map(props.data.post.body.references.map(ref => [ref.contentful_id,ref]))
const options = {
renderNode:{
[BLOCKS.EMBEDDED_ASSET]: node => {
const url = assets.get(node.data.target.sys.id).fixed.src
const alt = assets.get(node.data.target.sys.id).title
return <img alt={alt} src={url}/>
}
}
}
return(
<Layout>
<h1>{props.data.post.title}</h1>
<p>{props.data.post.publishedDate}</p>
{documentToReactComponents(JSON.parse(props.data.post.body.raw),options)}
</Layout>
)
}
export default Blog

I would suggest using the following approach:
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)
Note: extracted from Contentful docs
Using renderRichText built-in method (from gatsby-source-contentful/rich-text). Your code seems a little bit deprecated or at least, some methods.
The following code will never work:
"embedded-asset-block" : (node) => {
const alt= node.data.target.fields.title['en-US']
const url= node.data.target.fields.file['en-US'].url
return <imag src={url} alt={alt} />
}
You need to use the providers of the dependency (#contentful/rich-text-types), that are BLOCKS and MARKS. You can always access to the dependency to check the methods, however, embedded-asset-block stands for BLOCKS.EMBEDDED_ASSET or BLOCKS.EMBEDDED_ENTRY, so:
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 => {
// manipulate here your embedded asset
return (
<>
<h2>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
[BLOCKS.EMBEDDED_ENTRY]: node => {
// manipulate here your embedded entry
return (
<>
<div>I'm an embedded entry</div>
</>
)
},
},
}
​
renderRichText(node.bodyRichText, options)

Related

Rendering Rich Text from CMS in Gatsby v4

I'm trying to render rich text in my Gatsby v4 about page for my site, but I'm having trouble finding any info on how to render the data. I've read about adding blocks, but I'm lost on what I should be including or how to go about this. I really just need to render links, headers, and body text in the rich text. Could someone walk me through this?
Here's my component code snippet. The data is all coming through the query correctly in the page so far. I just want the text to go where it says "TEXT GOES HERE"
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import { GatsbyImage } from "gatsby-plugin-image"
import {renderRichText} from "gatsby-source-contentful/rich-text"
import {BLOCKS, MARKS} from "#contentful/rich-text-types"
import * as aboutStyles from "../styles/about.module.scss"
const query = graphql`
{
contentfulAbout {
about
bioImage {
title
url
gatsbyImageData(
layout: FULL_WIDTH
placeholder: BLURRED
resizingBehavior: SCALE
width: 1000
)
}
aboutText {
raw
}
}
}
`
const AboutSection = () => {
const data = useStaticQuery(query);
const contentfulAbout = data.contentfulAbout
return (
<div className={aboutStyles.parent}>
<section className={aboutStyles.container}>
<div className={aboutStyles.image}>
<GatsbyImage className={aboutStyles.bioImage} image={contentfulAbout.bioImage.gatsbyImageData} alt={contentfulAbout.bioImage.title} />
</div>
<div className={aboutStyles.text}>
<h2>{contentfulAbout.about}</h2>
<p>TEXT GOES HERE</p>
</div>
</section>
</div>
)
}
The idea is to use the exposed BLOCKS and MARKS to fully customize the fetched data from Contentful 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>,
},
},
}
​
renderRichText(node.bodyRichText, options)
Source: https://www.contentful.com/developers/docs/tutorials/general/rich-text-and-gatsby/
In that way, you can render a span with "bold" className when a MARKS.BOLD is fetched, using your own customized output.
In the snippet above, there's missing the implementation into a "standard" component, but the idea relies on the same fact. Using renderRichText what accepts two arguments:
The first one: your rich text node (aboutText in your case)
The second argument: the options with your custom output
Applied to your code, it should look like:
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import { GatsbyImage } from "gatsby-plugin-image"
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
import * as aboutStyles from "../styles/about.module.scss"
const options = {
renderMark: {
[MARKS.BOLD]: text => <strong>{text}</strong>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <p>{children}</p>,
},
}
const query = graphql`
{
contentfulAbout {
about
bioImage {
title
url
gatsbyImageData(
layout: FULL_WIDTH
placeholder: BLURRED
resizingBehavior: SCALE
width: 1000
)
}
aboutText {
raw
}
}
}
`
const AboutSection = () => {
const data = useStaticQuery(query);
const contentfulAbout = data.contentfulAbout
return (
<div className={aboutStyles.parent}>
<section className={aboutStyles.container}>
<div className={aboutStyles.image}>
<GatsbyImage className={aboutStyles.bioImage} image={contentfulAbout.bioImage.gatsbyImageData} alt={contentfulAbout.bioImage.title} />
</div>
<div className={aboutStyles.text}>
<h2>{contentfulAbout.about}</h2>
<p>{renderRichText(contentfulAbout.aboutText, options)}</p>
</div>
</section>
</div>
)
}
Of course, it may need some tweaking. Note that I've simplified the output so customize it as you wish/need.
Other resources:
https://www.contentful.com/developers/docs/concepts/rich-text/

Gatsby StaticImage from array

I am trying to make an image slider with React and Gatsby but the images aren't rendering and I get this in console
static-image.server.tsx:51 No data found for image "undefined"
Could not find values for the following props at build time: src
This is my code. When I change the image source to '../images/1.png' instead of images[sliderIndex] the image will show.
const ImageSlider = (props) => {
const [sliderIndex, setSliderIndex] = useState(0);
const images = ['../images/1.png', '../images/2.png']
useEffect(() => {
const sliderLoop = setInterval(() => {
if(sliderIndex+1 > images.length-1) {
setSliderIndex(0)
} else {
setSliderIndex(sliderIndex+1)
}
}, 5000)
return () => clearInterval(sliderLoop)
}, [sliderIndex])
console.log(sliderIndex)
return (
<>
{props.children}
<StaticImage src={images[sliderIndex]} alt=""/>
</>
)
}
<StaticImage /> would only works with a static string, see this part of the docs for more info.
You'd have to use the <GatsbyImage /> component instead & query image data manually, as instructed here. I copied the relevant sample code below.
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
function BlogPost({ data }) {
const image = getImage(data.blogPost.avatar)
return (
<section>
<h2>{data.blogPost.title}</h2>
<GatsbyImage image={image} alt={data.blogPost.author} />
<p>{data.blogPost.body}</p>
</section>
)
}
export const pageQuery = graphql`
query {
blogPost(id: { eq: $Id }) {
title
body
author
avatar {
childImageSharp {
gatsbyImageData(
width: 200
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
`

Displaying all blog posts with Gatsby Contentful

Im trying to display all my Contentful blog posts to my index page in Gatsby but i get an error.
im creating the Posts pages on gatsby-node.js like this:
const path = require(`path`)
// Log out information after a build is done
exports.onPostBuild = ({ reporter }) => {
reporter.info(`Your Gatsby site has been built!`)
}
// Create blog pages dynamically
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const blogPostTemplate = path.resolve(`src/templates/blogPost.js`)
const result = await graphql(`
query {
allContentfulPost {
edges {
node {
postTitle
slug
}
}
}
}
`)
result.data.allContentfulPost.edges.forEach(edge => {
createPage({
path: `${edge.node.slug}`,
component: blogPostTemplate,
context: {
title: edge.node.postTitle,
slug: edge.node.slug,
},
})
})
}
based on this template:
import React from "react"
import { graphql } from "gatsby"
import styled from "styled-components"
export const pageQuery = graphql`
query($slug: String!) {
post: contentfulPost(slug: { eq: $slug }) {
slug
postTitle
postContent {
childMarkdownRemark {
html
}
}
postImage {
title
fluid {
src
}
}
}
}
`
function blogPost({ data }) {
return (
<div>
<img
src={data.post.postImage.fluid.src}
alt={data.post.postImage.title}
></img>
<h1>{data.post.postTitle}</h1>
<h3
dangerouslySetInnerHTML={{
__html: data.post.postContent.childMarkdownRemark.html,
}}
/>
</div>
)
}
export default blogPost
Now i try to create a component which will hold all the blog posts so i can display it on my index.js page, like this:
import { Link, graphql, StaticQuery } from "gatsby"
import React from "react"
import styled from "styled-components"
function BlogSection() {
return (
<StaticQuery
query={graphql`
query blogQuery {
allContentfulPost {
edges {
node {
slug
postTitle
postImage {
file {
url
fileName
}
}
postContent {
postContent
}
postDate
}
}
}
}
`}
render={data => (
<ul>
<Link to={data.allContentfulPost.edges.node.slug}> //here's where the error happens
{data.allContentfulPost.edges.node.postTitle}
</Link>
</ul>
)}
/>
)
}
export default BlogSection
But i get an error Cannot read property 'slug' of undefined which is driving me crazy for days.
any help would be appreciated!
Use:
<ul>
{data.allContentfulPost.edges.map(({ node }) => {
return <Link to={node.slug} key={node.slug}>
{node.postTitle}
</Link>
})}
</ul>
You are querying all pots from Contentful (allContentfulPost) which following the nested structure, has an edges and a node inside: this last one has all the information of your posts (because of the nested structure, you have the slug, the postTitle, etc) so the node, is indeed your post. That said, you only need to loop through edges, which is an array of your posts. In the previous snippet:
data.allContentfulPost.edges.map(({ node })
You are destructuring the iterable variable at the same time you loop through it ({ node }). You can alias it for a more succint approach like:
<ul>
{data.allContentfulPost.edges.map(({ node: post }) => {
return <Link to={post.slug} key={post.slug}>
{post.postTitle}
</Link>
})}
</ul>
It's important to use the key attribute in all loops since it will help React to know what elements are changing.

How to create infinite scroll in React and Redux?

import React, {useState, useEffect} from 'react';
import {connect} from 'react-redux';
import {
fetchRecipes
} from '../../store/actions';
import './BeerRecipes.css';
const BeerRecipes = ({recipesData, fetchRecipes}) => {
const [page, setPage] = useState(1);
const [recipes, setRecipes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRecipes();
}, [])
return (
<div className='beer_recipes_block'>
<div className='title_wrapper'>
<h2 className='title'>Beer recipes</h2>
</div>
<div className='beer_recipes'>
<ul className='beer_recipes_items'>
{
recipesData && recipesData.recipes && recipesData.recipes.map(recipe =>
<li className='beer_recipes_item' id={recipe.id}>{recipe.name}</li>
)
}
</ul>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
recipesData: state.recipes
}
}
const mapDispatchToProps = dispatch => {
return {
fetchRecipes: () => dispatch(fetchRecipes())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerRecipes);
this is my component where I would like to create infinite scroll and below is my redux-action with axios:
import axios from "axios";
import * as actionTypes from "./actionTypes";
export const fetchRecipesRequest = () => {
return {
type: actionTypes.FETCH_RECIPES_REQUEST
}
}
export const fetchRecipesSuccess = recipes => {
return {
type: actionTypes.FETCH_RECIPES_SUCCESS,
payload: recipes
}
}
export const fetchRecipesFailure = error => {
return {
type: actionTypes.FETCH_RECIPES_FAILURE,
payload: error
}
}
export const fetchRecipes = (page) => {
return (dispatch) => {
dispatch(fetchRecipesRequest)
axios
.get('https://api.punkapi.com/v2/beers?page=1')
.then(response => {
const recipes = response.data;
dispatch(fetchRecipesSuccess(recipes));
})
.catch(error => {
const errorMsg = error.message;
dispatch(fetchRecipesFailure(errorMsg));
})
}
}
I want to create a scroll. I need, firstly, to display first 10 elements and then to add 5 elements with every loading. I have 25 elements altogether and when the list is done it should start from the first five again.
Assuming you already have everything ready to load your next page. You can probably simplify the entire process by using a package like react-in-viewport so you don't have to deal with all the scroll listeners.
then you use it like this way.
import handleViewport from 'react-in-viewport';
const Block = (props: { inViewport: boolean }) => {
const { inViewport, forwardedRef } = props;
const color = inViewport ? '#217ac0' : '#ff9800';
const text = inViewport ? 'In viewport' : 'Not in viewport';
return (
<div className="viewport-block" ref={forwardedRef}>
<h3>{ text }</h3>
<div style={{ width: '400px', height: '300px', background: color }} />
</div>
);
};
const ViewportBlock = handleViewport(Block, /** options: {}, config: {} **/);
const Component = (props) => (
<div>
<div style={{ height: '100vh' }}>
<h2>Scroll down to make component in viewport</h2>
</div>
<ViewportBlock
onEnterViewport={() => console.log('This is the bottom of the content, lets dispatch to load more post ')}
onLeaveViewport={() => console.log('We can choose not to use this.')} />
</div>
))
What happen here is, it creates a 'div' which is outside the viewport, once it comes into the view port ( it means user already scrolled to the bottom ), you can call a function to load more post.
To Note: Remember to add some kind of throttle to your fetch function.

React-Select with React-Apollo does not work

We are using react-select and fetching the items as the user types. I am not able to make it work with react-apollo.
Can someone help me provide a guideline?
Here is my unsuccessful attempt:
class PatientSearchByPhone extends Component {
updateProp = mobile => {
if (mobile.length < 10) return;
this.props.data.refetch({ input: { mobile } });
};
render() {
console.log(this.props.data);
return <AsyncSelect cacheOptions loadOptions={this.updateProp} />;
}
}
const FETCH_PATIENT = gql`
query Patient($input: PatientSearchInput) {
getPatients(input: $input) {
id
first_name
}
}
`;
export default graphql(FETCH_PATIENT, {
options: ({ mobile }) => ({ variables: { input: { mobile } } })
})(PatientSearchByPhone);
Versions:
"react-apollo": "^2.1.11",
"react-select": "^2.1.0"
Thanks for your time.
I got an e-mail asking a response to this question. It reminds me of this XKCD comics:
I do not recall the exact solution I implemented, so I setup a complete example for this.
This app (code snippet below) kickstarts searching as soon as you type 4 characters or more in the input box (You are expected to type artist's name. Try vinci?). Here is the code:
import React, { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import ApolloClient, { gql } from "apollo-boost";
const client = new ApolloClient({
uri: "https://metaphysics-production.artsy.net"
});
const fetchArtists = async (input: string, cb: any) => {
if (input && input.trim().length < 4) {
return [];
}
const res = await client.query({
query: gql`
query {
match_artist(term: "${input}") {
name
imageUrl
}
}
`
});
if (res.data && res.data.match_artist) {
return res.data.match_artist.map(
(a: { name: string; imageUrl: string }) => ({
label: a.name,
value: a.imageUrl
})
);
}
return [];
};
const App: React.FC = () => {
const [artist, setArtist] = useState({
label: "No Name",
value: "https://dummyimage.com/200x200/000/fff&text=No+Artist"
});
return (
<div className="App">
<header className="App-header">
<h4>Search artists and their image (type 4 char or more)</h4>
<AsyncSelect
loadOptions={fetchArtists}
onChange={(opt: any) => setArtist(opt)}
placeholder="Search an Artist"
className="select"
/>
<div>
<img alt={artist.label} src={artist.value} className="aimage" />
</div>
</header>
</div>
);
};
export default App;
You can clone https://github.com/naishe/react-select-apollo it is a working example. I have deployed the app here: https://apollo-select.naishe.in/, may be play a little?
The other option is to execute the graphql query manually using the client that is exposed by wrapping the base component with withApollo.
In the example below, we have,
BaseComponnent which renders the AsyncSelect react-select component
loadOptionsIndexes which executes the async graphql fetch via the client
BaseComponent.propTypes describes the required client prop
withApollo wraps the base component to give us the actual component we'll use elsewhere in the react app.
const BaseComponent = (props) => {
const loadOptionsIndexes = (inputValue) => {
let graphqlQueryExpression = {
query: QUERY_INDEXES,
variables: {
name: inputValue
}
}
const transformDataIntoValueLabel = (data) => {
return data.indexes.indexes.map(ix => { return { value: ix.id, label: ix.name }})
}
return new Promise(resolve => {
props.client.query(graphqlQueryExpression).then(response => {
resolve(transformDataIntoValueLabel(response.data))
})
});
}
return (
<>
<div className="chart-buttons-default">
<div className="select-index-input" style={{width: 400, display: "inline-block"}}>
<AsyncSelect
isMulti={true}
cacheOptions={true}
defaultOptions={true}
loadOptions={loadOptionsIndexes} />
</div>
</div>
</>
)
}
BaseComponent.propTypes = {
client: PropTypes.any,
}
const ComplementComponent = withApollo(BaseComponent);
Sorry if the example is a little off - copy and pasted what I had working rather than moving on without giving back.

Resources