Cyano
Takes a base component and returns a Mithril or React component. This is useful if you want to support both Mithril and React with a minimum of duplicate code.
- Online examples
- Getting started
- Usage
- API
- Additional setup when aliasing "cyano"
- Compatibility
- Sizes
- License
Online examples
Getting started
Usage of Cyano requires setting an alias setting in your bundler configuration. Read on for details.
Install
With Mithril:
npm install cyano-mithril
With React:
npm install cyano-react
Import
There are 2 methods for importing Cyano in your code. Which one to choose depends on your goal.
Writing applications
When writing an application, it is possible to use identical code for both Mithril and React. This method uses an import from "cyano" which the bundler will resolve to either the Mithril or React version.
See: Import for applications (aliasing "cyano")
Writing libraries
When writing a library, you don't want your library users to burden with a special Cyano setup. Imports need to be resolved after bundling.
This means that Mithril and React components are created in separate files, which pass Cyano methods to shared core component code.
See: Import for libraries (passing Cyano methods)
Usage
Write a single codebase, deploy twice
The basic idea behind Cyano is to make code portable to both Mithril and React.
Perhaps you are a library author who wants to have more users benefit from your solution. You can write once and deploy twice.
Or you are introducing Mithril in a company that is inclined to use React. By supporting both libraries you can hold out a decision until the team has had some more experience.
Cyano lets you create Mithril and React components from shared base components. To make these base components interoperable, they need to understand a shared language.
Shared language
Hooks instead of lifecycle methods
Base components are "functional components" - render functions without lifecycle/class methods.
React Hooks are a replacement for functionality that was previously available only in (React) classes. By replacing class lifecycles with hooks, code becomes more succinct and easier to reason about. Hooks make it trivial to define logic outside of the component and to import parts where needed. Local state and lifecycle methods can be implemented using hooks with little effort.
- React Hooks have been introduced in React 16.8.
- For Mithril hooks are implemented with helper library mithril-hooks.
Hyperscript or JSX
Base components can be written in hyperscript or JSX.
If you choose hyperscript: React Without JSX demonstrates how to use React.createElement
to write component code. Cyano uses an enhanced version by means of react-hyperscript, which is more lenient in omitting properties and keys.
If you choose JSX, see Configuring JSX how to setup JSX rendering. JSX can be used for both React and Mithril.
Dictionary of HTML attributes
React follows the camelCase convention for "official" HTML attributes, whereas Mithril uses lowercase names. Helper variable a
maps the lowercase attribute name to an accepted one.
For example:
a.onclick
returns "onclick" for Mithril and "onClick" for React.
Helper functions
Variable | Description | API doc |
---|---|---|
cast |
Takes a base component and returns a Mithril or React component. | cast |
h |
The render function for hyperscript. | h (render function) |
h.trust |
Function to insert HTML | Inserting trusted content |
h.fragment |
Function to wrap elements in a fragment | Inserting trusted content |
getRef |
Callback function that gets a reference to the DOM element. | getRef |
a |
Dictionary of accepted HTML attributes. | a (Accepted HTML attributes) |
jsx |
Babel pragma, import this when writing JSX. | jsx |
Using hooks
Base components have access to default hooks (see "supported hooks" below) and custom hooks.
import { h, a, useState, useEffect } from "cyano"
const SharedCounter = ({ initialCount }) => {
const [count, setCount] = useState(initialCount)
useEffect(
() => {
// ...
},
[count] // Only re-run when value has changed
)
return (
h("div",
[
h(".count",
{ key: "count" },
count
),
h("button",
{
key: "increment",
[a.onclick]: () => setCount(count + 1),
},
"More"
),
]
)
)
}
Supported hooks
useState
useEffect
useLayoutEffect
useReducer
useRef
useMemo
useCallback
These React hooks make little sense with Mithril and are not included:
useContext
useImperativeHandle
useDebugValue
Custom hooks
General introduction in React's documentation: Building Your Own Hooks
Using hooks with Mithril: see mithril-hooks for examples.
Importing options
Import for applications (aliasing "cyano")
NOTE: This methods is not recommended for writing libraries. See: Import for libraries (passing Cyano methods) for the alternative approach.
The "aliasing" method uses an import from "cyano" which the bundler will resolve to either the Mithril or React version.
import { cast /*, useState, h, a, etcetera */ } from "cyano"
Our bundler will resolve "cyano" to either cyano-react
or cyano-mithril
.
Configuring
We need to let the bundler point from "cyano" to cyano-react
or cyano-mithril
. See instructions for bundlers below:
Code example
Due to our agnostic (aliased) import from "cyano", the component code for React and Mithril is identical.
Hyperscript
import { cast, useState, h, a } from "cyano"
const _Toggle = ({ title }) => {
const [clicked, setClicked] = useState(false)
return h(".toggle", [
h("h2", title),
h("button",
{
[a.onclick]: () => setClicked(!clicked)
},
"Toggle"
),
h("div",
clicked ? "On" : "Off"
)
])
}
const Toggle = cast(_Toggle)
// Use:
h(Toggle, { title: "Switch!" })
JSX
The same code written in JSX. See Configuring JSX how to setup JSX rendering.
import { cast, useState, a, jsx } from "cyano"
/* jsx needs to be in scope for JSX to work */
const _Toggle = () => {
const [clicked, setClicked] = useState(false)
return (
<div className="toggle">
<h2>{title}</h2>
<div
className={`button ${clicked ? "is-info" : ""}`}
{...{
[a.onclick]: () => setClicked(!clicked)
}}>
Toggle
</div>
<div className="info">
{clicked ? "On" : "Off"}
</div>
</div>
)
}
const Toggle = cast(_Toggle)
// Use:
<Toggle title="Switch!" />
Note the HTML attribute "onclick" that must be passed in the properties object, because we can't use dynamic keys for JSX attributes.
Passing or nesting components
Example: a Navigation component that contains Link components.
Either convert the Link before using:
import { cast, h, a } from "cyano"
const _Link = () => {
// ...
}
const Link = cast(_Link)
const _Navigation = () => [
h(Link, { label: "Home", path: "/"} ),
h(Link, { label: "Contact", path: "/contact"} ),
]
const Navigation = cast(_Navigation)
Or pass the converted Link as initial parameter to Navigation:
import { cast, h, a } from "cyano"
const _Link = () => {
// ...
}
const _Navigation = ({ Link }) => [
h(Link, { label: "Home", path: "/"} ),
h(Link, { label: "Contact", path: "/contact"} ),
]
const Link = cast(_Link)
const Navigation = cast(_Navigation, { Link })
Import for libraries (passing Cyano methods)
Mithril and React components are created in separate files, which pass Cyano methods to shared core component code.
// react-button
import { _Button } from "core-button"
import { Icon } from "react-icon"
import { cast, h, a, useState, useEffect, useRef, getRef } from "cyano-react"
export const Button = cast(_Button, { h, a, getRef, useState, useEffect, useRef, Icon })
// core-button
export const _Button = ({ h, a, getRef, useState, useEffect, useRef, Icon, ...props }) => {
// etcetera
}
Solving issues
Issues with keys
This case may trip you up:
[
h("div",
{ key: 1 }
),
[2,3,4].map(n => // Error when Mithril runs this code
h("div",
{ key: n },
n
)
)
]
Mithril will throw an error because not all children in the array have a key.
The reason is that React and Mithril handle arrays differently: React flattens inner arrays, but Mithril doesn't.
This code ends up processed this way for each:
[
h("div", { key: 1 }),
[
h("div", { key: 2 })
]
]
In React:
// Everything's flattened, so everything's keyed as expected
[
h("div", { key: 1 }),
h("div", { key: 2 }),
]
In Mithril:
h.fragment([
h("div", { key: 1 }),
h.fragment([ // Lacks a key, so an error is thrown
h("div", { key: 2 }),
]),
])
To solve this in a cross-platform way, rewrite the tripping-up code with a keyed wrapper fragment:
[
h("div",
{ key: 1 }
),
h.fragment(
{ key: "numbers" },
[2,3,4].map(n =>
h("div",
{ key: n },
n
)
)
)
]
API
cast
Takes a base component and returns a Mithril or React component.
Signature
cast(renderFunction, initialProps?) => Component
Argument | Type | Required | Description |
---|---|---|---|
renderFunction |
Function component | Yes | The base/common/shared functional component to be converted for Mithril or React |
initialProps |
Object | No | Any variable to pass to renderFunction ; see also Passing or nesting components
|
Returns | Component |
The returned Component
can be called as any component:
h(Component, {
// component props
})
The component render function that is called will receive a combined object of initialProps
and component props, plus property children:
const _Component = ({ defaultTitle, title, children )} => {
// ...
}
const Component = cast(_Component, { defaultTitle: "a blue sky" })
h(Component,
{ title: "casting a shadow" },
h("div", "Cloud child")
)
h (render function)
The render function for hyperscript.
Signature
h(selector, properties?, children?) => Element
Argument | Type | Required | Description |
---|---|---|---|
selector |
`String | Object` | Yes |
properties |
Object |
No | HTML attributes or element properties |
children |
`Array<Vnode | ReactElement> | String |
Returns |
Vnode (for Mithril); ReactElement for React |
Inserting trusted content
Mithril's API contains m.trust that "turns an HTML or SVG string into unescaped HTML or SVG". The documentation continues with the warning
Do not use m.trust on unsanitized user input. Always try to use an alternative method first, before considering using m.trust.
With this caveat, it is sometimes useful to insert a piece of HTML or SVG into a container.
The render function h
is enhanced with method trust
to do that:
const iconBack = h.trust("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z\"/></svg>")
cyano-react
uses dangerouslySetInnerHTML
to set trusted content with a "div" as wrapper element. The element tag can be set with the second parameter.
For consistency, cyano-mithril
function h.trust
is enhanced with this second parameter too.
Signature
h.trust(html, wrapper) => Element
Argument | Type | Required | Default | Description |
---|---|---|---|---|
html |
String |
Yes | A string containing HTML or SVG text. | |
wrapper |
Element tag name | No |
cyano-react : "div"; cyano-mithril : undefined |
Wrapper element |
Returns |
Vnode (for Mithril); ReactElement for React |
Wrapping fragments
Both Mithril and React have a concept called "fragments" to group children in a list:
- Mitril: m.fragment
- React: Fragment
There are key differences between both libraries:
- React's
Fragment
only supports propertykey
, but does some smart handling of inserting extra HTML elements (see the doc example for table rows) - Mithrils
m.fragment
supports lifecycle methods in properties, but does not do any magic with HTML elements
Cyano's bridge function h.fragment
is oblivious to either library, so you could make use of either implementation, but for cross-platform code you'd need to stick to property key
and make no assumptions on handling HTML elements around the fragment.
Signature
h.fragment(properties?, children) => Element
Argument | Type | Required | Default | Description |
---|---|---|---|---|
properties |
object |
No | For cross-library code: only use key
|
|
children |
`Array<Vnode | ReactElement> | String | Number |
Returns |
Vnode (for Mithril); ReactElement for React |
a (HTML attributes)
Dictionary of accepted HTML attributes.
a
maps the lowercase attribute name to an accepted one. Instead of onClick
(for React) or (onclick
(for Mithril) you write:
[a.onclick]: () => setClicked(!clicked)
or in JSX:
{...{
[a.onclick]: () => setClicked(!clicked)
}}
Complete list of included html attributes:
getRef
Callback function getRef
gets a reference to the DOM element.
Signature
getRef(fn)
Argument | Type | Required | Description |
---|---|---|---|
fn |
Function |
Yes | Function that receives the DOM reference. |
Returns | function ref(dom) { /* use dom */} |
getRef
accepts a function that receives the DOM reference.
DOM reference are commonly stored in a useRef
object, with property current
to store the data. Note that setting or updating useRef
will not cause a re-render.
import { useRef, h, a, getRef } from "cyano"
const _Component = () => {
const domRef = useRef()
return (
h("div",
{
...getRef(dom => {
// React will call this each render
domRef.current = domRef.current || dom;
})
},
"My component"
)
)
}
The example above contains a check to prevent superfluous updates to the variable domRef
.
Forward refs
When getRef
is passed to a component, the component props will contain the function ref
. This can be used from the parent:
// Input.js
h("input",
{
...getRef((dom: Element) => {
if (dom && !domElement) {
setDomElement(dom);
if (props.ref) {
// pass back to parent
props.ref(dom);
}
}
}),
}
)
// parent that uses Input
h(Input, {
ref: dom => (
setDomElement(dom)
)
})
Under the hood
-
cyano-mithril
:-
getRef
uses Mithril's lifecycle methodoncreate
to access the DOM element. This method will be called once.
-
-
cyano-react
:-
getRef
uses React'sref
method to access the DOM element. This method will be called on each render. When the element is removed from the DOM,getRef
will receivenull
. Using an update check as shown above will prevent that the reference will be lost.
-
Children
Child elements are accessed through the component prop children
:
const _Component = ({ children }) => {
return [
h("h2", "My title"),
children
]
}
jsx
Babel pragma. Only import this when writing JSX.
Additional setup when aliasing "cyano"
NOTE: This setup is only needed when using identical code for both Mithril and React. This can be used in applications but is not recommended for writing libraries.
Bundler configuration
We need to let the bundler point from "cyano" to cyano-react
or cyano-mithril
.
Each bundler has a different method to to this - it is generally called "map" or "alias".
Environment variable setup
The examples below show how to direct from "cyano" to "cyano-mithril". When you are using the same bundler scripts for both Mithril and React, you should consider to configure the alias, for instance by using an environment variable.
Example (using Webpack):
- Setting the environment variable
- Reading the environment variable
- Using the environment variable to set an alias
Configuring Webpack
// webpack.config.js
const path = require("path");
const baseDir = process.cwd();
Then add to the configuration:
resolve: {
alias: {
"cyano": path.resolve(baseDir, "node_modules/cyano-mithril"),
}
}
Configuring Rollup
// rollup.config.js
import path from "path";
import pathmodify from "rollup-plugin-pathmodify";
const baseDir = process.cwd();
Then add to plugins:
pathmodify({
aliases: [
{
id: "cyano",
resolveTo: path.resolve(baseDir, "node_modules/cyano-mithril/dist/cyano-mithril.module.js"),
},
]
})
Configuring Browserify
Use the pathmodify plugin to change the default config path to your custom file:
const path = require("path");
const pathmodify = require('pathmodify');
const baseDir = process.cwd();
Then add to browserify()
:
.plugin(pathmodify, {
mods: [
pathmodify.mod.id("cyano", path.resolve(baseDir, "node_modules/cyano-mithril")),
]
})
// ...
Configuring JSX
This is an optional step.
Install @babel/plugin-transform-react-jsx
and add to the plugins
in your Babel configuration:
["@babel/plugin-transform-react-jsx", {
"pragma": "jsx"
}]
jsx
is a variable exported by Cyano. This needs to be in scope when using JSX in component code (but does not need to be called explicitly):
import { jsx } from "cyano"
React and Webpack
It may be necessary to point Webpack to the React module. Add an entry to resolve.alias
in the config:
resolve: {
alias: {
"react": path.resolve(baseDir, "node_modules/react"),
},
},
Compatibility
- Mithril: tested with Mithril 1.1.6 and Mithril 2.x
- React: tested with React 16.8.4 and higher
Sizes
cyano-mithril
Includes mithril-hooks
┌───────────────────────────────────────────┐
│ │
│ Bundle Name: cyano-mithril.module.js │
│ Bundle Size: 3.72 KB │
│ Minified Size: 2.93 KB │
│ Gzipped Size: 1.14 KB │
│ │
└───────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ │
│ Bundle Name: cyano-mithril.umd.js │
│ Bundle Size: 5.15 KB │
│ Minified Size: 3.56 KB │
│ Gzipped Size: 1.35 KB │
│ │
└────────────────────────────────────────┘
┌─────────────────────────────────────┐
│ │
│ Bundle Name: cyano-mithril.cjs │
│ Bundle Size: 4.4 KB │
│ Minified Size: 3.53 KB │
│ Gzipped Size: 1.26 KB │
│ │
└─────────────────────────────────────┘
cyano-react
Includes react-hyperscript
┌─────────────────────────────────────────┐
│ │
│ Bundle Name: cyano-react.module.js │
│ Bundle Size: 6.28 KB │
│ Minified Size: 4.27 KB │
│ Gzipped Size: 1.92 KB │
│ │
└─────────────────────────────────────────┘
┌──────────────────────────────────────┐
│ │
│ Bundle Name: cyano-react.umd.js │
│ Bundle Size: 7.83 KB │
│ Minified Size: 4.65 KB │
│ Gzipped Size: 2.03 KB │
│ │
└──────────────────────────────────────┘
┌───────────────────────────────────┐
│ │
│ Bundle Name: cyano-react.cjs │
│ Bundle Size: 6.93 KB │
│ Minified Size: 4.85 KB │
│ Gzipped Size: 2.03 KB │
│ │
└───────────────────────────────────┘
License
MIT