MDX-Observable
alpha project, API may change significantly0.2.0 does not actually use observables so the name may change 😬
Interactive documents powered by Markdown, React,
Share state between JSX blocks in a MDX document
- Declarative React automatically updates observers when data changes
- Write with Markdown store documents in plain text that can be revision-controlled
- State
- [Using render prop](#using-render-prop)
- [Using context to connect Observe components](#using-context-to-connect-observe-components)
- Observe
- Notebooks
- Other state management libraries for JS
- Usage outside MDX
- Warning about blank lines in JSXExamples
git clone git@github.com:alexkrolick/mdx-observable.git
cd mdx-observable
yarn install
yarn run demo
// notebook.mdx
import { Init, Observe } from 'mdx-observable';
# Counter
<State initialState={{ count: 0 }}>
<Observe>
{({ setState }) => (
<button onClick={() => setState(s => ({ count: s.count + 1 }))}>
Click me
</button>
)}
</Observe>
The button has been clicked:
<Observe>
{ ({...state}) => (<span>{state.count} times</span>) }
</Observe>
</State>
Example with a form, table, and graph running in OK-MDX:
API
State
State container componentProps:
initialState: Object
- initial statechildren: React.Children | function
Can either be:
{...state, setState}
as the argumentUsing render prop
Very similar to React Powerplug's StateNote: whitespace is sensitive in MDX, so the awkward spacing below is important. This PR may make this easier: https://github.com/mdx-js/mdx/pull/226
<State initialState={{}}>
{({setState, ...state}) => <React.Fragment>
<h1>Hello, World!</h1>
Some markdown
## Some header
- item a
- item b
</React.Fragment>}
</State>
Using context to connect Observe components
<State initialState={{}}>
...child nodes...
<Observe>
{({ ...state}) => <h1>Hello, World!</h1>}
</Observe>
...more child nodes...
</State>
Observe
Component that re-renders when the global state changes.Props:
children: ({...state, setState}) => React.Node
setState
: function like React setState
, can take an object or an updater function (state => patch
); result is shallow merged with current state
- the rest of the global state<Observe>
{({ setState, ...state }) => {
return <div>{state.something}</div>;
}}
</Observe>
<Observe>
{({ setState, something }) => {
return <div>{something</div>;
}}
</Observe>
Alternatives
Notebooks
Advantages of MDX-Observable over Jupyter or ObservableHQ:- No cells to run; entire document is live
- Interactivity powered by predictable one-way data flow
- Use standard JS imports and any React component
- Produces static bundles
- Edit using preferred JS tooling
- Bundle with anything that supports MDX, like Webpack, Gatsby, Parcel, etc.
Other state management libraries for JS
Most state management libraries don't work with MDX because you can't define variables, meaning APIs likeconst myStore = createStore();
are inaccessible. You can work around this by doing this work in another JS file and importing it, but the logic is hard to follow.Some renderless/headless libraries thatwork fully inline are:
- https://github.com/renatorib/react-powerplug
- https://github.com/ianstormtaylor/react-values
However the whitespace sensitivity may make them difficult to use.
Roadmap
- x See if
<Init />
could work as a wrapper instead of sibling of<Observer />
. This would allow better scoping and safer setup/teardown.
- Some way to define functions inline. This might map well to the concept of "selectors" from Redux. Currently you can work around this gap by defining utilities in external JS files, but this makes it hard to write self-contained notebooks.
Possible API:
<Init state={} selectors={{ selectCheapest: state => {/* compute */} }}>
- x Better live-reload support. MDX utils like
ok-mdx
do a full remount when the live editor changes or navigation occures; we could add arestoreKey
to persist a namespaced cache within the module.
Potential Issues
Usage outside MDX
mdx-observable
doesn't depend on MDX for anything, but since it uses a singleton for a cache, it is not a good fit for state management in an app.