Last active
October 29, 2018 10:14
-
-
Save michowski/ff444cf7e2b7c0ca2bef5446a65a698f to your computer and use it in GitHub Desktop.
Like Redux, but without any boilerplate.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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