Skip to content

Commit

Permalink
Refactor to use only react context + render props
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkrolick committed Aug 27, 2018
1 parent 3cdf3b2 commit f496afd
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 147 deletions.
107 changes: 85 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

_**alpha project**, API may change significantly_

Interactive documents powered by Markdown, React, and Observables
_0.2.0 does not actually use observables so the name may change 😬_

Interactive documents powered by Markdown, React, ~~and Observables~~

Share state between JSX blocks in a [MDX](https://mdxjs.com/) document

- **Declarative** React automatically updates observers when data changes
- **Write with Markdown** store documents in plain text that can be revision-controlled


<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Examples](#examples)
- [API](#api)
- [Init](#init)
- [State](#state)
- [Using render prop](#using-render-prop)
- [Using context to connect Observe components](#using-context-to-connect-observe-components)
- [Observe](#observe)
- [Alternatives](#alternatives)
- [Roadmap](#roadmap)
Expand All @@ -34,7 +40,8 @@ yarn install
yarn run demo
```

- [Counter](./demo/counter.mdx)
- [Counter w/Observer](./demo/counter.mdx)
- [Counter w/Render Prop](./demo/counter-child-function.mdx)
- [Simple Example](./demo/simple.mdx)
- [Complex Example](./demo/complex.mdx)

Expand All @@ -44,7 +51,7 @@ import { Init, Observe } from 'mdx-observable';

# Counter

<Init state={{ count: 0 }} />
<State initialState={{ count: 0 }}>

<Observe>
{({ setState }) => (
Expand All @@ -57,8 +64,10 @@ import { Init, Observe } from 'mdx-observable';
The button has been clicked:

<Observe>
{ ({state}) => (<span>{state.count} times</span>) }
{ ({...state}) => (<span>{state.count} times</span>) }
</Observe>

</State>
```

Example with a form, table, and graph running in [OK-MDX](https://github.com/jxnblk/ok-mdx):
Expand All @@ -68,44 +77,87 @@ Example with a form, table, and graph running in [OK-MDX](https://github.com/jxn

## API

### Init
### State

Sets initial state for the Observe components.
State container component

Observers cannot render until an Init component is mounted.
Props:

There should only be one Init per page.
- `initialState: Object` - initial state
- `children: React.Children | function` Can either be:
- React children: JSX or Markdown node(s)
- A render prop: a single function that gets called with `{...state, setState}` as the argument

```js
<Init state={{}} />
#### Using render prop

_Very similar to [React Powerplug's State](https://github.com/renatorib/react-powerplug/blob/master/docs/components/State.md)_

_Note: 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_

```mdx
<State initialState={{}}>
{({setState, ...state}) => <React.Fragment>

<h1>Hello, World!</h1>

Some markdown

## Some header

- item a
- item b

</React.Fragment>}
</State>
```

Props:
#### Using context to connect Observe components

```mdx
<State initialState={{}}>

- `state: Object` - initial state
...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, replaceState}) => React.Node`
- `children: ({...state, setState}) => React.Node`
function that accepts an object with:
- `state`: the global state
- `setState`: function like React `setState`, can take an object or an updater function (`state => patch`); result is _shallow merged_ with current state
- `replaceState`: function like Redux reducer; takes an object or an updater function (`state => newState`); result replaces the current state
- the rest of the global state

```js
<Observe>
{({ state, setState, replaceState }) => {
return <div>Something</div>;
{({ setState, ...state }) => {
return <div>{state.something}</div>;
}}
</Observe>

<Observe>
{({ setState, something }) => {
return <div>{something</div>;
}}
</Observe>
```
## Alternatives
### Notebooks
Advantages of MDX-Observable over [Jupyter](https://jupyter.org/) or [ObservableHQ](https://beta.observablehq.com/scratchpad):
- No cells to run; entire document is live
Expand All @@ -115,27 +167,38 @@ Advantages of MDX-Observable over [Jupyter](https://jupyter.org/) or [Observable
- Edit using preferred JS tooling
- Bundle with anything that supports [MDX](https://mdxjs.com/getting-started/), 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 like `const 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
- [ ] See if `<Init />` could work as a wrapper instead of sibling of `<Observer />`. This would allow better scoping and safer setup/teardown.
- [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:
```js
<Init state={} selectors={{ selectCheapest: state => {/* compute */} }} />
<Init state={} selectors={{ selectCheapest: state => {/* compute */} }}>
```
- [ ] Better live-reload support. MDX utils like `ok-mdx` do a full remount when the live editor changes or navigation occures; we could add a `restoreKey` to persist a namespaced cache within the module.
- [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 a `restoreKey` to persist a namespaced cache within the module.
- [ ] **Add tests**
## Potential Issues
### Usage outside MDX
Technically `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.
~~Technically `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.~~
### Warning about blank lines in JSX
Expand Down
19 changes: 10 additions & 9 deletions demo/complex.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Init, Observe } from '../src/index.js';
import { State, Observe } from '../src/index.js';
import Table from './helpers/table';
import { Form } from 'react-powerplug';
import { VictoryBar, VictoryChart, VictoryAxis, VictoryTheme } from 'victory';
Expand All @@ -13,8 +13,8 @@ There a number of factors to consider when buying a car. Use our special formula

Here are some cars to get you started with your comparison:

<Init
state={{
<State
initialState={{
vehicles: [
{ name: "Toyota", price: 29000, reliability: 8, luxury: 7 },
{ name: "Honda", price: 27000, reliability: 7, luxury: 6 },
Expand All @@ -24,15 +24,14 @@ Here are some cars to get you started with your comparison:
{ name: "BMW", price: 45000, reliability: 6, luxury: 10 }
]
}}
key="complex-example"
/>
>

<!--
This is a table
-->

<Observe>
{({ state: {vehicles} }) => (
{({ vehicles }) => (
<Table
data={[Object.keys(vehicles[0]), ...vehicles.map(v => Object.values(v))]}
/>
Expand All @@ -45,7 +44,7 @@ Form to add a car
-->

<Observe>
{({ state: { vehicles }, setState }) => {
{({ vehicles, setState }) => {
return (
<Form initial={{ name: "", price: 0, reliability: 0, luxury: 0 }}>
{({ input, values }) => (
Expand Down Expand Up @@ -93,7 +92,7 @@ Form to add a car

<Observe>
{
({ state: { vehicles }, setState }) => {
({ vehicles, setState }) => {
const cheapest = [...vehicles].sort((a, b) => a.price - b.price)[0];
const mostReliable = [...vehicles].sort(
(a, b) => b.reliability - a.reliability
Expand Down Expand Up @@ -132,7 +131,7 @@ Form to add a car

<Observe>
{
({ state: { vehicles }, setState }) => {
({ vehicles, setState }) => {
const specialFormula = ({ price, reliability, luxury }) =>
(reliability * 3 + luxury * 2) / price;
const byFormula = [...vehicles]
Expand All @@ -157,4 +156,6 @@ Form to add a car
}
</Observe>

</State>

</article>
32 changes: 32 additions & 0 deletions demo/counter-child-function.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { State, Observe } from '../src';

# Counter

<State initialState={{ count: 0 }}>
{({ setState, ...state }) => {
// cannot leave blank lines here
// but at least we can use JS variables because we are inside a function!
const x = 3;
// no blank lines here either
// you can use comments or a single ;
return (
// the body of the document can be put inside the render function
// this has to be wrapped in a fragment if it has multiple nodes
//
// code inside the block cannot be indented >= 4 spaces or it will be
// rendered as text instead of parsed
<React.Fragment>

# H1

<button onClick={() => setState(s => ({ count: s.count + x }))}>
Click me
</button>

The button has been clicked:

<span>{state.count} times</span>

</React.Fragment>
)}}
</State>
11 changes: 5 additions & 6 deletions demo/counter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { Init, Observe } from '../src';

# Counter

<Init
state={{ count: 0 }}
key="counter-example"
/>
<Init initialState={{ count: 0 }}>

<Observe>
{({ setState }) => (
Expand All @@ -18,5 +15,7 @@ import { Init, Observe } from '../src';
The button has been clicked:

<Observe>
{ ({state}) => (<span>{state.count} times</span>) }
</Observe>
{ ({...state}) => (<span>{state.count} times</span>) }
</Observe>

</Init>
15 changes: 8 additions & 7 deletions demo/simple.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Init, Observe } from '../src/index';
import { State, Observe } from '../src/index';
import Table from './helpers/table';

<link href="https://unpkg.com/[email protected]/github-markdown.css" rel="stylesheet" />
Expand All @@ -11,19 +11,18 @@ There a number of factors to consider when buying a car.

Use our special formula to help you decide!

<Init
state={{
<State
initialState={{
vehicles: [
{ name: "Volvo", price: 29000, reliability: 7 },
{ name: "Honda", price: 25000, reliability: 8 },
],
revealChoice: false,
}}
key="simple-example"
/>
>

<Observe>
{({ state: {vehicles} }) => (
{({vehicles}) => (
<Table
data={[Object.keys(vehicles[0]), ...vehicles.map(v => Object.values(v))]}
/>
Expand All @@ -34,7 +33,7 @@ Which should you buy?

<Observe>
{
({state: {vehicles, revealChoice}, setState}) => {
({vehicles, revealChoice, setState}) => {
const mostReliable = [...vehicles]
.sort((a, b) => b.reliability - a.reliability)[0];
const handleClick = e => {
Expand All @@ -53,4 +52,6 @@ Which should you buy?
}
</Observe>

</State>

</article>
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,8 @@
"prettier/prettier": 2
}
},
"dependencies": {
"callbag-from-obs": "^1.2.0",
"callbag-observe": "^1.0.0"
},
"dependencies": {},
"peerDependencies": {
"react": "15.x || 16.x"
"react": "^16.2"
}
}
Loading

0 comments on commit f496afd

Please sign in to comment.