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>
);
}
}
@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