├── .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 |
7 | Footer
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/example/src/components/header.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 | import { createElement } from '../../../'
3 |
4 | export default function (props) {
5 | return (
6 |
7 | Logo
8 | Hello {props.user.name}
9 |
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 |
12 | Your Name:
13 |
14 |
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 |
15 |
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 |
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 |
--------------------------------------------------------------------------------