Skip to content

Commit

Permalink
Add How It Works to the README
Browse files Browse the repository at this point in the history
  • Loading branch information
stasm committed Sep 8, 2017
1 parent 7aa2e68 commit ff368c9
Showing 1 changed file with 84 additions and 31 deletions.
115 changes: 84 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# innerself

A tiny view + state management solution using innerHTML. [Live demo][].
A tiny view + state management solution using `innerHTML`. [Live demo][].

[innerHTML is fast][1]. It's not fast enough if you're a Fortune 500 company
[`innerHTML` is fast][1]. It's not fast enough if you're a Fortune 500 company
or even if your app has more than just a handful of views. But it might be
just fast enough for you if you care about code size.

I wrote `innerself` because I needed to make sense of the UI for a game I wrote
I wrote _innerself_ because I needed to make sense of the UI for a game I wrote
for the [js13kGames][2] jam. The whole game had to fit into 13KB. I needed
something extremely small which would not make me lose sanity. `innerself`
clocks in at under 40 lines of code. That's around 400 bytes minified, ~250
something extremely small which would not make me lose sanity. _innerself_
clocks in at under 50 lines of code. That's around 600 bytes minified, ~350
gzipped.

`innerself` is inspired by React and Redux. It offers the following familiar
_innerself_ is inspired by React and Redux. It offers the following familiar
concepts:

- composable components,
Expand All @@ -22,14 +22,17 @@ concepts:
- and even an optional logging middleware for debugging!

It does all of this by serializing your component tree to a string and
assigning it to `innerHTML` of a root element. I know this sounds like I'm
crazy but it actually works quite nice for small and simple UIs.
assigning it to `innerHTML` of a root element. It even imitates Virtual DOM
diffing by comparing last known output of components with the new one :)
I know this sounds like I'm crazy but it actually works quite nice for small
and simple UIs.

If you don't care about size constraints, `innerself` might not be for you.
Real frameworks like React have much more to offer, don't sacrifice ease of use
nor performance, and you probably won't notice their size footprint.
If you don't care about size constraints, _innerself_ might not be for you.
Real frameworks like React have much more to offer, don’t sacrifice safety,
accessibility, nor performance, and you probably won’t notice their size
footprint.

`innerself` was a fun weekend project for me. Let me know what you think!
_innerself_ was a fun weekend project for me. Let me know what you think!

[Live demo]: https://stasm.github.io/innerself/example01/
[1]: https://www.quirksmode.org/dom/innerhtml.html
Expand All @@ -43,25 +46,27 @@ nor performance, and you probably won't notice their size footprint.

## Caveats

You need to know a few things before you jump right in. `innerself` is a poor
choice for form-heavy UIs. It tries to avoid unnecesary re-renders, but they
still happen if the DOM needs even a tiniest update. Your form elements will
keep losing focus because every re-render is essentially a new assignment to
the root element's `innerHTML`.
You need to know a few things before you jump right in. _innerself_ is
a less-than-serious pet project and I don't recommend using it in production.

It's a poor choice for form-heavy UIs. It tries to avoid unnecessary
re-renders, but they still happen if the DOM needs even a tiniest update. Your
form elements will keep losing focus because every re-render is essentially
a new assignment to the root element's `innerHTML`.

When dealing with user input in serious scenarios, any use of `innerHTML`
requires sanitization. `innerself` doesn't do anything to protect you or your
requires sanitization. _innerself_ doesn't do anything to protect you or your
users from XSS attacks. If you allow keyboard input or display data fetched
from a database, please take special care to secure your app. The
`innerself/sanitize` module provides a rudimentary sanitization function.

Perhaps the best use-case for `innerself` are simple mouse-only UIs with no
Perhaps the best use-case for _innerself_ are simple mouse-only UIs with no
keyboard input at all :)


## Usage

`innerself` expects you to build a serialized version of your DOM which will
_innerself_ expects you to build a serialized version of your DOM which will
then be assigned to `innerHTML` of a root element. The `html` helper allows
you to easily interpolate Arrays.

Expand Down Expand Up @@ -159,10 +164,10 @@ If you need side-effects, you have three choices:
- Put them in the reducer. (This is considered a bad practice in Redux
because it makes the reducer unpredictable and harder to test.)

The `dispatch` function will also re-render the entire top-level component. In
order to be able to do so, it needs to know where in the DOM to put the
`innerHTML` the top-level component generated. This is what `attach` returned
by `createStore` is for:
The `dispatch` function will also re-render the entire top-level component if
the state changes require it. In order to be able to do so, it needs to know
where in the DOM to put the `innerHTML` the top-level component generated.
This is what `attach` returned by `createStore` is for:

```javascript
import { attach } from "./store";
Expand Down Expand Up @@ -222,15 +227,9 @@ Connected components always receive the current state as their first argument,
and then any other arguments passed explicitly by the parent.


## Crazy, huh?

I know, I know. But it works! Check out the example in `example01/` and at
https://stasm.github.io/innerself/example01/.


## Logging Middleware

`innerself` comes with an optional helper middleware which prints state
_innerself_ comes with an optional helper middleware which prints state
changes to the console. To use it, simply decorate your reducer with the
default export of the `innerself/logger` module:

Expand All @@ -242,3 +241,57 @@ import reducer from "./reducer"
const { attach, connect, dispatch } =
createStore(withLogger(reducer));
```


## Crazy, huh?

I know, I know. But it works! Check out the source in `example01/` and at
https://stasm.github.io/innerself/example01/.


## How It Works

The update cycle starts with the `dispatch` function which passes the action to
the reducer and updates the state.

When the state changes, the store [compares the entire string output][diff] of
top-level components (the ones attached to a root element in the DOM) with the
output they produced last. This means that most of the time, even a slightest
change in output will re-render the entire root.

It's possible to dispatch actions which change the state and don't trigger
re-renders. For instance in `example01` the text input dispatches
`CHANGE_INPUT` actions on `keyup` events. The current value of the input is
then saved in the store. Crucially, this value is not used by the `TaskInput`
component to populate the input element. The whole thing relies on the fact
that the native HTML input element stores its own state when the user is typing
into it.

This limitation was fine for my use-case but it's worth pointing out that it
badly hurts accessibility. Any change to the state which causes a re-render
will make the currently focused element lose focus.

React is of course much smarter: the Virtual DOM is a lightweight
representation of the render tree and updates to components produce an actual
diff. React maps the items in the Virtual DOM to the elements in the real DOM
and is able to only update what has really changed, regardless of its position
in the tree.

Here's an interesting piece of trivia that I learned about while working on
this project. React only re-renders components when their local state changes,
as signaled by `this.setState()`. The fact that it also looks like components
re-render when their props change derives from that as well. Something needs to
pass those props in, after all, and this something is the parent component
which first needs to decide to re-render itself.

When you think about how you can `connect` components with _react-redux_ to
avoid passing state to them from parents it becomes clear why behind the scenes
it calls [`this.setState(dummyState)`][dummy] (which is an empty object) to
trigger a re-render of the connected component :) It does this only when the
sub-state as described by the selector (`mapStateToProps`) changes, which is
easy to compute (and fast) if the reducers use immutability right. In the best
case scenario it only needs to compare the identity of the sub-state to know
that it's changed.

[diff]: https://github.com/stasm/innerself/blob/7aa2e6857fd05cc7047dcd3bbdda6d3820b76f42/index.js#L20-L27
[dummy]: https://github.com/reactjs/react-redux/blob/fd81f1812c2420aa72805b61f1d06754cb5bfb43/src/components/connectAdvanced.js#L218

0 comments on commit ff368c9

Please sign in to comment.