JSX + React ⚛
Framework now supports JSX rendered with React, 🎉 providing a powerful new mechanism for implementing reusable stateful components. You can both write fenced code blocks (```jsx
) and import JSX modules (.jsx
). For example, to define a Greeting
component that accepts a subject
prop:
function Greeting({subject}) {
return <div>Hello, <b>{subject}</b>!</div>
}
You can then call the built-in display
function to render JSX content:
display(<Greeting subject="JSX" />);
The display
function in JSX code blocks uses React’s createRoot
to render the specified contents into the DOM. When you call display
again later, it applies React’s reconciliation algorithm to efficiently update the DOM.
Naturally, you can also use React’s built-in hooks such as useState, useEffect, and useRef. For example, here is a Counter
component whose count you can increment by clicking a button:
function Counter() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
You clicked {count} times
</button>
);
}
React
and ReactDOM
are now available by default in Markdown; if you prefer, you can also import them explicitly from npm:react
or npm:react-dom
.
Span-less inline expression rendering
Framework’s inline expressions allow you to interpolate dynamic content anywhere on the page using ${…}
. Previously, interpolated content was wrapped in a SPAN element; now Framework uses a comment (e.g., <!--:85902a01:-->
) to track where to insert displayed content. This enables a variety of new use cases. For example, you can now use inline expressions to populate the contents of a grid,
<div class="grid grid-cols-4">
${d3.range(4).map((i) => html`<div class="card">Hello ${i}</div>`)}
</div>
… or to generate table rows,
<table>
<thead><th>Index</th></thead>
<tbody>${d3.range(4).map((i) => html`<tr><td>${i}</td></tr>`)}</tbody>
</table>
… or even within SVG elements!
<svg width="640" height="120">
<text x="20" y="20">My favorite number is ${Math.random()}</text>
</svg>
As part of this change, we also fixed several edge cases in incremental updates during preview. (Previously we only tracked changes to top-level elements, but now we also track changes to top-level text nodes and comments.)
More robust inline expression parsing
We’ve also improved how we parse inline expressions: Framework now uses the HTML5 tokenizer algorithm (adopted from Hypertext Literal) to determine context, allowing Framework to ignore inline expressions in unsupported contexts such as attributes, coments, and raw text. This means that if you comment out a chunk of Markdown that includes an inline expression, Framework no longer runs the code (and no longer generates an error)!
<!-- ${"this code doesn’t run"} -->
In the future, we’d like to support interpolation into attributes <a href=${link}>
and possibly raw text <textarea>${1 + 2}</textarea>
; please upvote #32 if you’re interested in this feature.
Framework now also uses Acorn’s tokenizer to determine when an inline expression ends. Previously, Framework counted quotes and curly braces; by adopting Acorn’s tokenizer, Framework can correctly parse (and skip) JavaScript comments within inline expressions. For example, the following inline expression evaluates to 3
:
${1 + /* } */ 2}
Lastly, Framework now correctly handles backslash escaping of inline expressions within HTML blocks. To escape an inline expression, resulting in the literal text ${1 + 2}
, place a backslash \
before either the dollar sign $
or left curly brace {
:
<pre>\${1 + 2}</pre>
To instead show a literal backslash prior to the result of the inline expression \3
, use two backslashes \\
:
<pre>\\${1 + 2}</pre>
Or, to show a literal backslash followed by literal text \${1 + 2}
, use three backslashes:
<pre>\\\${1 + 2}</pre>
Together, these rendering and parsing improvements make Framework’s inline expressions feel more robust — they just work.
Other improvements
The FileAttachment
function now returns a canonical instance: calling FileAttachment
with the same name will return the same object, allowing easier comparison.
FileAttachment("foo.csv") === FileAttachment("foo.csv") // true
The Mod-Enter keyboard shortcut now opens search results in a new tab, making it easier to open multiple search results for the same query.
Sample datasets, such as penguins
and miserables
, are now self-hosted from npm:@observablehq/sample-datasets
, allowing you to work with sample data while offline.
The deploy command now supports the --deploy-config
command-line argument to specify an alternative path to the deploy.json
configuration file. (By default, this file lives in .observablehq/deploy.json
within the source root.) The deploy command now prints a better error message when attempting to deploy without authentication from a non-interactive terminal.
Examples improvements
We’ve added a variety of new technique examples:
loader-julia-to-txt
- Generating TXT from Julialoader-python-to-png
- Generating PNG from Pythonloader-python-to-zip
- Generating ZIP from Pythonloader-r-to-csv
- Generating CSV from Rloader-r-to-jpeg
- Generating JPEG from Rloader-r-to-json
- Generating JSON from Rcodemirror
- A text input powered by CodeMirrorinput-select-file
- Selecting a file from a drop-down menu
The examples are now searchable from the Framework documentation. The example config files have also been greatly simplified by removing shared boilerplate. Lastly, descenders in the hero text of the default template’s home page are no longer clipped.
Thanks @martinswan for contributing to the docs!
Full Changelog: v1.8.0...v1.9.0