Josh Pollock, Founder & Lead Developer, CalderaWP Reading estimate: 9 minutes
Isomorphic Gutenberg Blocks
When the first beta of Gutenberg was made available for testing, one of the most controversial decisions was that the default storage for block attributes is having the HTML saved in post content. Block attributes—the settings on blocks—can also be stored in post meta or other locations, but the system most core blocks use is to save the data as HTML (as the end user will view it) or to serialize it to comments.
For example, to slightly oversimplify how the core paragraph block works: in post content, you have some HTML like this:
The attribute for the paragraph content is stored as the inner HTML of the paragraph tag. I like this simplicity; it's semantic and makes sense (paragraphs are stored inside paragraph tags). The HTML that ends up on the page could be enhanced with CSS and JavaScript, or if those features of the browser are unavailable or unneeded, it's just semantic HTML for anyone or any client to read.
Isomorphic Blocks
As another example, the source of the image in an image block is stored in the src attribute of the image tag.
A block that saves an image tag might also use attributes to describe how it should be displayed. Those attributes would be used by CSS and JavaScript to make the block better but, without that, it's just an image. These kinds of blocks, which can be stored as HTML to preserve their state, are what I like to call "Isomorphic Blocks.” An Isomorphic Gutenberg Block, a term I made up to sound smart, uses the same React components for its "view" no matter where it is being displayed—editor, front-end or a client decoupled from WordPress. Around the JavaScript community you're likely to hear "isomorphic" used to describe using the same code on the server-side and in the browser.
I think this is a smart way of working, as it leads to testable, reusable code and the blocks are not reliant on JavaScript. This means the content created with the block remains accessible, even if the plugin adding the block has been removed or failed. This is not a perfect approach for all types of blocks, as it assumes the entire block's state can be public (not always true), but I think it's applicable for simple blocks and complex applications that are composed of related blocks.
Hydration And Server-Side Rendering
In my post on Gutenberg and Headless for Pantheon, I talked about reusing components between Gutenberg and other apps and gave some basic examples of how to re-use components in the front-end and editor. I didn't get too far into how to do so. I gave one example of saving an empty HTML container in the block and then in the front-end, mounting the same component on it.
This follows how we generally use React and ReactDOM for apps. Compare this fairly standard way of mounting a React app:
This example from my previous article for mounting the block "app" works, but if JavaScript is disabled or broken, the block disappears. I wanted to take this example a step further, so that JavaScript being enabled or a plugin on the block being activate would not be necessary for the block to show in the front-end with basic functionality.
Using ReactDOM.render() misses a huge opportunity. In the block editor, WordPress parses the attributes back to an object and hydrates its state from the saved HTML. This got me thinking: I know how to use ReactDOM.hydrate() to hydrate a server-side rendered React app running in node, so why can't I use ReactDOM.hydrate() to hydrate my blocks in the front-end?
It turned out, this works pretty well. In this article, I'm going to show how to build a block that adds a form to the page—something I know about. The form will become interactive in the front-end.
Modern JavaScript In WordPress Blocks
In this article, I will be using JSX and the latest ecmascript syntax for the JavaScript. This means that the code will require compilation before being used in the browser. In the past, I accomplished this with webpack, which I configured myself.
The Gutenberg team recently released the @wordpress/scripts package to simplify building, testing and linting blocks. It's really cool and very simple. I've spent a lot of time setting this all up manually, so I love this new, three-step process:
First, add the package to your project:
npm install @wordpress/scripts --save-dev
Then add scripts to package.json:
"scripts":
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses --production",
"lint:css": "wp-scripts lint-style '**/*.css'",
"lint:js": "wp-scripts lint-js .",
"lint:pkg-json": "wp-scripts lint-pkg-json .", "start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e --config e2e/jest.config.js",
"test:unit": "wp-scripts test-unit-js",
"env:start": "bash start.sh"
},
Last, create a block in src/index.js (we will get more into that next).
You can see a more complete list of commands in the README, but this gives us the ability to compile, with a hot module replacement by running `yarn start` and an optimized build by running `yarn build`.
This package requires no config to enable testing, linting and more. It is extensible, which we will get into later in this post, but first, let's make a block!
Composing Blocks From Components
Ok, now we need a block. I'm assuming in this article that you know how to build blocks and are familiar with React. If not, then I recommend taking a look at some more basics articles first. For a walk through of a basic block, with non ES6, I recommend this post by Amanda Giles. You should also check out the official tutorial on block creation. Zac Gordon’s complete course on Gutenberg development, which I also recommend.
I will walk through the block design highlighting what's unusual and unique on this.
State
Before we can flesh out an API for our components, we need to discuss what shared state they will pass around. This way we can design the components around displaying and updating those values. In Gutenberg, state is described with block attributes, so we will start there:
The block we are creating adds an input field to the post, so the first attribute is its default value. That attribute is stored in the value attribute of the HTML input that we will save for the block. The other attribute is the block ID. That's a unique ID for the block that will also get appended to the input, this is important for the front-end.
We have one piece of state to display—the default value. We will need a component to edit that attribute and a component to display it—in the block editor or front-end.
Let's start with the component to display the form, let's call it Form:
This is a pure component; it has no side effects. It is also a controlled component, its state is managed by its parent. If you used this component alone, changes to the input would not change the value. This is good, it's decoupled from the state management system. Let's wire it up to WordPress in the block editor.
In src/index.js, create a new block, I trust you know how to do that, using the attributes I showed earlier. Here is a skeleton block to start from:
That's a placeholder. First thing we need to do is put the Form component in the edit callback. It's going to need three props—defaultValue, onChange and id. The defaultValue and id we get from Gutenberg, via our attributes:
Now, we have everything we need to display the form in the edit callback:
Because this UI is a form field, it made sense to use that form field to update its own default value, so I passed my change handler there. I still wanted to have controls in the block sidebar, so I think it makes sense to add that in using inspector controls. Here is the full editor:
Saving Isomorphic Blocks
Now that we have the edit interface for the block, we need to add the save callback. The goal here is to put HTML on the page that is valid and has the saved state. We don't want to assume that the front-end will become interactive via React, but our goal is to make that possible.
To satisfy our goal of being totally isomorphic - lets use the same component as we used to preview it:
That component is going to be used again in the front-end. That's the point. One component, use as much as possible. This saves the default value attribute, class name—how WordPress identifies the block type—and unique Id to the DOM.
Customizing @wordpress/scripts
Before I wrote that the WordPress scripts package was zero config, which was awesome as that default configuration was all we needed. But, before we can talk about how to use this block in the front-end, we need a separate entry file for that JavaScript. The front-end JavaScript does not need the block, but it will need some new code. So, let's add an entry point to the configuration.
You can think of the WordPress scripts' webpack configuration as something that can be overridden as needed. In this case, we need to change the setting for "entry". According to WordPress' docs, we do this by creating a webpack.config.js file and merging the default config from WordPress with our own. Here is what that looks like:
This will compile /src/front.js to /build/front.js. That file will need to get loaded or inlined wherever the block is loaded.
Hydrating Blocks
The block's HTML is already on the page, as is its state. Once the page is loaded, we will pick that block's state off of the DOM and then rehdyrate it. Just to prove it works, my example is going to be to add an extra element to the DOM to display changes to the value in real time.
In this demo, the content will move around a little bit when the app is hydrated. That is actually what this approach, if done right avoids. If everything you need for your interactive app is already on the DOM, there is no flash of empty space as you wait for the app to load, it's just there and the end-user doesn’t really notice that it changed from plain HTML to a React app, since the end result is the same HTML on the page.
Let's see how that will work. Server-side rendered React apps use a two step process. One the server, ReactDOMServer.render(), which has the same API as ReactDOM.render(), creates and HTML page. In the client-side JavaScript, instead of mounting the app with ReactDOM.render() It has the same API as ReactDOM.render(), we use ReactDOM.hydrate(), to mount the app over the existing HTML, skipping the initial render.
In our case, WordPress already took care of the render. We just need to do step two of the process- hydrating.
WordPress identifies block type by a class name, so we can query for elements that have that class to see if any of our blocks are on the page:
If we have matching elements, we can loop through each one in the collection, find its state by reading the DOM attributes and use those to hydrate the app.
I am referring to this as "the app" as each instance is a mini-app. Because Gutenberg is not providing state management, we will need to provide our own. This will be a wrapper around our form component, which does not manage state, to provide state management:
That's a bit simplified from how the full example works. Check it out on Github:
https://github.com/Shelob9/isoblock
Other Possibilities
For a simple block like this, I am OK with setting state from the DOM this way. But there are some reasons that might not scale. For a more complex block with tons of attributes, the DOM reads might become slow. Also, the data is static. In some cases it might make more sense to supply state from an API response. That way there is less DOM manipulation and the content of the block can be updated after the post the block is in is updated.
The other issue I see with my approach of reading the attributes this way is its reverse-engineering Gutenberg. Gutenberg supplies a parser we could use. That's more complexity and out of scope of this article, but might be a better option than expanding on the naive implementation I have here.
Isomorphic Time
How blocks are displayed in the front-end of WordPress sites or decoupled clients, is a question that I've been asked a lot at WordCamps. There is no right answer, but I like to think HTML is the answer, most of the time. It took me awhile to get to this solution. What do you think about it?
Topics
Discover More
Safely Publish to Web from Google Docs with Pantheon Content Publisher
Roland Benedetti (Senior Director, Product) and Zack Rosen (Co-Founder)
Reading estimate: 7 minutes
Unifying Content and Code: Inside Pantheon’s Vision for Content Operations
Chris Yates
Reading estimate: 5 minutes
How Pantheon Protects Your Site from Software Supply Chain Risks in Open Source
Steve Persch
Reading estimate: 8 minutes