Created
July 13, 2022 08:12
-
-
Save Lucifier129/bd3bbeab98ed59db7b8e513cc02eea14 to your computer and use it in GitHub Desktop.
Modular state-management via codata & lens & state-machine
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
type PrimitiveData = string | number | boolean | null | |
type ListData = LensData[] | |
type ObjectData = { | |
[key: string]: LensData | |
} | |
type LensData = PrimitiveData | ListData | ObjectData | |
type Setter<data, source = data> = (data: data) => source | |
type Getter<data, source = data> = (source: source) => data | |
type Accessor<data, source = data> = { | |
get: Getter<data, source> | |
set: Setter<data, source> | |
} | |
type SelectByKey<data extends ObjectData, source> = <key extends keyof data>(key: key) => Lens<data[key], source> | |
type SelectByIndex<data extends ListData, source> = (index: number) => Lens<data[number], source> | |
type Select<data, source> = data extends ObjectData | |
? { | |
select: SelectByKey<data, source> | |
} | |
: data extends ListData | |
? { | |
select: SelectByIndex<data, source> | |
} | |
: {} | |
type LensActions<data, source = data> = Accessor<data, source> & Select<data, source> | |
type Lens<data, source = data> = [data, LensActions<data, source>] | |
const isObjectData = (value: unknown): value is ObjectData => typeof value === 'object' && value !== null | |
const isListData = (value: unknown): value is ListData => Array.isArray(value) | |
const isPrimitiveData = (value: unknown): value is PrimitiveData => | |
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null | |
const ListLens = <data extends ListData, source = data>(data: data, { get, set }: Accessor<data, source>) => { | |
const select: SelectByIndex<data, source> = (index) => { | |
return Lens<data[number], source>(data[index], { | |
get: (source) => get(source)[index] as data[number], | |
set: (item) => { | |
if (item === data[index]) { | |
return set(data) | |
} | |
const newData = [...data] as data | |
newData[index] = item | |
return set(newData) | |
}, | |
}) | |
} | |
return [data, { get, set, select }] as unknown as Lens<data, source> | |
} | |
const ObjectLens = <data extends ObjectData, source = data>(data: data, { get, set }: Accessor<data, source>) => { | |
const select: SelectByKey<data, source> = (key) => { | |
return Lens<data[typeof key], source>(data[key], { | |
get: (source) => get(source)[key], | |
set: (value) => { | |
if (value === data[key]) { | |
return set(data) | |
} | |
const newData = { ...data } | |
newData[key] = value | |
return set(newData) | |
}, | |
}) | |
} | |
return [data, { get, set, select }] as unknown as Lens<data, source> | |
} | |
const Lens = <data extends LensData, source = data>( | |
data: data, | |
accessor: Accessor<data, source>, | |
): Lens<data, source> => { | |
if (isListData(data)) { | |
return ListLens(data, accessor as unknown as Accessor<ListData, source>) as unknown as Lens<data, source> | |
} | |
if (isObjectData(data)) { | |
return ObjectLens(data, accessor as unknown as Accessor<ObjectData, source>) as unknown as Lens<data, source> | |
} | |
if (isPrimitiveData(data)) { | |
return [data, accessor] as Lens<data, source> | |
} | |
throw new Error(`Unsupported data type: ${data}`) | |
} | |
const identity = <x>(x: x): x => x | |
const from = <data extends LensData>(data: data): Lens<data> => { | |
return Lens(data, { | |
get: identity, | |
set: identity, | |
}) | |
} | |
const TextMachine = <source>([text, { set }]: Lens<string, source>) => { | |
const setText = (value: string) => { | |
return set(value) | |
} | |
const clearText = () => { | |
return set('') | |
} | |
return [text, { setText, clearText }] as const | |
} | |
const SwitchMachine = <data, source>([data, { set }]: Lens<data, source>) => { | |
const switchTo = (value: data) => { | |
return set(value) | |
} | |
return [data, { switchTo }] as const | |
} | |
const ToggleMachine = <source>(lens: Lens<boolean, source>) => { | |
const [value, { switchTo }] = SwitchMachine(lens) | |
const toggle = () => { | |
return switchTo(!value) | |
} | |
return [value, { toggle }] as const | |
} | |
type TodoData = { | |
id: string | |
title: string | |
completed: boolean | |
} | |
const TodoMachine = <source>([todo, { select }]: Lens<TodoData, source>) => { | |
const [, { setText, clearText }] = TextMachine(select('title')) | |
const [, { toggle }] = ToggleMachine(select('completed')) | |
return [ | |
todo, | |
{ | |
setTitle: setText, | |
clearTitle: clearText, | |
toggleCompleted: toggle, | |
}, | |
] as const | |
} | |
let todoUid = 0 | |
const TodoListMachine = <source>([todos, { set, select }]: Lens<TodoData[], source>) => { | |
const getTodoActions = (todoId: string) => { | |
const index = todos.findIndex((todo) => todo.id === todoId) | |
if (index === -1) { | |
throw new Error(`Todo with id ${todoId} not found`) | |
} | |
return TodoMachine(select(index))[1] | |
} | |
const addTodo = (title: string) => { | |
const newTodo = { | |
id: `${todoUid++}`, | |
title, | |
completed: false, | |
} | |
return set([...todos, newTodo]) | |
} | |
const removeTodo = (todoId: string) => { | |
const index = todos.findIndex((todo) => todo.id === todoId) | |
if (index === -1) { | |
throw new Error(`Todo with id ${todoId} not found`) | |
} | |
const newTodos = [...todos] | |
newTodos.splice(index, 1) | |
return set(newTodos) | |
} | |
return [todos, { addTodo, removeTodo, getTodoActions }] as const | |
} | |
type TodoFilter = 'all' | 'active' | 'completed' | |
type TodoAppState = { | |
todoInput: string | |
todos: TodoData[] | |
todoFilter: TodoFilter | |
} | |
const TodoAppMachine = <source>([state, { get, set, select }]: Lens<TodoAppState, source>) => { | |
const [, inputActions] = TextMachine(select('todoInput')) | |
const [, todosActions] = TodoListMachine(select('todos')) | |
const [, todoFilterActions] = SwitchMachine(select('todoFilter')) | |
const addTodo = () => { | |
const newTodos = get(todosActions.addTodo(state.todoInput)).todos | |
const newTodoInput = get(inputActions.clearText()).todoInput | |
return set({ | |
...state, | |
todoInput: newTodoInput, | |
todos: newTodos, | |
}) | |
} | |
return [ | |
state, | |
{ | |
addTodo, | |
todoInput: inputActions, | |
todos: todosActions, | |
todoFilter: todoFilterActions, | |
}, | |
] as const | |
} | |
const initialState: TodoAppState = { | |
todoInput: '', | |
todos: [], | |
todoFilter: 'all', | |
} | |
let [state, actions] = TodoAppMachine(from(initialState)) | |
console.log('initial-state', state) | |
;[state, actions] = TodoAppMachine(from(actions.todoInput.setText('Hello'))) | |
console.log('set todo input', state) | |
;[state, actions] = TodoAppMachine(from(actions.addTodo())) | |
console.log('add todo', state) | |
;[state, actions] = TodoAppMachine(from(actions.todoInput.setText('World'))) | |
console.log('set todo input', state) | |
;[state, actions] = TodoAppMachine(from(actions.addTodo())) | |
console.log('add todo', state) | |
;[state, actions] = TodoAppMachine(from(actions.todoFilter.switchTo('active'))) | |
console.log('switch todo filter', state) | |
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').toggleCompleted())) | |
console.log('toggle todo', state) | |
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').toggleCompleted())) | |
console.log('toggle todo', state) | |
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').setTitle('Hello World_update'))) | |
console.log('set todo title', state) | |
;[state, actions] = TodoAppMachine(from(actions.todos.removeTodo('0'))) | |
console.log('remove todo', state) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment