yarn add koa2jsx
koa2-jsx
is middleware for Koa2
which provides support for rendering JSX in a server application via react-dom/server
. It also uses Redux
to create a store updatable with actions for a better control of what data needs to be rendered, for example, it is possible to create a reducer with a title
slice, and an action to set that property from ctx
, and then print { title }
in the JSX template.
In addition to the core functionality, the package gives a minimum wireframe View
container and actions to set page title and viewport, add external and inline scripts and styles, icons and a link to the manifest file.
Finally, there's an extra middleware function which can be used after koa2-jsx
and wireframe actions were installed to include links to Bootstrap 4
scripts (including jQuery) and CSS.
The module will return a single middleware function which accepts 3 arguments: reducer
, View
and actions
. They are describe below in the Example section.
The middleware function will perform the following for each request:
- Initialise the Redux store by creating a reducer;
- assign actions to the context, such as
{ setTitle(title) }
becomesctx.setTitle
; - wait for all other middleware and pages to resolve; and
- render
ctx.Content
if found usingreact-dom/server
as a stream with doctype html sent, using the View.
import Koa from 'koa2'
import koa2Jsx from 'koa2-jsx'
import { combineReducers } from 'redux'
import { connect } from 'react-redux'
const app = new Koa()
const View = ({ title, children }) => {
return (
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
{children}
</body>
</html>
)
}
const jsx = koa2Jsx({
reducer: combineReducers({
title(state = null, { type, title }) {
if (type != 'SET_TITLE') return state
return title
}
})
actions: {
setTitle(title) {
return { type: 'SET_TITLE', title }
}
}
View: connect(state => state)(View),
})
app.use(jsx, async (ctx, next) => {
ctx.setTitle('Welcome to the koa2-jsx world')
ctx.Content = <div><h1> Hello @ there </h1></div>
await next()
})
When setting up middleware, ensure that the koa2-jsx
middleware function comes
ahead of pages so that the Redux store and render logic are initialised.
If ctx.Content
is set in downstream application middleware, <!doctype html>
is written and a readable stream from React Dom's
renderToStaticNodeStream(<WebPage />)
is be piped into ctx.body
.
koa2Jsx({
  reducer: function,
  View: Container,
  actions: object,
  static?: boolean = true,
  render?: function,
}): function
This will set up the middleware function and return it. Add it as a usual Koa2
middleware (shown below).
The example shows how to create a reducer, actions and View for a minimum HTML template.
/* yarn example/ */
import Koa from 'koa2'
import koa2Jsx from 'koa2-jsx'
import actions from './actions'
import reducer from './reducer'
import View from './Containers/View'
const app = new Koa()
const jsx = koa2Jsx({
reducer,
View,
actions,
static: true,
// ^ set to false for universal applications
pretty: false,
// ^ set to true for prettified HTML output
})
app.use(jsx, async (ctx, next) => {
ctx.setTitle('Welcome to the koa2-jsx world')
ctx.Content = <div><h1>Hello @ there</h1></div>
await next()
})
The reducer is either a simple function or a combination of reducers created with combineReducers
from the redux
package. The reducer is used during the initialisation of the middleware to create a store with createStore(reducer)
. The store is used in rendering as a context for the View container. This way, it's possible to pass data to the template by invoking methods on the Koa's context (see actions).
import { combineReducers } from 'redux'
const title = (state = null, { type, title }) => {
if (type != 'SET_TITLE') return state
return title
}
export default combineReducers({
title,
})
The view can be a connected react-redux
component when actions and a reducer are used, or a pure React
component when they're omitted. It follows the same principles as when developing for a browser-side react-redux
application, so that it accepts the state of the reducer as the first argument, with children
property (set to ctx.Content
).
import { connect } from 'react-redux'
const View = ({ title, children }) => {
return (
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
{children}
</body>
</html>
)
}
export default connect(state => state)(View)
Actions map action creators to Koa's context, so that it is possible to dispatch
actions from ctx
to control the state and thus data which goes into the
template.
const actions = {
setTitle: title => ({ type: 'SET_TITLE', title }),
// ^ exported as ctx.setTitle(title)
}
export default actions
Whether to use static rendering, i.e., without React's metadata required for hydration on the client-side. Set to false
when building universal applications.
Results with static:
<div class="container"><div class="row"><div class="col">test</div></div></div>
and without static:
<div class="container" data-reactroot=""><div class="row"><div class="col">test</div></div></div>
Prettify HTML output. This will use string rendering to get HTML before formatting, therefore it's slower to display a page.
<div class="container" data-reactroot="">
<div class="row">
<div class="col">
<p>Test</p>
</div>
</div>
</div>
It is possible to pass a custom render function. You should implement your own render for more control when needed. It accepts a Koa's context and a WebSite
arguments. The WebSite
is a View
container wrapped in a state provider.
Examples below show how you can implement (a) markup renderer:
import { renderToStaticMarkup } from 'react-dom/server'
import { prettyPrint } from 'html'
const render = (ctx, WebSite) => {
ctx.type = 'html'
ctx.status = 200
ctx.res.write('<!doctype html>\n')
const markup = renderToStaticMarkup(WebSite)
const s = prettyPrint(markup)
ctx.body = s
}
(b) stream renderer:
import { renderToStaticNodeStream } from 'react-dom/server'
const streamRender = (ctx, WebSite) => {
ctx.type = 'html'
ctx.status = 200
ctx.res.write('<!doctype html>\n')
const stream = renderToStaticNodeStream(WebSite)
ctx.body = stream
}
The wireframe provides a reducer
, actions
and View
to be used when creating web pages. It accounts for most common use cases, such as assigning viewport and icons. To include it in your application, use:
import koa2Jsx, { wireframe } from 'koa2-jsx'
const jsx = koa2Jsx(wireframe)
/* or using object destructuring */
const jsx = koa2Jsx({
...wireframe,
pretty: true,
})
The following template is used, which allows to set viewport, title, add links, external scripts and script and style blocks.
<html lang="en">
<head>
<meta charSet="utf-8" />
{viewport &&
<meta name="viewport" content={viewport} />
}
<title>{title}</title>
<!-- css, icons, manifest -->
{links.map((props, i) =>
<link key={i} {...props} />
)}
<!-- CSS -->
{styles.map((style, i) =>
<style key={i} dangerouslySetInnerHTML={{ __html: style }} />
)}
</head>
<body>
{children}
{scripts.map((props, i) =>
<script key={i} {...props} />
)}
{js.map((script, i) =>
<script key={i} dangerouslySetInnerHTML={{ __html: script }} />
)}
</body>
</html>
To update the data to present in the template, the actions API is as follows.
Set title of the page.
ctx.setTitle('koa2-jsx')
<title>koa2-jsx</title>
Set the viewport.
ctx.setViewport('width=device-width, initial-scale=1, shrink-to-fit=no')
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
Add a link to the manifest file.
ctx.addManifest('/manifest.json')
<link rel="manifest" href="/manifest.json" />
Add an icon or icons links.
ctx.addIcon('/icons/favicon.ico')
ctx.addIcon([
[
'/icons/favicon-32x32.png',
'image/png',
'32x32',
],
[
'/icons/apple-icon-180x180.png',
'image/png',
'180x180',
'apple-touch-icon',
],
])
<link href="/icons/favicon.ico" rel="icon" />
<link href="/icons/favicon-32x32.png" type="image/png" sizes="32x32" rel="icon" />
<link href="/icons/apple-icon-180x180.png" type="image/png" sizes="180x180" rel="apple-touch-icon" />
Add a single, or multiple script tags. If integrity and origin need to be used, an array must be passed.
ctx.addScript('/js/bundle.js')
ctx.addScript([
[
'https://code.jquery.com/jquery-3.2.1.slim.min.js',
...(process.env.NODE_ENV == 'production' ? [
'sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN',
'anonymous',
] : []),
],
])
<script src="/js/bundle.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
Add a single, or multiple style links. If integrity and origin need to be specified, an array must be passed.
ctx.addCss('https://fonts.googleapis.com/css?family=Roboto:700&effect=anaglyph|3d-float')
ctx.addCss([
[
'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css',
...(process.env.NODE_ENV == 'production' ? [
'sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm',
'anonymous',
] : []),
],
])
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Roboto:700&effect=anaglyph|3d-float" rel="stylesheet" />
Add a style block to the HTML.
ctx.addStyle('h1 { font-family: \'Roboto\', sans-serif; }')
<style>
h1 { font-family: 'Roboto', sans-serif; }
</style>
Add a block of JS code.
ctx.addJs('$(".alert").alert()')
<script>
$(".alert").alert()
</script>
To include the full Bootstrap 4
support to an HTML page, use the following snippet:
import koa2Jsx, { bootstrap, wireframe } from 'koa2-jsx'
const jsx = koa2Jsx(wireframe)
// ...
app.use(jsx)
app.use(bootstrap)
<!-- viewport is assigned -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<!-- css embedded -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" origin="anonymous" rel="stylesheet" />
<!-- script tags included -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" origin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" origin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" origin="anonymous"></script>
To start using koa2-jsx
, you really need to be able to write JSX
syntax.
During development, use @babel/register
, and for production compile your
project. You can of course create classes with create-react-class
(see reading) and then not require babel transpilation but the
point of this package is to write JSX
which is unfortunately is not native to
Node.JS.
@babel/register
is a good way to develop with koa and koa2-jsx
, however
when using in production, it is recommended to build the app.
require('@babel/register')
require('.')
To build JSX code, you can use the following .babelrc
snippet:
{
"plugins": [
"react-require",
"@babel/plugin-syntax-object-rest-spread",
"@babel/plugin-transform-modules-commonjs"
],
"presets": [
"@babel/preset-react"
]
}
Using with idio
The koa2-jsx
middlware comes with the Koa2-based framework idio
which
apart from other pre-installed middleware such as sessions and Mongo support
out of the box, also provides automatic routes initialisation from a given
folder and hot route reload.
With koa2-jsx
, idio
allows to use JSX with Koa2 for templates. It's
very powerful and can be used in isomorphic applications.
- Redux Server Rendering explains how to render pages with a store server-side, and send initial data to the browser.
- React without ES6 talks about how to use
create-react-class
to create instances of components, that is not usingjsx
syntax.
(c) Art Deco Code 2018