Skip to content

Instantly share code, notes, and snippets.

@roman01la
Last active September 9, 2017 06:49
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save roman01la/912265347dd5c46b0a2a to your computer and use it in GitHub Desktop.
Save roman01la/912265347dd5c46b0a2a to your computer and use it in GitHub Desktop.
Minimal setup for efficient state management and rendering in React with single atom state
import React from 'react';
import { render } from 'react-dom';
import Root from './components/root.jsx';
import { silentSwap } from './lib/atom';
import { fromJS } from 'immutable';
import './stores/ui';
const initialState = {
ui: {
counter: { count: 0 }
}
};
silentSwap(fromJS(initialState));
render(<Root />, document.getElementById('app'));
let state;
const listeners = {};
export function getState() {
return state;
}
export function notifySwap(query) {
const sQuery = JSON.stringify(query);
Object.entries(listeners)
.forEach(([lQuery, fns]) => {
if (lQuery.startsWith(sQuery)) {
fns.forEach((fn) => fn());
}
});
}
export function swap(nextState, query) {
state = nextState;
notifySwap(query);
return state;
}
export function silentSwap(nextState) {
state = nextState;
}
export function addChangeListener(queries, fn) {
Object.values(queries)
.forEach((query) => {
const sQuery = JSON.stringify(query);
listeners[sQuery] = listeners[sQuery] || [];
listeners[sQuery].push(fn);
});
}
export function removeChangeListener(queries, fn) {
Object.values(queries)
.forEach((query) => {
const sQuery = JSON.stringify(query);
listeners[sQuery] = listeners[sQuery].filter((lfn) => lfn === fn);
});
}
export function assocIn(query, value) {
swap(state.setIn(query, value), query);
}
export function updateIn(query, fn) {
swap(state.updateIn(query, fn), query);
}
export function getIn(query) {
return state.getIn(query);
}
import React from 'react';
import { getIn, addChangeListener, removeChangeListener } from './atom';
import equal from 'deep-equal';
import { partial } from 'fn.js';
import { is } from 'immutable';
function resolveQueries(queries) {
return Object.entries(queries)
.reduce((resolved, [name, query]) => {
resolved[name] = getIn(query);
return resolved;
}, {});
}
function stateEqual(state, nextState) {
return Object.keys(state)
.every((name) => is(state[name], nextState[name]));
}
export default function connect(Component) {
// Get queries into the atom state
const queries = Component.queries;
// Construct state producer function
const getNextState = partial(resolveQueries, queries);
// Data behind the queries is living here
let state = {};
return React.createClass({
// Set name of the component for debugging
displayName: `${Component.displayName}::Connected`,
componentWillMount() {
// Get initial state
state = getNextState();
},
componentDidMount() {
// Re-render when data behind the queries has been changed
addChangeListener(queries, this._update);
},
componentWillReceiveProps(nextProps) {
// Re-render if props has been changed
if (equal(this.props, nextProps) === false) {
this.forceUpdate();
}
},
shouldComponentUpdate() {
// Ignore SCU. Re-render only when props or data behind the queries has been changed
return false;
},
componentWillUnmount() {
removeChangeListener(queries, this._update);
},
_update() {
const nextState = getNextState();
// Re-render if current and next snapshots of data behind the queries are not equal
if (stateEqual(state, nextState) === false) {
state = nextState;
this.forceUpdate();
}
},
render() {
// Pass in both props and data behind the queries
return <Component {...this.props} {...state} />;
}
});
}
const listeners = {};
export function listen(event, fn) {
listeners[event] = listeners[event] || [];
listeners[event].push(fn);
return fn;
}
export function removeListener(event, fn) {
listeners[event] = listeners[event].filter((efn) => efn !== fn);
}
export function emit(event, ...payload) {
listeners[event] && listeners[event].forEach((fn) => fn(...payload));
}
import React from 'react';
import connect from '../lib/connect';
const Root = React.createClass({
statics: {
queries: {
count: ['ui', 'counter', 'count']
}
},
render() {
return <button>{this.props.count}</button>;
}
});
export default connect(Root);
import { updateIn } from '../lib/atom';
import { listen } from '../lib/dispatcher';
import actions from '../config/actions';
import { partial } from 'fn.js';
const s = {
count: ['ui', 'counter', 'count']
};
listen(actions.INC_COUNT, partial(updateIn, s.count, (count) => count + 1));
listen(actions.DEC_COUNT, partial(updateIn, s.count, (count) => count - 1));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment