@d3forge/react
v1.0.3
Published
> Persist and restore route re-entry state in React SPAs.
Readme
@d3forge/react
Persist and restore route re-entry state in React SPAs.
@d3forge/react helps you keep user context when they leave and return:
- restore scroll position (
scrollY) - restore custom state (
persistedData) - plug in any storage layer via an adapter (Redux, memory, storage, etc.)
Features
useReentryState(stateKey, adapter)hook- storage-agnostic adapter API
- TypeScript support out of the box
- tiny surface area, easy to integrate
Installation
npm i @d3forge/react react react-router-dom
# or
yarn add @d3forge/react react react-router-domPeer Dependencies
react >= 18react-router-dom >= 6
API
useReentryState(stateKey, adapter)
stateKey: string- key under which state is storedadapter: ReentryAdapter- read/write implementation
Returns:
key- currentlocation.keyscrollY- restored vertical scrollhasExistingEntry- whether saved state belongs to current route keypersistedData- restored custom payloadsetPersistedData(data)- merge new values into pending payload
Types
type ReentryData = Record<string, unknown>;
type ReentryState = {
key?: string;
scrollY?: number;
persistedData?: ReentryData;
};
type ReentryAdapter = {
initialReentryState: (stateKey: string) => ReentryState | undefined;
setReentryState: (stateKey: string, nextState: ReentryState) => void;
};Quick Start (in-memory adapter)
import { useMemo } from "react";
import { useReentryState, type ReentryAdapter } from "@d3forge/react";
const memory = new Map<string, unknown>();
export function Demo() {
const adapter: ReentryAdapter = useMemo(
() => ({
initialReentryState: (k) => memory.get(k) as any,
setReentryState: (k, next) => memory.set(k, next),
}),
[]
);
const { persistedData, setPersistedData, hasExistingEntry, scrollY } =
useReentryState("products:list", adapter);
return (
<div>
<p>hasExistingEntry: {String(hasExistingEntry)}</p>
<p>scrollY: {scrollY}</p>
<pre>{JSON.stringify(persistedData, null, 2)}</pre>
<button
onClick={() =>
setPersistedData({
clicks: ((persistedData?.clicks as number) ?? 0) + 1,
})
}
>
Increment
</button>
</div>
);
}Redux Integration
Redux is optional. If you already use Redux, create an adapter from your store.
Hook-level adapter example
import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useReentryState, type ReentryAdapter } from "@d3forge/react";
export function ProductsPage() {
const dispatch = useDispatch();
const reentryMap = useSelector((state: any) => state.layout.reentryMap);
const adapter: ReentryAdapter = useMemo(
() => ({
initialReentryState: (stateKey) => reentryMap?.[stateKey],
setReentryState: (stateKey, nextState) =>
dispatch({
type: "layout/setReentryState",
payload: { stateKey, nextState },
}),
}),
[dispatch, reentryMap]
);
const reentry = useReentryState("products:list", adapter);
return <div>{String(reentry.hasExistingEntry)}</div>;
}HOC wrapper example (for Redux projects)
If your team prefers HOC-style integration, you can create a wrapper once and reuse it:
import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useReentryState, type ReentryAdapter } from "@d3forge/react";
type ReentryProps = { reentry: ReturnType<typeof useReentryState> };
export function withReduxReentry(stateKey: string) {
return function <P extends object>(
Wrapped: React.ComponentType<P & ReentryProps>
) {
return function WithReduxReentry(props: P) {
const dispatch = useDispatch();
const reentryMap = useSelector((state: any) => state.layout.reentryMap);
const adapter: ReentryAdapter = useMemo(
() => ({
initialReentryState: (key) => reentryMap?.[key],
setReentryState: (key, nextState) =>
dispatch({
type: "layout/setReentryState",
payload: { stateKey: key, nextState },
}),
}),
[dispatch, reentryMap]
);
const reentry = useReentryState(stateKey, adapter);
return <Wrapped {...props} reentry={reentry} />;
};
};
}Notes
- Works best in SPA apps with React Router.
- For Next.js, you usually want a router-specific adapter pattern.
License
ISC
