Last active
December 6, 2021 12:42
-
-
Save takkaria/d7ffe1b8a7637ac775029966be74c596 to your computer and use it in GitHub Desktop.
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 React, { createContext, useContext, ReactElement } from 'react'; | |
import { render, unmountComponentAtNode } from 'react-dom'; | |
/*** Shared ***/ | |
/* | |
* A function that returns a promise that resolves to a message. | |
* | |
* We do this to keep the length of lines down a bit. It also matches with the | |
* terminology used in Elm. | |
*/ | |
type Cmd<M> = () => Promise<M>; | |
/* | |
* A complete Elm Architecture view module | |
* | |
* init: takes some initial 'outer state' of unknown shape and returns some internal | |
* state. The outer state is intended to be Macquette's current big blob. | |
* | |
* update: takes the current state and a message, returns a new state and optionally | |
* a Cmd (see above). You use the Cmd to do async stuff like fetching. Elm has a more | |
* sophisticated 'effects manager' system but this is more lightweight. | |
* | |
* view: given a state, render it out. | |
*/ | |
type Module<T, M> = { | |
init: (outerState: unknown) => T; | |
update: (state: Readonly<T>, message: Readonly<M>) => [T, Cmd<M>?]; | |
view: (state: Readonly<T>) => ReactElement; | |
mutate: (state: Readonly<T>) => void; | |
}; | |
/** | |
* The type of the data that our module context will hold. | |
* | |
* Context is React's way to avoid 'prop drilling', i.e. passing down things deep | |
* into the virtual DOM tree. | |
* | |
* Most importantly, here is where we place our 'dispatch' function, which takes a | |
* message and passes it through the update function in a Module. Note that we lose | |
* some type safety this way as we don't know exactly what message type the dispatch | |
* function should take here. | |
*/ | |
type ModuleContextType = { | |
mountPoint: HTMLElement | null; | |
dispatch: (message: unknown) => void; | |
}; | |
const ModuleContext = createContext<ModuleContextType>({ | |
mountPoint: null, | |
dispatch: (message) => {}, | |
}); | |
/** | |
* Define a view module. | |
* | |
* Returns a function that mounts the view on a given element, which returns a | |
* function which unmounts the view again. | |
*/ | |
function module<T, M>(module: Module<T, M>) { | |
return (mountPoint: HTMLElement, outerState: unknown): (() => void) => { | |
let state = module.init(outerState); | |
const renderView = () => | |
// When we render we provide a context parent so we don't have to do | |
// prop drilling for the dispatch function. We're round-tripping this | |
// through the virtual DOM though, which means we lose type info. | |
render( | |
<ModuleContext.Provider value={{ mountPoint, dispatch }}> | |
{module.view(state)} | |
</ModuleContext.Provider>, | |
mountPoint | |
); | |
const dispatch = (message: M) => { | |
let [newState, cmd] = module.update(state, message); | |
if (newState !== state) { | |
state = newState; | |
renderView(); | |
module.mutate(newState); | |
} | |
if (cmd !== undefined) { | |
// TODO: Need to catch any failures too | |
cmd().then((m) => dispatch(m)); | |
} | |
}; | |
renderView(); | |
return () => unmountComponentAtNode(mountPoint); | |
}; | |
} | |
type ButtonProps<M> = { text: string; onPress: M }; | |
/* | |
* Example button element using the ModuleContext above to dispatch a message. | |
*/ | |
function Button<M>({ text, onPress }: ButtonProps<M>): ReactElement { | |
const { dispatch } = useContext(ModuleContext); | |
return <button onClick={() => dispatch(onPress)}>{text}</button>; | |
} | |
/*** View-specific ***/ | |
type State = { | |
counter: number; | |
}; | |
type Message = { type: 'FETCH' } | { type: 'NEW'; payload: number }; | |
const init = (_) => { | |
return { | |
counter: _ !== undefined ? 5 : 10, | |
}; | |
}; | |
const update = (state: State, message: Message): [State, Cmd<Message>?] => { | |
if (message.type === 'FETCH') { | |
return [state, fetchNewState]; | |
} else if (message.type === 'NEW') { | |
return [{ counter: message.payload }]; | |
} | |
throw new Error("Never return"); | |
}; | |
const view = (state: State) => { | |
return ( | |
<div> | |
<p>{state.counter}</p> | |
<Button text="Fetch" onPress={{ type: 'FETCH' }} /> | |
</div> | |
); | |
}; | |
async function fetchNewState(): Promise<Message> { | |
const response = await fetch('/fail_randomly'); | |
if (response.ok) { | |
return { type: 'NEW', payload: 5 }; | |
} else { | |
return { type: 'NEW', payload: 10 }; | |
} | |
} | |
function mutate() { | |
// Mutate global state etc. | |
} | |
export const helloWorld = module({ init, update, view, mutate }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment