diff --git a/src/components/PoolMap.tsx b/src/components/PoolMap.tsx
index 73e9123..db1a236 100644
--- a/src/components/PoolMap.tsx
+++ b/src/components/PoolMap.tsx
@@ -17,6 +17,8 @@ export default function PoolMap() {
console.log('Loaded Base Map')}
+ onTilesLoaded={() => console.log('Loaded Tile Map')}
>
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..9479715
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,88 @@
+import { useEffect, useState } from 'react';
+
+export type UpdateListener = (newValue: StoreValue | null) => void;
+
+export type ValueFetcher = (key: string) => Promise;
+
+export type StoreValue = { value: T } | { error: any };
+
+/**
+ * This is a general-purpose, subscribable key-value store for content like posts, groups, and spaces.
+ */
+export class Store {
+ /**
+ * Stores the internal data. If the value is `null`, then we attempted to fetch the data, but it did not exist.
+ */
+ private data = new Map | null>();
+ private listeners = new Map>>();
+
+ constructor(private fetcher: ValueFetcher) {}
+
+ /**
+ *
+ * @param key The key to get the data for
+ * @param forceRefresh If the data already exists, fetch it again anyway
+ */
+ get(key: string, forceRefresh = false): StoreValue | null {
+ if (!this.data.has(key) || forceRefresh) {
+ this.fetcher(key)
+ .then((value) => {
+ this.set(key, value ? { value } : null);
+ })
+ .catch((error) => {
+ this.set(key, { error });
+ });
+
+ return null;
+ }
+
+ return this.data.get(key) ?? null;
+ }
+
+ set(key: string, value: StoreValue | null) {
+ if (this.listeners.has(key)) {
+ this.listeners.get(key)?.forEach((callback) => {
+ callback(value);
+ });
+ }
+
+ return this.data.set(key, value);
+ }
+
+ subscribe(key: string, listener: UpdateListener) {
+ if (!this.listeners.has(key)) {
+ this.listeners.set(key, new Set());
+ }
+
+ this.listeners.get(key)?.add(listener);
+ }
+
+ unsubscribe(key: string, listener: UpdateListener) {
+ if (this.listeners.has(key)) {
+ if (this.listeners.get(key)?.has(listener)) {
+ this.listeners.get(key)?.delete(listener);
+ return;
+ }
+ }
+
+ console.warn(
+ 'Unsubscribed from',
+ key,
+ 'but listener does not exist: ',
+ listener
+ );
+ }
+}
+
+export function useStoredValue(store: Store, key: string) {
+ const [value, setValue] = useState | null>(store.get(key));
+
+ useEffect(() => {
+ const callback = (value: StoreValue | null) => setValue(value);
+ store.subscribe(key, callback);
+
+ return () => store.unsubscribe(key, callback);
+ }, [key, store]);
+
+ return value;
+}