Gatsby reusable components with own query

Viewed 2319

Is it currently possible in Gatsby to build reusable components that have their own graphql query? If not, currently what is the least "coupled" way to pass data to reusable components? I can't seem to find any examples that do this and I can't find anything in the documentation.

Say, for example, you want to make a tag cloud component for a blog that would show up in the side bar of all "post" pages as well as on a single "/tag-cloud" page. You could use like <TagCloud limit={20} /> or <TagCloud tags={uniqueTags} />. For this, you would need to query all edges, map-reduce/extract each tags array down to a unique, ordered set of unique tag strings.

Or, say you wanted an "other products" component for a catalog page that excludes the current product. Here, you might have <ProductsList exclude={currentProduct} />. This would be a straight-forward filtered query at build-time.

The only way I can see doing this is either to monkey-patch the query results from createPages() or to pass data through context in createPage({path, component, context}). Should this take place in gatsby-node.js? Are there any other ways?

2 Answers

I am answering my own question. If someone has a better solution, please submit it and I will gladly accept it.

My current solution, which I referenced in my question and I think is suboptimal, is to build all of the data required for top-level reusable components in pages/index.js (or anywhere you use createPage({path, component, context}) and inject the data into context. This makes it available to the component constructor via the pathContext property. The name, pathContext makes me think this is an abuse of spirit of this object, but it gets the job done.

For example, if you had a blog whose entries could have an array of tags, this might be your gatsby-node.js:

const path = require('path');

const getUniqueTags = edges => {
    const set = new Set();

    edges.forEach(edge => edge.node.frontmatter.tags.forEach(tag => set.add(tag)));

    return [...set];
};

exports.createPages = ({ graphql, boundActionCreators }) => resolve(graphql(`
        query AllPagesQuery {
            allMarkdownRemark {
                edges {
                    node {
                        frontmatter {
                            path
                            tags
                        }
                    }
                }
            }
        }
    `)))
    .then(({ errors, data }) => {
        const { createPage } = boundActionCreators;
        const pageTemplate = path.resolve('./src/templates/page.js');

        if (errors) return Promise.reject(errors);

        return data.allMarkdownRemark.edges.forEach(edge => {
            createPage({
                path: edge.node.frontmatter.path,
                component: pageTemplate,
                context: {
                    path: edge.node.frontmatter.path,
                    tags: getUniqueTags(data.allMarkdownRemark.edges)
                }
            });
        });
    })
    .catch(err => console.log(err));

Then your templates/page.js could receive the unique tags array like this:

const IndexPage = ({ pathContext }) => {
    const { tags } = pathContext.map(tag => <li>{ tag }</li>);

    return <div>{ tags }</div>;
};

export default IndexPage;

You can create a fragment query in your layouts/index.js

export const avatarsFragmentQuery = graphql`
  fragment avatars on RootQueryType {
    avatars: allImageSharp {
      edges {
        node {
           id

         }
       }
    }
  }
`

And use it in your pages:

export const pageQuery = graphql`
  query BlogQuery {
    site {
      siteMetadata {
        title
      }
    }

    ...avatars
  }
`
Related