Skip to content

Instantly share code, notes, and snippets.

@michowski
Last active October 29, 2018 10:14
Show Gist options
  • Save michowski/ff444cf7e2b7c0ca2bef5446a65a698f to your computer and use it in GitHub Desktop.
Save michowski/ff444cf7e2b7c0ca2bef5446a65a698f to your computer and use it in GitHub Desktop.
Like Redux, but without any boilerplate.
import { Component, ComponentType } from 'react';
import getKeys from 'lib/getKeys';
import mapValues from 'lodash/mapValues';
export type Update<S> = (s: S) => S;
export type Intent<S, P> = (p: P) => Update<S>;
export type Intents<K extends string, S, P, I extends Intent<S, P>> = {
[k in K]: I
};
export type Updates<K extends string, S, U extends Update<S>> = { [k in K]: U };
export type Action<P> = (p: P) => void;
export type DumbAction = () => void;
export type ActionFromIntent<SomeIntent> = SomeIntent extends Intent<
any,
infer P
>
? Action<P>
: any;
export type ActionsFromIntents<SomeIntents> = SomeIntents extends Intents<
infer K,
any,
any,
infer I
>
? { [k in K]: ActionFromIntent<I> }
: any;
export type ActionsFromUpdates<SomeUpdates> = SomeUpdates extends Updates<
infer K,
any,
any
>
? { [k in K]: DumbAction }
: any;
export const createStore = <S extends object>() => {
type Listener = ((state: S) => void);
let state: S | undefined = undefined;
let listeners: Listener[] = [];
const subscribe = (listener: Listener) => {
const index = listeners.length;
listeners.push(listener);
return () => {
listeners.splice(index, 1);
};
};
const setState = (newState: S) => {
state = newState;
listeners.forEach(sub => {
if (sub) {
sub(newState);
}
});
};
const getState = () => state;
const replaceState = (update: Update<S>) => {
if (!state) {
return;
}
setState(update(state));
};
class Provider extends Component<{ initState: S }> {
componentDidMount() {
setState(this.props.initState);
this.forceUpdate();
}
render() {
if (!state) {
return null;
}
return this.props.children;
}
}
function connect<Props extends object, OwnProps extends object>(
selector: ((state: S) => Props)
): ((Comp: ComponentType<Props & OwnProps>) => ComponentType<OwnProps>);
function connect<
Props extends object,
OwnProps extends object,
UK extends string,
U extends Update<S>
>(
selector: ((state: S) => Props),
updates: Updates<UK, S, U>
): ((
Comp: ComponentType<Props & OwnProps & { [k in UK]: DumbAction }>
) => ComponentType<OwnProps>);
function connect<
Props extends object,
OwnProps extends object,
UK extends string,
U extends Update<S>,
IK extends string,
P,
I extends Intent<S, P>
>(
selector: ((state: S) => Props),
updates?: Updates<UK, S, U>,
intents?: Intents<IK, S, P, I>
) {
type ComponentProps = Props &
OwnProps &
{ [k in UK]: DumbAction } &
{ [k in IK]: Action<P> };
const actionsFromUpdates =
updates &&
mapValues(updates, <U extends Update<S>>(update: U) => () =>
replaceState(update)
);
const actionsFromIntents =
intents &&
mapValues(intents, <Payload extends P>(intent: Intent<S, Payload>) => {
return (p: Payload) => replaceState(intent(p));
});
return (Comp: ComponentType<ComponentProps>): ComponentType<OwnProps> => {
class Connected extends Component<OwnProps> {
unsubscribe: any;
propsFromState = state ? selector(state) : undefined;
componentDidMount() {
this.unsubscribe = subscribe(newState => {
const newProps = selector(newState);
if (
!this.propsFromState ||
getKeys(newProps).some(
k => newProps[k] !== (this.propsFromState as Props)[k]
)
) {
this.propsFromState = newProps;
this.forceUpdate();
}
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
if (!this.propsFromState) {
return null;
}
const allProps: ComponentProps = Object.assign(
{},
this.props,
this.propsFromState,
actionsFromUpdates,
actionsFromIntents
);
return <Comp {...allProps} />;
}
}
return Connected;
};
}
return { connect, setState, getState, Provider };
};
// It's almost the same as with Redux
export interface TodosProps extends
ActionsFromUpdates<typeof updates>,
ActionsFromIntents<typeof intents> {
todos: Todo[],
}
const Todos: SFC<TodosProps> = ({
todos,
removeAllTodos,
addTodo,
limitTodos
}) => {
return (
<div>
<ul>
{todos.map(todo => <li>{todo.title}</li>)}
</ul>
<button onClick={() => removeAllTodos()}>Remove all todos</button>
<button onClick={() => addTodo('foo')}>Add foo todo</button>
<button onClick={() => addTodo('bar')}>Add bar todo</button>
<button onClick={() => limitTodos(3)}>Show only first 3 todos</button>
</div>
)
}
// update = like intent, but without any input
const updates = { removeAllTodos };
// intent = a little like action/action creator from redux
const intents = { addTodo, limitTodos };
// selector = mapStateToProps from Redux
const selector = state => ({ todos: state.todos });
// And that's a connected component, as in Redux
export default connect(selector, updates, intents)(Todos);
// Of course a to-do list!
export interface Todo {
title: string
}
export interface State {
todos: Todo[]
}
// These 3 (udpates and intents) are similar to actions/action creators from redux.
export const removeAllTodos: Update<State> = state => ({...state, todos: []});
export const addTodo: Intent<State, Todo> = todo => state => ({...state: todos: state.todos.concat(todo)});
export const limitTodos: Intent<State, number> = n => state => ({...state, todos: state.todos.slice(0, n)});
const { connect } = createStore<State>();
export { connect };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment