Skip to content

Commit

Permalink
initial state rehydration
Browse files Browse the repository at this point in the history
  • Loading branch information
jescalan committed May 2, 2017
1 parent c167f88 commit e91d3d2
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 2 deletions.
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Render preact components to static html and use them like custom elements.

### Usage

Setup is pretty simple -- just add the plugin to reshape and pass it an object with the key being the custom element name you want to be replaced with the rendered component, and the value as the actual component. Reshape will render your components and replace the given custom element names with the components' static html.

```js
const {h} = require('preact')
const reshape = require('reshape')
Expand All @@ -33,6 +35,142 @@ reshape({ plugins: [ssr({ 'my-component': MyComponent })] })
})
```

#### Rehydrating Initial State

So there is one case where you might want some additional logic to avoid duplication. Luckily, we have this logic ready to go, and will walk you through both the use case and solution here. So imagine you have a component like this:

```js
export default class SortableList {
render () {
return (
<ul className='sortable'>
<span className='sort-icon' />
{this.props.children}
</ul>
)
}

componentDidMount () {
// some logic here to make this list sortable
}
}
```

Now you set up the component through the ssr plugin as such:

```js
const ssr = require('reshape-preact-ssr')
const SortableList = require('./sortable-list')

ssr({ 'sortable-list': SortableList })
```

And now in your html, you'd put down something like this:

```html
<body>
<sortable-list>
<li>wow</li>
<li>amaze</li>
<li>very list</li>
</sortable-list>
</body>
```

Ok, so you would get the rendered out `ul` with the classes and span elements you wanted, as expected. However, with this element, you definitely want to also client-side render it since it contains interactive elements. So if your client-side javascript, you run something like this:

```js
const {render} = 'preact'
const SortableList = require('./sortable-list')

render(<SortableList />, document.body, document.querySelector('.sortable'))
```

Ok so this would find the right element and add the javascript interactivity on top. But it would also remove all the contents of your list as soon as the javascript render loads in, because you just rendered an empty element in the code above. Oops! Let's fix that:

```js
const {render} = 'preact'
const SortableList = require('./sortable-list')

render(
<SortableList>
<li>wow</li>
<li>amaze</li>
<li>very list</li>
</SortableList>,
document.body,
document.querySelector('.sortable')
)
```

Ok so this works, but now we have some seriously non-DRY code. Now our markup has to be repeated both in our html for the initial static render, and in the client-side js for the client render. Luckily, reshape-preact-ssr has got your back. By default, it takes the initial html you used to render your preact element, parsed into a reshape AST and compressed as efficiently as possible, and gives it to your element as a prop called `_ssr`. It also provides a helper that you can use to decompress and rehydrate it into a vdom tree that can be directly rendered by preact. So let's take advantage of this in our code and completely cut out all repetition - starting with our component.

What we'll do here is put our compressed initial state on a data attribute so that our client-side js can pick it up and rehydrate:

```js
export default class SortableList {
render () {
return (
<ul className='sortable' data-ssr={this.props._ssr}>
<span className='sort-icon' />
{this.props.children}
</ul>
)
}

componentDidMount () {
// some logic here to make this list sortable
}
}
```

You can see on the top level `ul`, we placed an additional data prop. If you render this to the page, you'll see something like this:

```html
<ul class='sortable' data-ssr='3nko2ir2cR3i2nr2croi23nrc23='></ul>
```

Now let's pick up that compressed initial state from out client side javascript:

```js
const {render} = 'preact'
const SortableList = require('./sortable-list')
const sortableEl = document.querySelector('.sortable')
console.log(sortable.dataset.ssr)
```

Looking good -- now we can pull in `reshape-preact-ssr`'s helper function that will rehydrate the initial state as a vdom tree that's directly renderable by preact. We just need to pass it the compressed initial state, and a remapping back from the custom element name to the actual component as we required it on the client side.

```js
const {render} = 'preact'
const SortableList = require('./sortable-list')
const {hydrateInitialState} = require('reshape-preact-ssr')

const sortableEl = document.querySelector('.sortable')
const vdom = hydrateInitialState(sortableEl.dataset.ssr, {
'sortable-list': SortableList
})

console.log(vdom)
```

You'll see that we have a full preact vdom ready to go, using the right components everywhere you needed them. Now the last step is just to render it!

```js
const {render} = 'preact'
const SortableList = require('./sortable-list')
const {hydrateInitialState} = require('reshape-preact-ssr')

const sortableEl = document.querySelector('.sortable')
const vdom = hydrateInitialState(sortableEl.dataset.ssr, {
'sortable-list': SortableList
})

render(vdom, document.body, sortableEl)
```

And that's it! You'll see no visual difference, as preact won't re-render existing html, but it will remove the `data-ssr` property and layer on the javascript interaction as soon as it loads. Perfect!

### License & Contributing

- Details on the license [can be found here](LICENSE.md)
Expand Down
17 changes: 15 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,38 @@ const util = require('reshape-plugin-util')
const {render} = require('preact-render-to-string')
const {h} = require('preact')
const parse = require('reshape-parser')
const {gzipSync, gunzipSync} = require('zlib')

module.exports = (components) => {
return (tree) => {
return util.modifyNodes(tree, (node) => {
return Object.keys(components).indexOf(node.name) > -1
}, (node) => {
return parse(render(toVnode(components, node)))
// encode/compress the original html structure
// this can be rehydrated later to reduce client/server duplication
const originalHtml = gzipSync(JSON.stringify(node)).toString('base64')
return parse(render(toVnode(components, node, originalHtml)))
})
}
}

function toVnode (components, node) {
// Given an encoded _ssr attribute and an object that maps keys as custom
// element names to values as preact components, provides a full rehydrated
// vdom object representing the initial state before server rendering
module.exports.hydrateInitialState = (encoded, components) => {
return toVnode(components, JSON.parse(gunzipSync(Buffer.from(encoded, 'base64'))))
}

function toVnode (components, node, originalHtml) {
// get element name or component name if registered
const name = components[node.name] || node.name
// convert props to strings
const props = {}
for (let k in node.attrs) {
props[k] = node.attrs[k].map((n) => n.content).join('')
}
// if there is a compressed original source, add it as _ssr prop
if (originalHtml) { props._ssr = originalHtml }
// content is either a string, a subtree, or there isn't any
if (typeof node.content === 'string') {
return h(name, props, node.content)
Expand Down
17 changes: 17 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const {h} = require('preact')
const {render} = require('preact-render-to-string')
const reshape = require('reshape')
const ssr = require('..')
const test = require('ava')
Expand Down Expand Up @@ -48,3 +49,19 @@ test('renders children', (t) => {
t.is(res.output(), '<div class="parent">hello<p>wow</p><div><div class="wow">hello from c2</div></div></div>')
})
})

test('initial state rehydration', (t) => {
const MyComponent = ({ foo, _ssr }) => {
return h('p', { 'data-ssr': _ssr }, `the value of foo is "${foo}"`)
}

const html = "<my-component foo='bar' />"

return reshape({ plugins: [ssr({ 'my-component': MyComponent })] })
.process(html)
.then((res) => {
const compressed = res.output().match(/data-ssr="(.*?)"/)[1]
const rendered = render(ssr.hydrateInitialState(compressed, { 'my-component': MyComponent }))
t.is(rendered, '<p>the value of foo is &quot;bar&quot;</p>')
})
})

0 comments on commit e91d3d2

Please sign in to comment.