Our application for our Tour of Taiwan event. Utilizes Gatsby, React, Netlify, and Firebase
- Installation
- File Structure
- Customizing Theme and Styling
- Page Transitions
- Firebase
- Stripe
- Animations
- Netlify
- React Rehydration
Everything is already set-up in the package.json so all you have to do is
npm install
Note that because of this issue with react-spring, we currently have to manually patch the react-spring packages. Luckily with the use of the patch-packages
package, this has already been done for you via the postinstall
command.
You will have to install gatsby-cli which you can do with npm install -g gatsby-cli
Their website has a nice tutorial which I recommend following.
You also can find the original Gatsby's original README.md here. That doc details a quick look at some file structure and basic files for this repo
Note: On windows, you may not be able to run gatsby develop
with hot reloading if you're using the Bash subsystem but your code is in the Windows file system. This is because the bash subsystem has trouble passing data and setting flags across to the windows file system. To fix this, move your project into the Unix filesystem.
Look at this GitHub issue on hot reloading for more info
In order for the gatsby-plugin-firebase plugin to work properly, you'll have to create a .env
file and add our credentials there. Copy the contents of the .env.example file into the new .env
file. To get our credentials:
- Go to the Firebase console
- Sign into the UMCP TASA email
- Click on Tour of Taiwan
- Under the title, click the button that says
</> Tour of Taiwan Web
- Click the gear icon
- Scroll down to "Your Apps"
- Copy the information in the code highlight under firebase config to your .env file
- Names of the keys should correspond to the variable in the .env file
Tbh I don't know if we actually had to store these in an environment variable, since it doesn't look like these are secret keys. But this is a way to prevent anyone on the Internet from accessing our config information.
Types are cool. I hope I can convert you to Typescript (or Flow) too. Here are some resources for Typescript and Typescript with React
- https://github.com/typescript-cheatsheets/react
- https://ts.chibicode.com/todo/
- https://2ality.com/2018/04/type-notation-typescript.html
- src/components: Holds all of the components for our site. Each sub-folder has an
index.ts
file that re-exports components so we can have a niceimport {} from "components/.."
- src/hooks: Holds the custom hooks for our project. Take a look at React's documentation for hooks for a nice introduction to hooks
- src/assets: Our assets folder that'll contain any images, videos, or json files we end up using
- src/pages: Each file in this folder correspond to a page on the site. The path for the page matches the filename
- src/types: Our custom Typescript types
This site uses Material-UI components for styling. The theme can be modified in theme.tsx
to change the primary and secondary colors, the spacing used throughout the site, and typography. More info about customizing theme can be found on the official Material-UI cutomization guide. The theme is provided to all the pages in gatsby-browser.js
via the ThemeProvider component.
The Material-UI framework relies on the idea of css-in-js. In particular, we use Material-UI's Hook API method of adding styles. It works by defining a custom hook for each component useStyles
. We can add styles to components by defining objects in the function body. There's a slight difference in naming between usual CSS fields and CSS-in-JS fields, but that's usually replacing '-' with camelCase. We can also use media queries to use different styles depending on the size of the screen!
// We pass in a function with theme as the argument so that we can access when defining styles
const useStyles = makeStyles(theme => ({
// Styles generated for classes.root
root: {
padding: theme.spacing(1), // theme.spacing(x) lets us have consistent
backgroundColor: "red", // Notice how we use camelCase instead of background-color
[theme.breakpoints.up("md")]: {
// A media query that means for sizes greater than medium, apply these styles
padding: theme.spacing(2),
},
"& p": {
// A nested selector that targets all <p> tags under the parent
backgroundColor: "green",
},
},
}))
const CoolComponent = ({}: Props) => {
const classes = useStyles()
return (
<div className={classes.root}>
Some nice content
<p>Some nice content with a green background</p>
</div>
)
}
All h1, h2, h3, etc elements can be customized across the site in themes as well. Here's the example from the Material-UI documentation
const theme = createMuiTheme({
typography: {
subtitle1: {
fontSize: 12,
},
body1: {
fontWeight: 500,
},
button: {
fontStyle: "italic",
},
},
})
Smooth page transitions make everything look polished. We'll be using gatsby-plugin-transition-link to handle the transitions. I'm still not entirely sure how this works, so if anyone has a better solution please let me know! Right now, our footer and header components use AniLink, the plugin's default transition component, to create a swipe like effect like how apps transition between pages.
I had to add a custom ".d.ts" file in src/types in order to add types to this plugin. I followed this Medium article to create a declarations file.
We utilize gatsby-plugin-firebase to handle importing and utilizing our firebase instance. Due to how Gatsby uses server side rendering to pre-render some of the sites, we have to have a different way of using firebase. The gatsby-plugin-firebase plugin handles all of that for us.
We then use react-firebase-hooks for that sweet hook abstraction on querying our Cloud Firestore. We also use our own Firebase cloud function to generate the session ID for stripe.
Our Firebase Function that we use to auto-generate tickets when an account is created and to create tickets is located in our private repo tour-of-taiwan-admin
We use Stripe for payment! When the user clicks on the Checkout button, we call our Firebase Cloud Function that generates the Stripe Checkout Session ID for us. Then we pass that ID to redirectToCheckout
which brings us to a Stripe hosted checkout page. By doing this, we can prevent malicious users from manipulating prices and listening in on credit card info. Once stripe processes the order, it sends a webhook to another Firebase Cloud Function that then generates the raffle tickets for the user. Our checkout session contains metadata of who to associate the ticket with and how many of each ticket to generate. Stripe passes this metadata to our Firebase function which then uses it to generate the right amount of tickets.
use-shopping-cart is a wonderful plugin that helps us manage cart state. It's a provider, so it sits in our App.tsx
This site use react-spring for animations. In order to fix some issues with typing, I upgraded to the 9.0.0-rc.3 version of react-spring. This means that some of the docs on the main page are outdated. Instead, refer to the react-spring v9 docs.
We're using Netlify to host our site! You can access our control panel by signing into app.netlify.com with our UMCP TASA email. All of the environment variables are set up already.
If styles look like they work on first load but don't on subsequent loads or vice versa, the problem is likely with server side rendering. In order to serve faster pages, Gatsby first pre-compiles during build the DOM from our React code on build. Gatsby uses the functions wrapRootElement and wrapPageElement in gatsby-ssr.js
during this time, which is why those two functions have to be the same as the ones in gatsby-browser.js
which is the file that determines what the site uses on the client side.
Because React uses rehydration instead of re-rendering to reconcile the differences between server and client side rendering, things can start to get wonky. Rehydration relies on the assumption that the DOM stays the same which sometimes isn't the case if we have dynamic content. So if styles look wonky between first load, which is what the server provides to the user, and the second load, where the client usually kicks in to render the page, it's likely that some element isn't in the right place in the DOM. To be honest, I'm not sure if this is exactly why things don't look right, but it seems to be the best explanation I've found.
That's where the ClientOnly
component comes in! It utilizes useEffect to only mount the component when the page is loaded. Since this only happens for the client, then the DOM stays the same between when the page is compiled during server-side rendering and when the client gets the page. Rehydration works, and then, the client can add the content that's missing after rehydration occurs. This is called two-pass rendering!
The code for the component as well as a better explanation about this issue can be found on Josh W Comeau's blog post The Perils of Rehydration
Because React Globe uses three.js which requires the window object, we can't render React Globe during Server Side Rendering. To fix this, we wrap the React Globe call in index.tsx with a typeof window === undefined
which means it won't get rendered during server side rendering. We also have to replace the three.js module with a dummy module during SSR. That's what our exports.onCreateWebpackConfig
in gatsby-node.js does. The Gatsby docs on debugging HTML builds has more information about this.
Patching gatsby-source-stripe
per this open issue about how images aren't downloaded for Price objects