Skip to content

Instantly share code, notes, and snippets.

@takkaria
Last active December 6, 2021 12:42
Show Gist options
  • Save takkaria/d7ffe1b8a7637ac775029966be74c596 to your computer and use it in GitHub Desktop.
Save takkaria/d7ffe1b8a7637ac775029966be74c596 to your computer and use it in GitHub Desktop.
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