├── .npmignore ├── .prettierrc ├── .gitignore ├── example ├── src │ ├── store │ │ └── index.js │ ├── actions │ │ └── user.js │ ├── reducers │ │ ├── index.js │ │ └── user.js │ ├── components │ │ ├── footer.js │ │ ├── header.js │ │ ├── main.js │ │ └── user.js │ └── main.js ├── index.html └── package.json ├── src ├── createElement.mjs ├── useContext.mjs ├── useReducer.mjs ├── useState.mjs ├── index.mjs ├── createStore.mjs └── render.mjs ├── .vscode └── launch.json ├── __tests__ └── unit │ ├── dom.jsx │ ├── hooks.js │ └── store.js ├── package.json ├── logo.svg └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn.lock 4 | example/dist 5 | example/dist/bundle.js 6 | lib 7 | .vscode 8 | dist 9 | -------------------------------------------------------------------------------- /example/src/store/index.js: -------------------------------------------------------------------------------- 1 | import createStore from '../../../' 2 | import app from '../reducers' 3 | 4 | const store = createStore(app) 5 | export default store 6 | -------------------------------------------------------------------------------- /example/src/actions/user.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | 3 | export function updateName(name) { 4 | return store.dispatch({ 5 | type: 'UPDATE_NAME', 6 | name, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /example/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import user from './user' 2 | 3 | const app = (state, action) => ({ 4 | user: user(state.user, action), 5 | // ... add more here 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /example/src/components/footer.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import { createElement } from '../../../' 3 | 4 | export default function (props) { 5 | return ( 6 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /example/src/components/header.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import { createElement } from '../../../' 3 | 4 | export default function (props) { 5 | return ( 6 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /example/src/components/main.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import { createElement } from '../../../' 3 | import User from './user' 4 | 5 | export default function (props) { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /example/src/reducers/user.js: -------------------------------------------------------------------------------- 1 | const initialState = { name: 'World' } 2 | const user = (state = initialState, action) => { 3 | switch (action.type) { 4 | case 'UPDATE_NAME': 5 | return { ...state, name: action.name } 6 | default: 7 | return state 8 | } 9 | } 10 | 11 | export default user 12 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/createElement.mjs: -------------------------------------------------------------------------------- 1 | export default function createElement(tagName, props, ...children) { 2 | if (typeof tagName === 'function') { 3 | const Component = tagName 4 | if (Component.prototype) { 5 | const component = new Component({ ...props, children }) 6 | return component.render ? component.render() : component 7 | } else { 8 | return Component(props) 9 | } 10 | } 11 | return { tagName, ...props, children } 12 | } 13 | -------------------------------------------------------------------------------- /example/src/components/user.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import { createElement } from '../../../' 3 | import { updateName } from '../actions/user' 4 | 5 | export default function (props) { 6 | return name({ ...props, onkeyup: (e) => updateName(e.target.value) }) 7 | } 8 | 9 | function name(props) { 10 | return ( 11 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/useContext.mjs: -------------------------------------------------------------------------------- 1 | let state = [] 2 | let cursor = -1 3 | 4 | export default function useContext(context) { 5 | const current = cursor++ 6 | const store = (state[current] = state[current] || context) 7 | return { state: store.getState(), dispatch: store.dispatch.bind(store) } 8 | } 9 | 10 | // internal, only used for tests 11 | useContext.__reset = () => { 12 | state = [] 13 | cursor = [] 14 | useContext.flush() 15 | } 16 | 17 | useContext.flush = () => (cursor = -1) 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Chrome", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceRoot}/src", 10 | "userDataDir": "${workspaceRoot}/.chrome", 11 | "sourceMapPathOverrides": { 12 | "webpack:///src/*": "${webRoot}/*" 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /__tests__/unit/dom.jsx: -------------------------------------------------------------------------------- 1 | const render = require('../../src/render') 2 | xtest('should render a tag with children', (done) => { 3 | const appendChild = jest.fn() 4 | const insertBefore = jest.fn() 5 | 6 | const nav = { tagName: 'nav', children: [] } 7 | const div = { tagName: 'div', className: 'nav', children: [nav] } 8 | const node = { tagName: 'img', appendChild, children: [] } 9 | window.document = global.document = { 10 | createElement: jest.fn(() => ({ appendChild, insertBefore })), 11 | } 12 | 13 | render(div, node) 14 | expect(node.appendChild).to.be.calledOnce 15 | done() 16 | }) 17 | -------------------------------------------------------------------------------- /src/useReducer.mjs: -------------------------------------------------------------------------------- 1 | import createStore from './createStore.mjs' 2 | 3 | let state = [] 4 | let cursor = -1 5 | 6 | export default function useReducer(reducer, initialState) { 7 | const current = cursor++ 8 | const store = (state[current] = 9 | state[current] || initialState.dispatch 10 | ? initialState 11 | : createStore(reducer, initialState)) 12 | return [store.getState(), store.dispatch.bind(store)] 13 | } 14 | 15 | // internal, only used for tests 16 | useReducer.__reset = () => { 17 | state = [] 18 | cursor = [] 19 | useReducer.flush() 20 | } 21 | 22 | useReducer.flush = () => (cursor = -1) 23 | -------------------------------------------------------------------------------- /src/useState.mjs: -------------------------------------------------------------------------------- 1 | let state = [] 2 | let cursor = -1 3 | 4 | export default function useState(initialState) { 5 | const current = cursor++ 6 | const setter = (value) => { 7 | state[current] = typeof value === 'function' ? value() : value 8 | if (useState.dispatch) 9 | Promise.resolve(state[current]).then( 10 | () => useState.flush() && useState.dispatch() 11 | ) 12 | } 13 | return [state[current] || initialState, setter] 14 | } 15 | 16 | // internal, only used for tests 17 | useState.__reset = () => { 18 | state = [] 19 | cursor = [] 20 | useState.flush() 21 | } 22 | 23 | useState.flush = () => (cursor = -1) 24 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | import { createElement, render } from '../../' 3 | import store from './store' 4 | 5 | import Header from './components/header' 6 | import Main from './components/main' 7 | import Footer from './components/footer' 8 | 9 | const main = function (props) { 10 | return ( 11 |
12 |
13 |
14 |
16 | ) 17 | } 18 | 19 | store.subscribe(() => { 20 | const state = store.getState() 21 | const tree = main(state) 22 | render(tree, document.getElementById('root')) 23 | }) 24 | 25 | // start the app 26 | store.dispatch({}) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pureact", 3 | "version": "1.4.7", 4 | "author": "Christian Landgren", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/irony/pureact" 9 | }, 10 | "type": "module", 11 | "source": "src/index.mjs", 12 | "main": "./dist/index.cjs", 13 | "module": "./dist/index.module.js", 14 | "unpkg": "./dist/index.umd.js", 15 | "exports": "./dist/index.modern.mjs", 16 | "dependencies": { 17 | "snabbdom": "3.5.1" 18 | }, 19 | "scripts": { 20 | "build": "npx microbundle src/*", 21 | "test": "npx jest", 22 | "watch": "npx jest --watch", 23 | "prepublishOnly": "npm run build" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "author": "Christian Landgren", 5 | "license": "MIT", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "babel": "^6.5.2", 9 | "babelify": "^7.3.0", 10 | "watchify": "^3.9.0" 11 | }, 12 | "peerDependencies": { 13 | "browserify": "^13.0.1", 14 | "mkdirp": "^0.5.1", 15 | "watchify": "^3.7.0" 16 | }, 17 | "browserify": { 18 | "transform": [ 19 | "babelify" 20 | ] 21 | }, 22 | "scripts": { 23 | "prebrowserify": "mkdirp dist", 24 | "browserify": "browserify src/main.js -t babelify --outfile dist/bundle.js", 25 | "watchify": "watchify src/main.js -t [ babelify --sourceMapsAbsolute ] --debug -o dist/bundle.js", 26 | "start": "npm install && npm run browserify && echo 'OPEN index.html IN YOUR BROWSER'" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | export { default as render } from './render.mjs' 2 | export { default as createElement } from './createElement.mjs' 3 | export { default as createStore } from './createStore.mjs' 4 | export { default as useState } from './useState.mjs' 5 | export { default as useReducer } from './useReducer.mjs' 6 | export { default as useContext } from './useContext.mjs' 7 | 8 | import render from './render.mjs' 9 | import createElement from './createElement.mjs' 10 | import createStore from './createStore.mjs' 11 | import useState from './useState.mjs' 12 | import useReducer from './useReducer.mjs' 13 | import useContext from './useContext.mjs' 14 | 15 | export function Component(props) { 16 | this.props = props 17 | this.setState = () => { 18 | throw new Error('Unsupported. Use hooks/useState instead') 19 | } 20 | } 21 | 22 | export default { 23 | render, 24 | createElement, 25 | createStore, 26 | useState, 27 | useReducer, 28 | useContext, 29 | Component, 30 | } 31 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/createStore.mjs: -------------------------------------------------------------------------------- 1 | import useState from './useState.mjs' 2 | 3 | export default function createStore(reducer, initialState) { 4 | let state = initialState || {} 5 | let promisedState = state 6 | const listeners = [] 7 | const store = { 8 | getState: () => state, 9 | dispatch: (action) => { 10 | // for thunks 11 | if (typeof action === 'function') 12 | return action(store.dispatch, store.getState) 13 | // for promises 14 | if (Promise.resolve(action) === action) 15 | return Promise.resolve(action).then(store.dispatch) 16 | promisedState = Promise.resolve(promisedState).then((state) => 17 | reducer(state, action || {}) 18 | ) 19 | return Promise.resolve(promisedState).then((result) => { 20 | state = result 21 | listeners.forEach((listener, i) => 22 | listener(() => { 23 | delete listeners[i] 24 | }) 25 | ) 26 | return (state = result) 27 | }) 28 | }, 29 | subscribe: (callback) => listeners.push(callback), 30 | } 31 | if (initialState) setTimeout(store.dispatch) 32 | 33 | // connect hooks to store 34 | useState.dispatch = store.dispatch 35 | return store 36 | } 37 | -------------------------------------------------------------------------------- /src/render.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | init, 3 | classModule, 4 | propsModule, 5 | styleModule, 6 | attributesModule, 7 | eventListenersModule, 8 | h, 9 | } from 'snabbdom' 10 | 11 | const patch = init([ 12 | // Init patch function with chosen modules 13 | classModule, // makes it easy to toggle classes 14 | propsModule, // for setting properties on DOM elements 15 | styleModule, // handles styling on elements with support for animations 16 | attributesModule, // for setting attributes on DOM elements 17 | eventListenersModule, // attaches event listeners 18 | ]) 19 | 20 | const omit = (o, fields) => 21 | Object.keys(o).reduce( 22 | (a, b) => (!fields.includes(b) ? Object.assign(a, { [b]: o[b] }) : a), 23 | {} 24 | ) 25 | const shadowRoot = (child) => h('span', {}, child.map(deflate)) // Should be replaced with reference to parent instead? 26 | const deflate = (child) => 27 | child 28 | ? Array.isArray(child) 29 | ? shadowRoot(child) 30 | : child.tagName 31 | ? vtree(child) 32 | : child 33 | : '' 34 | function vtree(tree) { 35 | const props = omit(tree, ['element', 'children', 'style', 'tagName']) 36 | const children = tree.children && tree.children.map(deflate) 37 | return h( 38 | tree.tagName, 39 | { 40 | props, 41 | style: tree.style, 42 | attrs: props.attrs || (props.properties || {}).attributes, 43 | }, 44 | children 45 | ) 46 | } 47 | 48 | export default function render(tree, node, oldTree) { 49 | const newTree = vtree(tree) 50 | if (oldTree) { 51 | patch(oldTree, newTree) 52 | } else { 53 | patch(node, newTree) 54 | } 55 | return newTree 56 | } 57 | -------------------------------------------------------------------------------- /__tests__/unit/hooks.js: -------------------------------------------------------------------------------- 1 | import { useState, useReducer, createStore } from '../../src/index.mjs' 2 | 3 | describe('hooks', () => { 4 | describe('useState', () => { 5 | beforeEach(() => { 6 | useState.__reset() 7 | }) 8 | test('initializer', () => { 9 | expect(useState).not.toBeFalsy() 10 | }) 11 | 12 | test('defaultValue', () => { 13 | const [value, method] = useState(11) 14 | expect(method).toHaveProperty('apply') 15 | expect(value).toEqual(11) 16 | }) 17 | 18 | test('updater works', () => { 19 | { 20 | const [size, setSize] = useState(0) 21 | expect(size).toEqual(0) 22 | expect(setSize).toHaveProperty('apply') 23 | setSize(1337) 24 | } 25 | useState.flush() 26 | { 27 | const [size, setSize] = useState(0) 28 | expect(setSize).toHaveProperty('apply') 29 | expect(size).toEqual(1337) 30 | } 31 | }) 32 | 33 | test('works with thunks as updater', () => { 34 | { 35 | const [size, setSize] = useState(0) 36 | expect(size).toEqual(0) 37 | expect(setSize).toHaveProperty('apply') 38 | setSize(() => 1337) 39 | } 40 | useState.flush() 41 | { 42 | const [size, setSize] = useState(0) 43 | expect(setSize).toHaveProperty('apply') 44 | expect(size).toEqual(1337) 45 | } 46 | }) 47 | 48 | test('two states in parallel works', () => { 49 | { 50 | const [count, setCount] = useState(43) 51 | const [visits, setVisits] = useState(111) 52 | expect(count).toEqual(43) 53 | setCount(1337) 54 | setVisits(visits + 1) 55 | } 56 | useState.flush() 57 | { 58 | const [count, setCount] = useState(43) 59 | const [visits, setVisits] = useState(111) 60 | expect(setCount).toHaveProperty('apply') 61 | expect(setVisits).toHaveProperty('apply') 62 | expect(count).toEqual(1337) 63 | expect(visits).toEqual(112) 64 | } 65 | }) 66 | 67 | test('dispatches a new render to store', (done) => { 68 | const [count, setCount] = useState(43) 69 | const store = createStore(() => {}) 70 | expect(count).toEqual(43) 71 | setCount(1337) 72 | store.subscribe(() => { 73 | const [count] = useState(43) 74 | expect(count).toEqual(1337) 75 | done() 76 | }) 77 | }) 78 | 79 | xtest('works with promises when working with store', (done) => { 80 | const [size, setSize] = useState(12) 81 | expect(size).toEqual(12) 82 | const store = createStore(() => {}) 83 | setSize(Promise.resolve(1337)) 84 | store.subscribe(() => { 85 | const [size, setSize] = useState(12) 86 | expect(size).toEqual(1337) 87 | done() 88 | }) 89 | }) 90 | }) 91 | 92 | describe('useReducer', () => { 93 | test('useReducer exists', () => { 94 | expect(useReducer).not.toBeFalsy() 95 | }) 96 | 97 | it('returns an array with inital state as first item', () => { 98 | const [state] = useReducer(() => {}, { foo: 'bar' }) 99 | expect(state).toHaveProperty('foo') 100 | expect(state.foo).toEqual('bar') 101 | }) 102 | 103 | it('returns an dispatcher', () => { 104 | const [state, dispatch] = useReducer(() => {}, { foo: 'bar' }) 105 | expect(state).toHaveProperty('foo') 106 | expect(dispatch).toHaveProperty('apply') 107 | expect(dispatch.name).toEqual('bound dispatch') 108 | }) 109 | 110 | describe('dispatcher', () => { 111 | beforeEach(() => { 112 | useReducer.__reset() 113 | }) 114 | 115 | it('it calls the reducer when being called', (done) => { 116 | const add = jest.fn((state, action) => ({ 117 | count: state.count + action.number, 118 | })) 119 | const [state, dispatch] = useReducer(add, { count: 0 }) 120 | expect(state).toEqual({ count: 0 }) 121 | dispatch({ number: 1 }).then((state) => { 122 | expect(add).toBeCalledWith({ count: 0 }, { number: 1 }) 123 | expect(state).toEqual({ count: 1 }) 124 | done() 125 | }) 126 | }) 127 | 128 | it('it works in a more complex example', (done) => { 129 | const initialState = { count: 0 } 130 | function reducer(state, action) { 131 | switch (action.type) { 132 | case 'reset': 133 | return { ...action.payload } 134 | case 'increment': 135 | return { count: state.count + 1 } 136 | case 'decrement': 137 | return { count: state.count - 1 } 138 | default: 139 | return state 140 | } 141 | } 142 | const [state, dispatch] = useReducer(reducer, initialState) 143 | expect(state.count).toEqual(0) 144 | 145 | dispatch({ type: 'reset', payload: initialState }) 146 | dispatch({ type: 'increment' }) 147 | dispatch({ type: 'increment' }) 148 | dispatch({ type: 'decrement' }).then((state) => { 149 | expect(state).toEqual({ count: 1 }) 150 | done() 151 | }) 152 | }) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /__tests__/unit/store.js: -------------------------------------------------------------------------------- 1 | import createStore from '../../src/createStore.mjs' 2 | 3 | describe('store', () => { 4 | describe('with reducer', function () { 5 | const reducer = jest.fn(() => ({ name: 'foo' })) 6 | const store = createStore(reducer) 7 | 8 | test('create store', (done) => { 9 | store.subscribe(() => { 10 | expect(store.getState()).toHaveProperty('name') 11 | done() 12 | }) 13 | store.dispatch() 14 | }) 15 | }) 16 | 17 | describe('with multiple reducers', function () { 18 | const user = jest.fn(() => ({ name: 'foo' })) 19 | const did = jest.fn(() => ({ what: 'bar' })) 20 | const reducer = (state, action) => ({ user: user(), did: did() }) 21 | 22 | test('create store', (done) => { 23 | const store = createStore(reducer) 24 | store.subscribe(() => { 25 | expect(store.getState()).toHaveProperty('user') 26 | expect(store.getState().user).toHaveProperty('name') 27 | expect(store.getState().user.name).toEqual('foo') 28 | done() 29 | }) 30 | store.dispatch() 31 | }) 32 | }) 33 | 34 | describe('dispatch', function () { 35 | const reducer = jest.fn(() => ({ name: 'foo' })) 36 | const store = createStore(reducer) 37 | 38 | test('dispatches initial data', (done) => { 39 | reducer.mockReturnValue({ name: 'christian' }) 40 | store.dispatch({ type: 'EMPTY' }) 41 | store.subscribe(() => { 42 | expect(store.getState()).toHaveProperty('name') 43 | expect(store.getState()).not.toHaveProperty('type') 44 | expect(store.getState().name).toEqual('christian') 45 | done() 46 | }) 47 | }) 48 | 49 | test('dispatches new data', (done) => { 50 | const reducer = (state, action) => { 51 | switch (action.type) { 52 | case 'LEAVING': 53 | return { 54 | name: action.name, 55 | last: `${action.did} the ${action.what}`, 56 | } 57 | } 58 | return state 59 | } 60 | const store = createStore(reducer) 61 | const action = { 62 | type: 'LEAVING', 63 | name: 'elvis', 64 | did: 'left', 65 | what: 'building', 66 | } 67 | store.dispatch(action) 68 | store.subscribe(() => { 69 | try { 70 | expect(store.getState().name).toEqual('elvis') 71 | expect(store.getState().last).toEqual('left the building') 72 | done() 73 | } catch (err) { 74 | done(err) 75 | } 76 | }) 77 | }) 78 | 79 | test('supports thunks', (done) => { 80 | const reducer = (state, action) => { 81 | switch (action.type) { 82 | case 'LEAVING': 83 | return { 84 | name: action.name, 85 | last: `${action.did} the ${action.what}`, 86 | } 87 | } 88 | return state 89 | } 90 | const store = createStore(reducer) 91 | const action = (dispatch) => 92 | dispatch({ 93 | type: 'LEAVING', 94 | name: 'elvis', 95 | did: 'left', 96 | what: 'building', 97 | }) 98 | store.subscribe(() => { 99 | expect(store.getState().name).toEqual('elvis') 100 | expect(store.getState().last).toEqual('left the building') 101 | done() 102 | }) 103 | store.dispatch(action) 104 | }) 105 | 106 | test('supports promises in actions', (done) => { 107 | const reducer = (state, action) => { 108 | switch (action.type) { 109 | case 'LEAVING': 110 | return { 111 | name: action.name, 112 | last: `${action.did} the ${action.what}`, 113 | } 114 | } 115 | return state 116 | } 117 | const store = createStore(reducer) 118 | const action = new Promise((resolve) => 119 | resolve({ 120 | type: 'LEAVING', 121 | name: 'elvis', 122 | did: 'left', 123 | what: 'building', 124 | }) 125 | ) 126 | store.subscribe(() => { 127 | expect(store.getState().name).toEqual('elvis') 128 | expect(store.getState().last).toEqual('left the building') 129 | done() 130 | }) 131 | store.dispatch(action) 132 | }) 133 | 134 | test('supports promises in state', (done) => { 135 | const reducer = (state, action) => { 136 | switch (action.type) { 137 | case 'LEAVING': 138 | return Promise.resolve({ 139 | name: action.name, 140 | last: `${action.did} the ${action.what}`, 141 | }) 142 | default: 143 | return state 144 | } 145 | } 146 | const store = createStore(reducer) 147 | store.subscribe(() => { 148 | try { 149 | expect(store.getState().name).toEqual('elvis') 150 | expect(store.getState().last).toEqual('left the building') 151 | done() 152 | } catch (err) { 153 | done(err) 154 | } 155 | }) 156 | const action = { 157 | type: 'LEAVING', 158 | name: 'elvis', 159 | did: 'left', 160 | what: 'building', 161 | } 162 | store.dispatch(action) 163 | }) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pureact - a tiny and pure alternative to React 2 | 3 | This is very small implementation of the idea of React+Redux with a very light weight approach. The result is a small lib (~150 lines of code, 6kb incl dependencies gzipped) and superfast (based on snabbdom) with batteries included (a minmal version of Redux+Hooks). 4 | 5 | For small projects you don't want to spend time on upgrading a huge dependency tree when you just have a few JSX components. Pureact has a minimal implementation where you can solve the basic needs. Render a couple of components, updating state and fetching data from external API:s. In a few years when you want to update your code you will not need to go through a huge list of dependencies - Pureact only has one dependency. 6 | 7 | ## Get started 8 | 9 | This is a very stratight forward starting point. Just create two files: index.html and index.js: 10 | 11 | index.html: 12 | ```html 13 | 14 | ``` 15 | 16 | index.js: 17 | ```javascript 18 | import React, { render } from 'pureact' 19 | const state = { user: 'John' } 20 | const App = (props) =>

Hi {props.user}

21 | render(, document.body) 22 | ``` 23 | 24 | ```bash 25 | $> parcel index.html 26 | ``` 27 | 28 | ## Demo 29 | 30 | - Mandatkollen, built with pureact: https://mandatkollen.se (code: https://github.com/iteam1337/mandatkollen) 31 | - Todo app, code examples: https://pureact-todo.irony.now.sh (code: https://github.com/irony/pureact-todo ) 32 | 33 | ## Install Pureact 34 | 35 | npm i pureact 36 | npm i -g parcel 37 | 38 | ## Add pureact pragma in .babelrc 39 | 40 | "plugins": [ 41 | ["transform-react-jsx", { "pragma": "Pureact.createElement" }] 42 | ] 43 | 44 | ## Start coding 45 | 46 | Then define your app with pure functions: 47 | 48 | ```javascript 49 | const props = {name} 50 | const App = (props) =>

Hi {props.user}

51 | Pureact.render(, document.body)) 52 | ``` 53 | 54 | ## Run and ship it 55 | 56 | // starts dev server and listens to changes 57 | parcel index.html 58 | 59 | // package 60 | parcel build index.html // 16kb 61 | 62 | ## Also pure components 63 | 64 | ...or using components with a pure render function. Only render method is supported, no other lifetime or state methods are implemented (intentional to keep the pure fashion) 65 | 66 | ```javascript 67 | import Pureact, { Component } from 'pureact' 68 | import logo from './logo.svg' 69 | import './App.css' 70 | 71 | class App extends Component { 72 | render() { 73 | return ( 74 |
75 |
76 | logo 77 |

Welcome to React {this.props.name}!

78 |
79 |

80 | To get started, edit src/App.js and save to reload. 81 |

82 |
83 | ) 84 | } 85 | } 86 | 87 | export default App 88 | ``` 89 | 90 | ## A lightweight redux-compatible store is also included 91 | 92 | ```javascript 93 | import Pureact, { createStore } from 'pureact' 94 | 95 | const reducer = (state, action) => ({ 96 | ...state, 97 | name: action.name, // naive example 98 | }) 99 | 100 | const store = createStore(reducer) 101 | ``` 102 | 103 | Plug it in in your render lifecycle: 104 | 105 | ```javascript 106 | const App = (props) =>

{props.name}

107 | let oldTree 108 | 109 | store.subscribe(() => { 110 | const state = store.getState() 111 | oldTree = Pureact.render(, document.body, oldTree) 112 | }) 113 | ``` 114 | 115 | To dispatch events, just use the dispatcher 116 | 117 | ```javascript 118 | store.dispatch({ 119 | type: 'UPDATE_NAME', 120 | name, 121 | }) 122 | ``` 123 | 124 | Note that both reducers and actions can be asyncronous (!) 125 | 126 | ```javascript 127 | const reducer = async (state, action) => ({ 128 | user: await user(state.user, action) 129 | }) 130 | 131 | store.dispatch(() => fetch('/user').then(user => ({ type: 'UPDATE_USER', user})) 132 | ``` 133 | 134 | (both promises and thunks are supported) 135 | 136 | ## Hooks are also included (beta - only works for non-lists right now) 137 | 138 | ```javascript 139 | import React, { useState } from 'pureact' 140 | 141 | const Name = (props) => { 142 | const [name, setName] = useState('') 143 | return ( 144 |
145 | setName(e.target.value)} 149 | /> 150 |
151 | ) 152 | } 153 | ``` 154 | 155 | ## Motivation 156 | 157 | - React is a great idea but has become bloated 158 | - Redux is a great idea but should have been included 159 | - Pure functions are a great way of describing components 160 | - For small projects you don't want 1000s of dependencies to update each time 161 | 162 | ## Current state 163 | 164 | The lib has been used in production for four years without any problems. With the latest development in React which moves in the same direction (pure functions and state/hooks included you start to wonder why not just use 66 lines of code instead of thousands? 165 | 166 | Let me know if you miss anything important. Either send a pull request or issue. I'm going to try to keep this lib as tiny as possible. 167 | 168 | ## License 169 | 170 | MIT, © Copyright 2023 Christian Landgren 171 | --------------------------------------------------------------------------------