diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a278f0aa..3cd89156 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -3,7 +3,7 @@ import { SpeedInsights } from '@vercel/speed-insights/react';
import { clsx } from 'clsx/lite';
import { IBM_Plex_Mono } from 'next/font/google';
import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config';
-import StateProvider from '@/state/AppStateProvider';
+import AppStateProvider from '@/state/AppStateProvider';
import ThemeProviderClient from '@/site/ThemeProviderClient';
import Nav from '@/site/Nav';
import ToasterWithThemes from '@/toast/ToasterWithThemes';
@@ -13,6 +13,7 @@ import { Suspense } from 'react';
import FooterClient from '@/site/FooterClient';
import NavClient from '@/site/NavClient';
import { Metadata } from 'next/types';
+import MoreComponentsProvider from '@/state/MoreComponentsProvider';
import '../site/globals.css';
@@ -72,27 +73,29 @@ export default function RootLayout({
suppressHydrationWarning
>
-
-
-
- }>
-
-
-
+
+
+
- {children}
-
- }>
-
-
-
-
-
+ }>
+
+
+
+ {children}
+
+ }>
+
+
+
+
+
+
diff --git a/src/components/AnimateItems.tsx b/src/components/AnimateItems.tsx
index 9179a697..c4ddb2ad 100644
--- a/src/components/AnimateItems.tsx
+++ b/src/components/AnimateItems.tsx
@@ -2,7 +2,7 @@
import { ReactNode, useRef } from 'react';
import { Variant, motion } from 'framer-motion';
-import { useAppState } from '@/state';
+import { useAppState } from '@/state/AppState';
import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion';
export type AnimationType = 'none' | 'scale' | 'left' | 'right' | 'bottom';
diff --git a/src/components/MoreComponents.tsx b/src/components/MoreComponents.tsx
index 112850b4..6104548c 100644
--- a/src/components/MoreComponents.tsx
+++ b/src/components/MoreComponents.tsx
@@ -1,14 +1,20 @@
'use client';
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
import Spinner from './Spinner';
import SiteGrid from './SiteGrid';
+import {
+ MoreComponentsKey,
+ MoreComponentsStateForKeyArgument,
+ useMoreComponentsState,
+} from '@/state/MoreComponentsState';
const MAX_ATTEMPTS_PER_REQUEST = 5;
const MAX_TOTAL_REQUESTS = 500;
const RETRY_DELAY_IN_SECONDS = 1.5;
export default function MoreComponents({
+ stateKey,
initialOffset,
itemsPerRequest,
getNextComponent,
@@ -16,6 +22,7 @@ export default function MoreComponents({
triggerOnView = true,
prefetch = true,
}: {
+ stateKey: MoreComponentsKey
initialOffset: number
itemsPerRequest: number
getNextComponent: (offset: number, limit: number) => Promise<{
@@ -27,11 +34,20 @@ export default function MoreComponents({
triggerOnView?: boolean
prefetch?: boolean
}) {
- const [indexToView, setIndexToView] = useState(0);
- const [indexLoaded, setIndexLoaded] = useState(0);
- const [isLoading, setIsLoading] = useState(false);
- const [lastIndexToLoad, setLastIndexToLoad] = useState();
- const [components, setComponents] = useState([]);
+ const { state, setStateForKey } = useMoreComponentsState();
+
+ const setState = useCallback(
+ (stateForKey: MoreComponentsStateForKeyArgument) =>
+ setStateForKey(stateKey, stateForKey),
+ [setStateForKey, stateKey]);
+
+ const {
+ indexToView,
+ indexLoaded,
+ isLoading,
+ lastIndexToLoad,
+ components,
+ } = state[stateKey];
// When prefetching, always stay one request ahead of what's visible
const indexToLoad = lastIndexToLoad
@@ -53,7 +69,7 @@ export default function MoreComponents({
if (totalRequests.current < MAX_TOTAL_REQUESTS) {
attemptsPerRequest.current += 1;
totalRequests.current += 1;
- setIsLoading(true);
+ setState({ isLoading: true });
const handleError = () => {
setTimeout(() => {
attempt();
@@ -65,14 +81,17 @@ export default function MoreComponents({
)
.then(({ nextComponent, isFinished, didFail }) => {
if (!didFail && nextComponent) {
- setComponents(current => {
- const updatedComponents = [...current];
+ setState(state => {
+ const updatedComponents = [...state.components];
updatedComponents[indexToLoad] = nextComponent;
- return updatedComponents;
+ return {
+ ...state,
+ components: updatedComponents,
+ indexLoaded: indexToLoad,
+ };
});
- setIndexLoaded(indexToLoad);
if (isFinished) {
- setLastIndexToLoad(indexToLoad);
+ setState({ lastIndexToLoad: indexToLoad });
}
attemptsPerRequest.current = 0;
} else {
@@ -80,7 +99,7 @@ export default function MoreComponents({
}
})
.catch(handleError)
- .finally(() => setIsLoading(false));
+ .finally(() => setState({ isLoading: false }));
} else {
console.error(
`Max total attempts reached (${MAX_TOTAL_REQUESTS})`
@@ -92,9 +111,10 @@ export default function MoreComponents({
);
}
}, [
+ setState,
getNextComponent,
- indexToLoad,
initialOffset,
+ indexToLoad,
itemsPerRequest,
]);
@@ -118,9 +138,9 @@ export default function MoreComponents({
const advance = useCallback(() => {
if (indexToView <= indexLoaded) {
- setIndexToView(i => i + 1);
+ setState({ indexToView: indexToView + 1 });
}
- }, [indexToView, indexLoaded]);
+ }, [setState, indexToView, indexLoaded]);
useEffect(() => {
// Only add observer if button is rendered
diff --git a/src/photo/MorePhotosLarge.tsx b/src/photo/MorePhotosLarge.tsx
index d460a869..1255092e 100644
--- a/src/photo/MorePhotosLarge.tsx
+++ b/src/photo/MorePhotosLarge.tsx
@@ -13,6 +13,7 @@ export function MorePhotosLarge({
}) {
return (
(MORE_COMPONENTS_INITIAL_STATE);
+
+ const setStateForKey = useCallback((
+ key: MoreComponentsKey,
+ state: MoreComponentsStateForKeyArgument
+ ) => {
+ setState(existingState => ({
+ ...existingState,
+ ...typeof state === 'function'
+ ? { [key]: state(existingState[key]) }
+ : { [key]: { ...existingState[key], ...state } },
+ }));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/state/MoreComponentsState.ts b/src/state/MoreComponentsState.ts
new file mode 100644
index 00000000..5cf54c63
--- /dev/null
+++ b/src/state/MoreComponentsState.ts
@@ -0,0 +1,50 @@
+import { createContext, useContext } from 'react';
+
+export type MoreComponentsKey =
+ 'PhotosLarge';
+
+export interface MoreComponentsStateForKey {
+ indexToView: number
+ indexLoaded: number
+ isLoading: boolean
+ lastIndexToLoad?: number
+ components: JSX.Element[]
+}
+
+export const createInitialStateForKey =
+ (): MoreComponentsStateForKey => ({
+ indexToView: 0,
+ indexLoaded: 0,
+ isLoading: false,
+ components: [],
+ });
+
+export type MoreComponentsState = Record<
+ MoreComponentsKey,
+ MoreComponentsStateForKey
+>;
+
+export type MoreComponentsStateForKeyArgument =
+ Partial |
+ ((existingValue: MoreComponentsStateForKey) => MoreComponentsStateForKey);
+
+export interface MoreComponentsContext {
+ state: MoreComponentsState
+ setStateForKey: (
+ key: MoreComponentsKey,
+ state: MoreComponentsStateForKeyArgument,
+ ) => void
+}
+
+export const MORE_COMPONENTS_INITIAL_STATE: MoreComponentsState = {
+ PhotosLarge: createInitialStateForKey(),
+};
+
+export const MoreComponentsContext =
+ createContext({
+ state: MORE_COMPONENTS_INITIAL_STATE,
+ setStateForKey: () => {},
+ });
+
+export const useMoreComponentsState = () =>
+ useContext(MoreComponentsContext);