Skip to content

Commit

Permalink
Merge pull request #10 from bensmithett/docs-and-hydration
Browse files Browse the repository at this point in the history
Add documentation, rename isomorphic to hydration & remove document template
  • Loading branch information
bensmithett authored Jul 8, 2020
2 parents df64910 + 094fde3 commit cc75f2e
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 35 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Welcome to your new [San Blas](https://sanblas.netlify.com/) site!
- Install dependencies: `yarn` or `npm install`
- `yarn start` or `npm start` & visit http://localhost:5000

[Read the docs](https://sanblas.netlify.com/) for more.
Explore the `app` folder or [read the docs](https://sanblas.netlify.com/) for more.
52 changes: 52 additions & 0 deletions app/components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Components

Components are the building blocks that make up your pages and layouts. They're where you define HTML, CSS and (optionally) client-side JS behaviour. Build them quickly in Storybook, then compose them into pages.

San Blas components are built with 2 opinionated conventions:

- Style with Fela
- Opt-in client-side JS

## Style with Fela

San Blas uses [Fela](https://fela.js.org/) for styling components. See the Fela docs and provided examples, but essentially you write CSS-in-JS that looks like inline styles, which Fela translates into optimised atomic classes:

```js
<button className={css({
background: 'green',
borderRadius: 5
})}>

// Renders <button class='a b'>
```

## Opt-in client-side JS

By default, San Blas components are **only prerendered** and have **no client-side JS behaviour**.

For example, by default this button will appear in the page's prerendered HTML, but nothing will happen when it's clicked.

```js
const HiButton = () => (
<button onClick={() => alert('hi')}>
Say Hi
</button>
)
```

### Progressive enhancement

Prerendered component HTML can be [progressively enhanced](https://en.wikipedia.org/wiki/Progressive_enhancement) in the browser by any client-side JS added to `entry.client.js`.

### React component hydration

A single React component can be used to both:

- prerender static HTML in a server environment, and
- attach client-side behaviour to that prerendered HTML in a browser environment via [hydration](https://reactjs.org/docs/react-dom.html#hydrate)

In many React-based frameworks, the top-level page component is hydrated along with every component that makes up that page, regardless of whether they all actually have client-side behaviour that requires hydrating.

By contrast, San Blas requires you to **opt specific components in** to hydration (you may of course choose to hydrate your top-level component).

Helpers are provided in `hydration_helpers.js` simplify hydration (though you may hydrate manually in `entry.client.js`).
2 changes: 1 addition & 1 deletion app/components/welcome_banner/welcome_banner.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { useFela } from 'react-fela'
import island from '../../images/island.jpg'
import {asIsland} from '../../isomorphic_helpers'
import {asIsland} from '../../hydration_helpers'

const styles = {
root: {
Expand Down
18 changes: 10 additions & 8 deletions app/entry.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@ Analytics snippets, ReactDOM.render(), fancy graphs, etc...

/*
⚛️ Optional:
Rehydrate prerendered React/Fela components using San Blas isomorphic helpers.
Hydrate prerendered React/Fela components using San Blas hydration helpers.
If you use asIsland() to render components in your pages, the following code
will rehydrate those components with the same props.
If you wrap components in your pages with the asIsland() HOC, the following code
will hydrate those components with the same props they were rendered with.
If you remove this code, the prerendered component HTML won't be rehydrated.
All components requiring hydration must be explicitly imported and passed to hydrateIslands().
If you remove the following code, the prerendered component won't be hydrated.
*/

import { createRenderer } from 'fela'
import { rehydrate } from 'fela-dom'
import { rehydrateIslands } from './isomorphic_helpers'
import { hydrateIslands } from './hydration_helpers'

// Rehydrate Fela styles
// Hydrate Fela styles
const felaRenderer = createRenderer()
rehydrate(felaRenderer)

// Rehydrate San Blas islands
// Hydrate San Blas islands
import WelcomeBanner from './components/welcome_banner/welcome_banner'

rehydrateIslands({
hydrateIslands({
WelcomeBanner
}, felaRenderer)
23 changes: 22 additions & 1 deletion app/entry.prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { RendererProvider } from 'react-fela'
import { renderToMarkup } from 'fela-dom'
import { Helmet } from 'react-helmet'
import { cssReset } from './components/global_css'
import documentTemplate from './layouts/document_template'
import DefaultLayout from './layouts/default_layout'

export default function prerender (manifest, mode) {
Expand Down Expand Up @@ -163,3 +162,25 @@ function buildJSONFeedFile (pageProps) {
console.log(chalk.green(`🏝 JSON Feed built: ${outputFilePath}`))
})
}

function documentTemplate ({
stylesHTML,
bodyHTML,
helmet,
clientBundlePath
}) {
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
${stylesHTML}
</head>
<body ${helmet.bodyAttributes.toString()}>
${bodyHTML}
<script src='${clientBundlePath}'></script>
</body>
</html>
`
}
2 changes: 1 addition & 1 deletion app/isomorphic_helpers.js → app/hydration_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function asIsland (componentName, Component, {
return Hoc
}

export function rehydrateIslands (islands, felaRenderer) {
export function hydrateIslands (islands, felaRenderer) {
document.querySelectorAll('[data-sanblas-hydrate-as]').forEach(island => {
const Component = islands[island.dataset.sanblasHydrateAs]
const componentProps = JSON.parse(island.dataset.sanblasHydrateWith)
Expand Down
31 changes: 31 additions & 0 deletions app/layouts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Layouts

Layouts, a concept borrowed from [Rails](https://api.rubyonrails.org/classes/ActionView/Layouts.html), allow you to template the HTML _around_ your page content.

This could include things like:

- Tags for your `<head>`
- "Wrapper" divs, or a shared header, footer or sidebar

Unlike Rails, you don't need to directly template your layout's `html`, `head` or `body` tags. Instead, manage those tags with `Helmet` and the San Blas prerender function will put everything in the right place in the final rendered HTML document.

## Layout props

Layout components are rendered with 2 props:

- `meta` (object): the `meta` object exported by each page
- `children` (React Element): the page content

## Specifying a layout for a page

Pages use the `DefaultLayout` component unless another component is specified in the page's `meta.Layout`:

```js
import DifferentLayout from '../layouts/different_layout'

export const meta = {
title: 'My Page Title',
description: 'This page has a different layout',
Layout: DifferentLayout
}
```
2 changes: 1 addition & 1 deletion app/layouts/default_layout.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import favicon from '../images/favicon.png'
import favicon from './favicon.png'

export default function DefaultLayout ({ meta, children }) {
return (
Expand Down
21 changes: 0 additions & 21 deletions app/layouts/document_template.js

This file was deleted.

File renamed without changes
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sanblas",
"version": "3.0.0",
"version": "4.0.0",
"scripts": {
"start": "concurrently \"node ./app/build.js development\" \"serve\" --kill-others --prefix-colors=yellow.dim,cyan.dim",
"build": "node ./app/build.js production",
Expand Down

0 comments on commit cc75f2e

Please sign in to comment.