Last active
April 7, 2025 02:42
-
-
Save seia-soto/bdfed40af70b235378088e4133b6bdd6 to your computer and use it in GitHub Desktop.
A self-containable React external state manager
This file contains hidden or 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 { | |
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; | |
}; | |
} |
This file contains hidden or 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 { 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