I was recently working on my portfolio site, and I wanted to easily pull in the site title with a graphQL query. After a bunch of trial and error, I ended up using StaticQuery
to get the data and created a wrapper around react-helmet
to make setting the title easy. However, I wasn’t able to find any great resources for this in Typescript––here’s how to do all of this with Typescript.
Update: Gatsby recently added a React hook to do this with less code. Scroll to the bottom to see how to use the hook.
An Introduction to StaticQuery
As part of my search for information on this, I came across some excellent documentation on using StaticQuery in Gatsby’s docs (worth a read if you want more background). The initial code sample they provided was this:
import React from "react"
import { StaticQuery, graphql } from "gatsby"
export default () => (
<StaticQuery
query={graphql`
query HeadingQuery {
site {
siteMetadata {
title
}
}
}
`}
render={data => (
<header>
<h1>{data.site.siteMetadata.title}</h1>
</header>
)}
/>
)
This code:
- Declares a StaticQuery object and a graphQL query for what it’s looking for
- Exposes and object called data
- Passes data along to its render function, which then consumes the data in some way
Then the author gets us a little bit closer with propTypes
:
import React from "react"
import { StaticQuery, graphql } from "gatsby"
import PropTypes from "prop-types"
const Header = ({ data }) => (
<header>
<h1>{data.site.siteMetadata.title}</h1>
</header>
)
export default props => (
<StaticQuery
query={graphql`
query {
site {
siteMetadata {
title
}
}
}
`}
render={data => <Header data={data} {...props} />}
/>
)
Header.propTypes = {
data: PropTypes.shape({
site: PropTypes.shape({
siteMetadata: PropTypes.shape({
title: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
}
Now the code has a better separation of concerns, which allows us to add propTypes
to our Header object.
What about with Typescript?
Let’s re-write this to use Typescript, and also add a className
prop that we can pass through to our title. First, our imports:
import { graphql, StaticQuery } from 'gatsby';
import * as React from 'react';
Next, we’ll define two interfaces: one for props we want to pass into this component, and one for the data. We’re going to extend the first interface (HeaderProps
) in our second interface (HeaderPropsWithData
), so that we have a comprehensive representation of our props types:
interface HeaderProps {
className: string;
}
interface HeaderPropsWithData extends HeaderProps {
data: {
site: {
siteMetadata: {
title: string;
},
},
};
}
Now that we have an interface, let’s use it in our Header
component. I’m using a stateless functional component here, but you could use a regular component, too. We’re extending the HeaderPropsWithData
so that we have type definitions for both user-defined and graphQL-provided props.
const Header: React.SFC<HeaderPropsWithData> = ({
className,
data,
}) => {
return (
<header>
<h1 className={className}>{data.site.siteMetadata.title}</h1>
</header>
);
};
Now that we have our Header
function defined, let’s declare our export and our StaticQuery
:
export default (props: HeaderProps) => (
<StaticQuery
query={graphql`
query {
site {
siteMetadata {
title
}
}
}
`}
// tslint:disable-next-line jsx-no-lambda
render={(data) => <Header data={data} {...props} />}
/>
);
I’m using HeaderProps
as the type definition for props
, which allows us to define things that the user can pass to this function (like className
). We wouldn’t want to use HeaderPropsWithData
because it would throw an error about missing the data object.
I’m also disabling TSLint because TSLint doesn’t like it when we pass a lambda to a render function. This can result in performance issues, but because this is using Gatsby and it is generating a static site for us, we can just ignore that error here.
The finished code
If you’re just looking for the finished code, I decided to stick it in a Github gist. And if you happen to have the exact use case as me, you’re welcome to look at the wrapper class I created for react-helmet
.
Update: The useStaticQuery Hook
Since the time I originally wrote this, Gatsby released a React hook that simplifies using StaticQuery. Here's the same example from before, but rewritten using the new useStaticQuery
hook.
// 1
import { graphql, useStaticQuery } from 'gatsby';
import * as React from 'react';
interface HeaderProps {
className: string;
}
// 2
interface HeaderData {
site: {
siteMetadata: {
title: string;
},
};
}
// 3
const Header: React.SFC<HeaderProps> = ({
className,
}) => {
// 4
const data: HeaderData = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
}
}
}
`)
return (
<header>
<h1 className={className}>{data.site.siteMetadata.title}</h1>
</header>
);
};
// 5
export default Header;
Here are the changes from the original version:
- This imports
useStaticQuery
instead ofStaticQuery
. - Instead of inheriting the previous interface, this creates a separate interface for the data.
- The
Header
function directly usesHeaderProps
as its interface. - The
data
object is declared and usesHeaderData
as its interface. The output of theuseStaticQuery
hook (with the graphql statement) is then assigned to thedata
object. - Instead of exporting
StaticQuery
within an anonymous function, I can directly export theHeader
component.
Using the hook makes static queries within components cleaner and easier to follow.