Skip to content

Instantly share code, notes, and snippets.

@seia-soto
Last active April 7, 2025 02:42
Show Gist options
  • Save seia-soto/bdfed40af70b235378088e4133b6bdd6 to your computer and use it in GitHub Desktop.
Save seia-soto/bdfed40af70b235378088e4133b6bdd6 to your computer and use it in GitHub Desktop.
A self-containable React external state manager
import {
type Dispatch,
type SetStateAction,
useLayoutEffect,
useState,
} from 'react';
// Create encapsulated context for the type of T.
export function create<
T,
// Hint is optional by default. If you want to force the use
// of hint, it's necessary not to add `void` in the type.
Hint = void,
>(
initialState: T,
{
hookOnBeforeStateSet,
hookOnRenderContextCreates,
hookOnRenderContextCloses,
}: {
// -- Hooks; are created to control lifecycle of external
// state directly. It's useful not to affect components'
// lifecycle.
// NOTE: Changes made by hooks will not trigger re-render!
// Preprocess new state before the depolyment.
hookOnBeforeStateSet?: (newState: T) => T;
// Preprocess state before render context being created.
// This hook is not called inside of `useLayoutEffect` not to
// disturb render process due to intermediate state change.
// However, misuse of this hook is expected to impact
// the application performance directly.
hookOnRenderContextCreates?: (state: T, hint: Hint) => T;
// Postprocess state after render context being closed.
// This hooks is called in the callback of `useLayoutEffect`.
hookOnRenderContextCloses?: (state: T, hint: Hint) => T;
} = {},
) {
// Keeps the reference to the state even in case of the first
// citizens.
const container = { state: initialState };
// Manage dispatchers to be triggered.
const dispatchers: Dispatch<SetStateAction<T>>[] = [];
type SetStateCallback = (current: T) => T;
function isSetStateCallback(
callback: T | SetStateCallback,
): callback is SetStateCallback {
return typeof callback === 'function';
}
function setState(newState: T | SetStateCallback) {
if (isSetStateCallback(newState)) {
newState = newState(container.state);
}
if (typeof hookOnBeforeStateSet !== 'undefined') {
newState = hookOnBeforeStateSet(newState);
}
// Override the state. This does not trigger re-render in any
// render context.
container.state = newState;
for (const dispatcher of dispatchers) {
// Trigger re-render manually. There is no exception as
// everything here is done synchronously.
dispatcher(newState);
}
}
return function useContext(hint: Hint) {
if (typeof hookOnRenderContextCreates !== 'undefined') {
container.state = hookOnRenderContextCreates(
container.state,
hint,
);
}
// Below function body is expected to be run in the render context.
// While we handle the state outside of React lifecycle, we provide
// a reference to our state on time.
const [local, setLocal] = useState<T>(container.state);
useLayoutEffect(function () {
// When render starts, we register the local dispatcher to our
// context.
dispatchers.push(setLocal);
return function () {
// When render ends, we unregister the local dispatcher.
const index = dispatchers.indexOf(setLocal);
if (typeof index !== 'undefined') {
dispatchers.splice(index, 1);
}
if (typeof hookOnRenderContextCloses !== 'undefined') {
container.state = hookOnRenderContextCloses(
container.state,
hint,
);
}
};
});
return [local, setState] as const;
};
}
import { Flight } from '../../modules/flights/types.js';
import { computeFlightHash } from '../../modules/flights/utils.js';
import { karney, miles } from '../../utils/geodesic.js';
import { create } from './useContext.js';
export type FlightsState = {
numbers: {
miles: number;
};
updatedAt: number;
list: Flight[];
listByHash: Map<number, Flight>;
};
export const useFlightsContext = create<FlightsState>({
numbers: {
miles: 0,
},
updatedAt: -1,
list: [],
listByHash: new Map(),
});
export function useFlights() {
const [flights, setFlights] = useFlightsContext();
function add(newFlight: Flight) {
setFlights(function (flights) {
const hash = computeFlightHash(newFlight);
if (flights.listByHash.has(hash)) {
return flights;
}
let position = flights.list.findIndex(function (flight) {
return flight.date > newFlight.date;
});
if (position === -1) {
position = flights.list.length;
}
return {
...flights,
numbers: {
...flights.numbers,
miles:
flights.numbers.miles +
miles(
karney(
newFlight.departure.coordinates,
newFlight.arrival.coordinates,
),
),
},
list: [
...flights.list.slice(0, position),
newFlight,
...flights.list.slice(position),
],
listByHash: new Map(flights.listByHash).set(
hash,
newFlight,
),
};
});
}
function remove(flight: Flight) {
setFlights(function (flights) {
const hash = computeFlightHash(flight);
if (flights.listByHash.has(hash) === false) {
return flights;
}
flights.listByHash.delete(hash);
return {
...flights,
list: flights.list.filter(function (item) {
return computeFlightHash(item) !== hash;
}),
listByHash: new Map(flights.listByHash),
};
});
}
return {
...flights,
add,
remove,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment