Skip to content

Instantly share code, notes, and snippets.

@developit
Last active February 6, 2021 00:44
Show Gist options
  • Save developit/f603a1a716f75a420929aaac0edc1e78 to your computer and use it in GitHub Desktop.
Save developit/f603a1a716f75a420929aaac0edc1e78 to your computer and use it in GitHub Desktop.
265b lib for building pure functional state machine components. https://github.com/developit/state-machine-component

state-machine-component

A tiny (265 byte) utility to create state machine components using two pure functions.

🔥 JSFiddle Demo

Usage

The API is a single function that accepts 2 pure functions as arguments:

stateMachineComponent(reduce, render)

The first function, reduce(), takes in the current state and applies an action to it, similar to a reducer in Redux:

// Reduce is a redux-style reducer
function reduce(state, action) {
	// actions are like Redux Standard Actions:
	let { type, data, props } = action

	return { }  // just return the new state
}

The second function, render(), is a pure functional component that gets passed the current state instead of props, and a second argument action() - a function that creates a bound dispatcher for the given action type:

// Render is a functional component with little twist
function render(state, action) {
	// action() creates a dispatcher for an action type:
	return <button onClick={ action('TYPE') } />
}

Simple Example: Counter

// Remember:
//  `state` is the current state.
//  `action` is a redux standard action.
function reduce(state, action) {
	switch (action.type) {
		case '@@INIT': return { count: 0 }
		case 'ADD': return { count: state.count+1 }
	}
}

function render(state, action) {
	return (
		<div class="counter">
			Current count: {state.count}
			<button onClick={action('ADD')}>Add 1</button>
		</div>
	)
}

stateMachineComponent(reduce, render)

Full Example: To-Do List

const ToDos = stateMachineComponent(
	// (state, action)
	({ todos, text }, { type, data, props }) => {
		switch (type) {
			case '@@INIT':return { todos: props.todos || [], text: '' };
			case 'ADD': return { todos: todos.concat(text), text: '' };
			case 'TEXT': return { text: data.target.value };
		}
	},
	// state, action(type)
	({ todos, text }, action) => (
		<div>
			<h2>State Machine ToDos</h2>
			<ul>{todos.map( todo => <li>{todo}</li> )}</ul>
			<form onSubmit={action('ADD')}>
				<input value={text} onInput={action('TEXT')} />
			</form>
		</div>
	)
);
node_modules
npm-debug.log
dist
README.md
import { Component } from 'preact';
export default (reducer, render) => {
function Machine() {
Component.call(this);
let cache = {};
let action = type => cache[type] || (cache[type] = data => {
this.setState(reducer(this.state, { type, data, props: this.props }));
});
this.render = (props, state) => render(state, action);
this.componentWillReceiveProps = action('@@PROPS');
this.componentWillMount = action('@@INIT');
}
return (Machine.prototype = new Component()).constructor = Machine;
};
{
"name": "state-machine-component",
"amdName": "stateMachineComponent",
"version": "1.0.2",
"description": "Create tiny pure state machine components.",
"source": "machine.js",
"module": "dist/machine.es.js",
"main": "dist/machine.js",
"umd:main": "dist/machine.umd.js",
"scripts": {
"build": "npm run transpile && npm run size",
"transpile": "rollup -c --environment FORMAT:umd && rollup -c --environment FORMAT:cjs && rollup -c --environment FORMAT:es",
"size": "strip-json-comments --no-whitespace dist/machine.js | gzip-size",
"test": "eslint machine.js",
"prepublish": "npm run build && cp \"*state-machine-component.md\" README.md"
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true
},
"parserOptions": {
"sourceType": "module"
}
},
"files": [
"machine.js",
"dist"
],
"keywords": [
"preact",
"component",
"state machine",
"redux"
],
"author": "Jason Miller <jason@developit.ca>",
"license": "MIT",
"devDependencies": {
"eslint": "^4.6.1",
"gzip-size": "^3.0.0",
"rollup": "^0.49.2",
"rollup-plugin-buble": "^0.15.0",
"rollup-plugin-post-replace": "^1.0.0",
"rollup-plugin-uglify": "^2.0.1",
"strip-json-comments-cli": "^1.0.1",
"uglify-js": "^2.8.29"
}
}
import fs from 'fs';
import buble from 'rollup-plugin-buble';
import uglify from 'rollup-plugin-uglify';
import replace from 'rollup-plugin-post-replace';
let pkg = JSON.parse(fs.readFileSync('./package.json'));
let format = process.env.FORMAT;
export default {
strict: false,
sourcemap: true,
exports: 'default',
input: pkg.source,
output: {
format,
name: pkg.amdName,
file: format==='es' ? pkg.module : format==='umd' ? pkg['umd:main'] : pkg.main
},
external: ['preact'],
globals: {
preact: 'preact'
},
plugins: [
buble(),
format==='cjs' && replace({
'module.exports = index;': '',
'var index =': 'module.exports ='
}),
format==='umd' && replace({
'return index;': '',
'var index =': 'return'
}),
format!=='es' && uglify({
output: { comments: false },
mangle: {
toplevel: format==='cjs'
}
})
]
};
@captDaylight
Copy link

This is really cool! Any thoughts on how async?

@developit
Copy link
Author

not yet no

@sebinsua
Copy link

sebinsua commented Oct 20, 2017

Interesting.

I've been working on something very similar here (conventional-component) . However, my goal is to create a convention and library to create components once for React (or Preact) and then be able to hoist their state into whatever state management solution you wish. I have a working solution atm, but it's early days so I'm still improving the design.

One thing which confuses me about this code is what is the cache doing? Is it some way of avoiding recreating action creators (and therefore re-renders)?

@developit
Copy link
Author

state-machine-component grew up!

https://github.com/developit/state-machine-component

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment