Skip to content

Instantly share code, notes, and snippets.

@acdlite
Last active October 7, 2021 17:19
Show Gist options
  • Star 65 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save acdlite/9f1b5883d132ad242323 to your computer and use it in GitHub Desktop.
Save acdlite/9f1b5883d132ad242323 to your computer and use it in GitHub Desktop.
A Redux-like Flux implementation in <75 lines of code
/**
* Basic proof of concept.
* - Hot reloadable
* - Stateless stores
* - Stores and action creators interoperable with Redux.
*/
import React, { Component } from 'react';
export default function dispatch(store, atom, action) {
return store(atom, action);
}
export class Dispatcher extends Component {
static propTypes = {
store: React.PropTypes.func.isRequired
};
static childContextTypes = {
dispatch: React.PropTypes.func,
atom: React.PropTypes.any
};
getChildContext() {
return {
atom: this.state.atom,
dispatch: this.dispatch.bind(this)
};
}
constructor(props, context) {
super(props, context);
this.state = { atom: dispatch(props.store, undefined, {}) };
}
dispatch(payload) {
this.setState(prevState => ({
atom: dispatch(this.props.store, prevState.atom, payload)
}));
}
render() {
return typeof this.props.children === 'function'
? this.props.children(this.state.atom)
: this.props.children;
}
}
export class Injector extends Component {
static contextTypes = {
dispatch: React.PropTypes.func.isRequired,
atom: React.PropTypes.any
};
static propTypes = {
actions: React.PropTypes.object
};
performAction(actionCreator, ...args) {
const { dispatch } = this.context;
const payload = actionCreator(...args);
return typeof payload === 'function'
? payload(dispatch)
: dispatch(payload);
};
render() {
const { dispatch, atom } = this.context;
const { actions: _actions } = this.props;
const actions = Object.keys(_actions).reduce((result, key) => {
result[key] = this.performAction.bind(this, _actions[key]);
return result;
}, {});
return this.props.children({ actions, atom });
}
}
/**
* Example usage
*
* Based on Redux's counter example
* https://github.com/gaearon/redux/tree/master/examples/counter
*/
import React, { Component, PropTypes } from 'react';
import { Dispatcher, Injector } from '../';
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
function counterStore(counter = 0, action) {
switch (action.type) {
case INCREMENT_COUNTER:
return counter + 1;
case DECREMENT_COUNTER:
return counter - 10;
default:
return counter;
}
}
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function decrement() {
return {
type: DECREMENT_COUNTER
};
}
export default class CounterApp {
render() {
return (
<Dispatcher
// Instead of specifying an object of keys mapped to stores, just use a
// higher-order store!
store={(state = {}, action) => ({
counter: counterStore(state.counter, action)
})}
>
{() => (
<Injector actions={{ increment, decrement }}>
{({ actions, atom }) => (
<Counter
increment={actions.increment}
decrement={actions.decrement}
counter={atom.counter}
/>
)}
</Injector>
)}
</Dispatcher>
);
}
}
class Counter {
static propTypes = {
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired
};
render() {
const { increment, decrement, counter } = this.props;
return (
<p>
Clicked: {counter} times
{' '}
<button onClick={increment}>+</button>
{' '}
<button onClick={decrement}>-</button>
</p>
);
}
}
@acdlite
Copy link
Author

acdlite commented Jun 6, 2015

Two key differences from Redux:

  1. Instead of specifying an object of stores, just a single "higher-order store." See Dan's gist about combining stateless stores: https://gist.github.com/gaearon/d77ca812015c0356654f
  2. No subscribing to individual stores; the entire atom is sent on each change. This is simpler for both the user and the library author, and certainly more flexible, IMO. Use container components to pass down more specific parts of the state tree. If you're concerned about performance, that's what shouldComponentUpdate() is for.

@acdlite
Copy link
Author

acdlite commented Jun 6, 2015

Actually I guess "higher-order store" is technically incorrect, since it's not a store that returns another store. Oh well :D

@gaearon
Copy link

gaearon commented Jun 6, 2015

This makes my weekend. 👍

@gaearon
Copy link

gaearon commented Jun 6, 2015

Say we provide compose that takes an array of a map of Stores and combines them into a single Store. Now that would be a higher-order Store :-)

@gaearon
Copy link

gaearon commented Jun 6, 2015

No subscribing to individual stores; the entire atom is sent on each change.

Yeah, I suppose we could do that. But technically I'd still put redux.observe in context instead of atom because context doesn't work well with shouldComponentUpdate currently.

@gaearon
Copy link

gaearon commented Jun 6, 2015

@corlaez
Copy link

corlaez commented May 30, 2019

@acdlite I want to share with you this state management solution. Works well with hooks and it has an amazing typescript support. It is also a single state tree, check it out: https://overmindjs.org

@behnammodi
Copy link

behnammodi commented Jun 7, 2021

@gaearon @acdlite
This is a draft and not completed:
demo: https://codesandbox.io/s/atomic-redux-q9prt

Atomic Redux:

import createAtom from "./atomic-redux";

const incomeAtom = createAtom({
  initialState: 0,
  actions: {
    change: (state, payload) => payload
  }
});

const taxAtom = createAtom({
  initialState: 0.1,
  subscribes: [incomeAtom],
  so: (income) => {
    if (income < 1000) return 0.1;
    if (income < 2000) return 0.15;
    if (income < 3000) return 0.2;
    if (income < 4000) return 0.25;
    return 0.3;
  }
});

export default function App() {
  const [income, incomeActions] = incomeAtom.useHook();
  const [tax] = taxAtom.useHook();

  const handleChange = (event) => incomeActions.change(event.target.value);

  return (
    <div>
      <input value={income} onChange={handleChange} />
      <br />
      Your Income is: {income}
      <br />
      Your Tax is: {tax}
      <br />
    </div>
  );
}

And inside /atomic-redux.js

import { useEffect, useState } from "react";
import { createStore } from "redux";

const PRIVATE_UPDATER = Symbol("PRIVATE_UPDATER");
const noop = () => {};

function createAtom({
  initialState,
  actions = {},
  subscribes = [],
  so = noop
}) {
  actions[PRIVATE_UPDATER] = (_, payload) => payload;

  let {
    getState,
    dispatch,
    subscribe
  } = createStore((state = initialState, { type, payload }) =>
    (actions[type] || (() => state))(state, payload)
  );

  subscribes.forEach((atom) =>
    atom.subscriber({
      dispatch,
      so
    })
  );

  const useHook = () => {
    const [state, setState] = useState(getState());
    useEffect(() => {
      const unsubscribe = subscribe(() => setState(getState()));
      return unsubscribe;
    }, []);
    return [
      state,
      Object.keys(actions).reduce(
        (acc, action) => ({
          ...acc,
          [action]: (payload) => dispatch({ type: action, payload })
        }),
        {}
      )
    ];
  };

  const subscriber = ({ dispatch, so }) =>
    subscribe(() =>
      dispatch({ type: PRIVATE_UPDATER, payload: so(getState()) })
    );

  return { useHook, subscriber };
}

export default createAtom;

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