{"id":17349804,"url":"https://github.com/stasm/innerself","last_synced_at":"2025-04-13T02:17:08.649Z","repository":{"id":57274218,"uuid":"102408713","full_name":"stasm/innerself","owner":"stasm","description":"◘ A tiny view + state management solution using innerHTML","archived":false,"fork":false,"pushed_at":"2018-08-25T14:51:09.000Z","size":5942,"stargazers_count":778,"open_issues_count":5,"forks_count":28,"subscribers_count":24,"default_branch":"master","last_synced_at":"2025-04-13T02:17:04.242Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stasm.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-04T22:45:36.000Z","updated_at":"2025-01-30T15:45:50.000Z","dependencies_parsed_at":"2022-09-17T10:12:49.415Z","dependency_job_id":null,"html_url":"https://github.com/stasm/innerself","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stasm%2Finnerself","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stasm%2Finnerself/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stasm%2Finnerself/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stasm%2Finnerself/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stasm","download_url":"https://codeload.github.com/stasm/innerself/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654104,"owners_count":21140237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T16:57:10.639Z","updated_at":"2025-04-13T02:17:08.624Z","avatar_url":"https://github.com/stasm.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"#\"\u003e\n    \u003cimg width=\"192\" height=\"192\" src=\"https://raw.githubusercontent.com/stasm/innerself/master/logo/logo.png\" /\u003e\n  \u003c/a\u003e\n  \u003ch1\u003einnerself\u003c/h1\u003e\n  \u003cp\u003e\n    \u003ca href=\"https://travis-ci.org/stasm/innerself\"\u003e\n      \u003cimg alt=\"Build Status\" src=\"https://travis-ci.org/stasm/innerself.svg?branch=master\" /\u003e\n    \u003c/a\u003e\n  \u003c/p\u003e\n  \u003cp\u003eA tiny view + state management solution using \u003ccode\u003einnerHTML\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\n\n[`innerHTML` is fast][quirksmode].  It's not fast enough if you're a Fortune 500 company\nor even if your app has more than just a handful of views.  But it might be\njust fast enough for you if you care about code size.\n\nI wrote _innerself_ because I needed to make sense of the UI for a game I wrote\nfor the [js13kGames][] jam.  The whole game had to fit into 13KB.  I needed\nsomething extremely small which would not make me lose sanity.  _innerself_\nclocks in at under 50 lines of code.  That's around 600 bytes minified, ~350\ngzipped.\n\n_innerself_ is inspired by React and Redux.  It offers the following familiar\nconcepts:\n\n  - composable components,\n  - a single store,\n  - a `dispatch` function,\n  - reducers,\n  - and even an optional logging middleware for debugging!\n\nIt does all of this by serializing your component tree to a string and\nassigning it to `innerHTML` of a root element.  It even imitates Virtual DOM\ndiffing by comparing last known output of components with the new one :)\nI know this sounds like I'm crazy but it actually works quite nice for small\nand simple UIs.\n\nIf you don't care about size constraints, _innerself_ might not be for you.\nReal frameworks like React have much more to offer, don’t sacrifice safety,\naccessibility, nor performance, and you probably won’t notice their size\nfootprint.\n\n_innerself_ was a fun weekend project for me.  Let me know what you think!\n\n[Live demo]: https://stasm.github.io/innerself/example01/\n[quirksmode]: https://www.quirksmode.org/dom/innerhtml.html\n[js13kGames]: http://js13kgames.com/\n\n\n## Caveats\n\nYou need to know a few things before you jump right in.  _innerself_ is\na less-than-serious pet project and I don't recommend using it in production.\n\nIt's a poor choice for form-heavy UIs.  It tries to avoid unnecessary\nre-renders, but they still happen if the DOM needs even a tiniest update.  Your\nform elements will keep losing focus because every re-render is essentially\na new assignment to the root element's `innerHTML`.\n\nWhen dealing with user input in serious scenarios, any use of `innerHTML`\nrequires sanitization.  _innerself_ doesn't do anything to protect you or your\nusers from XSS attacks.  If you allow keyboard input or display data fetched\nfrom a database, please take special care to secure your app.  The\n`innerself/sanitize` module provides a rudimentary sanitization function.\n\nPerhaps the best use-case for _innerself_ are simple mouse-only UIs with no\nkeyboard input at all :)\n\n\n## Showcase\n\n  - [A moment lost in time.][moment-lost] - a first-person exploration puzzle\n    game by [@michalbe][] and myself.  I originally wrote _innerself_ for this.\n  - [Innerself Hacker News Clone][innerself-hn] - a Hacker News single page app by [@bsouthga][] with\n   _innerself_ as the only dependency. Also serves as an example of a [TypeScript][typescript] _innerself_ app.\n  - [Reach/Steal Draft Tracker][reach-steal] - a fantasy football draft tracker by [@bcruddy][] that tests the rendering performance with 300+ table rows backed by an [expressjs][] server.\n  - [TodoMVC][todomvc-innerself] - a [TodoMVC][todomvc] app based on _innerself_ by [@Cweili][@cweili].\n\n\n[moment-lost]: https://github.com/piesku/moment-lost\n[@michalbe]: https://github.com/michalbe\n[innerself-hn]: https://github.com/bsouthga/innerself-hn\n[@bsouthga]: https://github.com/bsouthga\n[typescript]: https://github.com/Microsoft/TypeScript\n[reach-steal]: https://github.com/bcruddy/reach-steal\n[@bcruddy]: https://github.com/bcruddy\n[expressjs]: https://github.com/expressjs/express\n[todomvc-innerself]: https://codepen.io/Cweili/pen/ZXOeQa\n[todomvc]: http://todomvc.com/\n[@cweili]: https://github.com/Cweili\n\n\n## Install\n\n    $ npm install innerself\n\nFor a more structured approach [@bsouthga][] created [innerself-app][]. Use it\nto bootstrap new _innerself_ apps from a predefined template.\n\n[innerself-app]: https://github.com/bsouthga/innerself-app\n\n\n## Usage\n\n_innerself_ expects you to build a serialized version of your DOM which will\nthen be assigned to `innerHTML` of a root element.  The `html` helper allows\nyou to easily interpolate Arrays.\n\n```javascript\nimport html from \"innerself\";\nimport ActiveTask from \"./ActiveTask\";\n\nexport default function ActiveList(tasks) {\n    return html`\n        \u003ch2\u003eMy Active Tasks\u003c/h2\u003e\n        \u003cul\u003e\n            ${tasks.map(ActiveTask)}\n        \u003c/ul\u003e\n    `;\n}\n```\n\nThe state of your app lives in a store, which you create by passing the reducer\nfunction to `createStore`:\n\n```javascript\nconst { attach, connect, dispatch } = createStore(reducer);\nwindow.dispatch = dispatch;\nexport { attach, connect };\n```\n\nYou need to make `dispatch` available globally in one way or another.  You can\nrename it, namespace it or put it on a DOM Element. The reason why it needs to\nbe global is that the entire structure of your app must be serializable to\nstring at all times.  This includes event handlers, too.\n\n```javascript\nimport html from \"innerself\";\n\nexport default function ActiveTask(text, index) {\n    return html`\n        \u003cli\u003e\n            ${text} ${index}\n            \u003cbutton\n                onclick=\"dispatch('COMPLETE_TASK', ${index})\"\u003e\n                Mark As Done\u003c/button\u003e\n        \u003c/li\u003e\n    `;\n}\n```\n\nYou can put any JavaScript into the `on\u003cevent\u003e` attributes. [The browser will\nwrap it in a function][mdn-event] which takes the `event` as the first argument\n(in most cases) and in which `this` refers to the DOM Element on which the\nevent has been registered.\n\n[mdn-event]: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers#Event_handler's_parameters_this_binding_and_the_return_value\n\nThe `dispatch` function takes an action name and a variable number of\narguments.  They are passed to the reducer which should return a new version of\nthe state.\n\n```javascript\nconst init = {\n    tasks: [],\n    archive: []\n};\n\nexport default function reducer(state = init, action, args) {\n    switch (action) {\n        case \"ADD_TASK\": {\n            const {tasks} = state;\n            const [value] = args;\n            return Object.assign({}, state, {\n                tasks: [...tasks, value],\n            });\n        }\n        case \"COMPLETE_TASK\": {\n            const {tasks, archive} = state;\n            const [index] = args;\n            const task = tasks[index];\n            return Object.assign({}, state, {\n                tasks: [\n                    ...tasks.slice(0, index),\n                    ...tasks.slice(index + 1)\n                ],\n                archive: [...archive, task]\n            });\n        }\n        default:\n            return state;\n    }\n}\n```\n\nIf you need side-effects, you have three choices:\n\n  - Put them right in the `on\u003cevent\u003e` attributes.\n  - Expose global action creators.\n  - Put them in the reducer.  (This is considered a bad practice in Redux\n    because it makes the reducer unpredictable and harder to test.)\n\nThe `dispatch` function will also re-render the entire top-level component if\nthe state changes require it.  In order to be able to do so, it needs to know\nwhere in the DOM to put the `innerHTML` the top-level component generated.\nThis is what `attach` returned by `createStore` is for:\n\n```javascript\nimport { attach } from \"./store\";\nimport App from \"./App\";\n\nattach(App, document.querySelector(\"#root\"));\n```\n\n`createStore` also returns a `connect` function.  Use it to avoid passing data\nfrom top-level components down to its children where it makes sense.  In the\nfirst snippet above, `ActiveList` receives a `tasks` argument which must be\npassed by the top-level component.\n\nInstead you can do this:\n\n```javascript\nimport html from \"innerself\";\nimport { connect } from \"./store\";\nimport ActiveTask from \"./ActiveTask\";\nimport TaskInput from \"./TaskInput\";\n\nfunction ActiveList(state) {\n    const { tasks } = state;\n    return html`\n        \u003ch2\u003eMy Active Tasks\u003c/h2\u003e\n        \u003cul\u003e\n            ${tasks.map(ActiveTask)}\n            \u003cli\u003e\n                ${TaskInput()}\n            \u003c/li\u003e\n        \u003c/ul\u003e\n    `;\n}\n\nexport default connect(ActiveList);\n```\n\nYou can then avoid passing the state explicitly in the top-level component:\n\n```javascript\n\nimport html from \"innerself\";\nimport { connect } from \"./store\";\n\nimport ActiveList from \"./ActiveList\";\nimport ArchivedList from \"./ArchivedList\";\n\nexport default function App(tasks) {\n    return html`\n        ${ActiveList()}\n        ${ArchivedList()}\n    `;\n}\n```\n\nConnected components always receive the current state as their first argument,\nand then any other arguments passed explicitly by the parent.\n\n\n## Logging Middleware\n\n_innerself_ comes with an optional helper middleware which prints state\nchanges to the console.  To use it, simply decorate your reducer with the\ndefault export of the `innerself/logger` module:\n\n```javascript\nimport { createStore } from \"innerself\";\nimport withLogger from \"innerself/logger\";\nimport reducer from \"./reducer\"\n\nconst { attach, connect, dispatch } =\n    createStore(withLogger(reducer));\n```\n\n\n## Crazy, huh?\n\nI know, I know.  But it works!  Check out the examples:\n\n  - [example01][] - an obligatory Todo App.\n  - [example02][] by @flynnham.\n  - [example03][] illustrates limitations of _innerself_ when dealing with text\n    inputs and how to work around them.\n\n[example01]: https://stasm.github.io/innerself/example01/\n[example02]: https://stasm.github.io/innerself/example02/\n[example03]: https://stasm.github.io/innerself/example03/\n\n\n## How It Works\n\nThe update cycle starts with the `dispatch` function which passes the action to\nthe reducer and updates the state.\n\nWhen the state changes, the store [compares the entire string output][diff] of\ntop-level components (the ones attached to a root element in the DOM) with the\noutput they produced last. This means that most of the time, even a slightest\nchange in output will re-render the entire root.\n\nIt's possible to dispatch actions which change the state and don't trigger\nre-renders. For instance in `example01` the text input dispatches\n`CHANGE_INPUT` actions on `keyup` events. The current value of the input is\nthen saved in the store. Crucially, this value is not used by the `TaskInput`\ncomponent to populate the input element. The whole thing relies on the fact\nthat the native HTML input element stores its own state when the user is typing\ninto it.\n\nThis limitation was fine for my use-case but it's worth pointing out that it\nbadly hurts accessibility. Any change to the state which causes a re-render\nwill make the currently focused element lose focus.\n\nReact is of course much smarter: the Virtual DOM is a lightweight\nrepresentation of the render tree and updates to components produce an actual\ndiff. React maps the items in the Virtual DOM to the elements in the real DOM\nand is able to only update what has really changed, regardless of its position\nin the tree.\n\nHere's an interesting piece of trivia that I learned about while working on\nthis project. React only re-renders components when their local state changes,\nas signaled by `this.setState()`. The fact that it also looks like components\nre-render when their props change derives from that as well. Something needs to\npass those props in, after all, and this something is the parent component\nwhich first needs to decide to re-render itself.\n\nWhen you think about how you can `connect` components with _react-redux_ to\navoid passing state to them from parents it becomes clear why behind the scenes\nit calls [`this.setState(dummyState)`][dummy] (which is an empty object) to\ntrigger a re-render of the connected component :) It does this only when the\nsub-state as described by the selector (`mapStateToProps`) changes, which is\neasy to compute (and fast) if the reducers use immutability right. In the best\ncase scenario it only needs to compare the identity of the sub-state to know\nthat it's changed.\n\n[diff]: https://github.com/stasm/innerself/blob/7aa2e6857fd05cc7047dcd3bbdda6d3820b76f42/index.js#L20-L27\n[dummy]: https://github.com/reactjs/react-redux/blob/fd81f1812c2420aa72805b61f1d06754cb5bfb43/src/components/connectAdvanced.js#L218\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstasm%2Finnerself","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstasm%2Finnerself","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstasm%2Finnerself/lists"}