Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/wouter/src/memory-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const memoryLocation = ({
searchPath = "",
static: staticLocation,
record,
state: initialState = null,
} = {}) => {
let initialPath = path;
if (searchPath) {
Expand All @@ -19,10 +20,11 @@ export const memoryLocation = ({
}

let [currentPath, currentSearch = ""] = initialPath.split("?");
let currentState = initialState;
const history = [initialPath];
const emitter = mitt();

const navigateImplementation = (path, { replace = false } = {}) => {
const navigateImplementation = (path, { replace = false, state } = {}) => {
if (record) {
if (replace) {
history.splice(history.length - 1, 1, path);
Expand All @@ -32,6 +34,13 @@ export const memoryLocation = ({
}

[currentPath, currentSearch = ""] = path.split("?");

// Update state if provided, otherwise keep current state
// This matches browser behavior where state persists unless explicitly changed
if (state !== undefined) {
currentState = state;
}

emitter.emit("navigate", path);
};

Expand All @@ -50,21 +59,36 @@ export const memoryLocation = ({
const useMemoryQuery = () =>
useSyncExternalStore(subscribe, () => currentSearch);

const useMemoryState = () =>
useSyncExternalStore(subscribe, () => currentState);

// Attach searchHook to the location hook for auto-inheritance in Router
useMemoryLocation.searchHook = useMemoryQuery;

function reset() {
// clean history array with mutation to preserve link
history.splice(0, history.length);

// Reset state to initial state
currentState = initialState;

navigateImplementation(initialPath);
}

// Create a getter for state that always returns current value
const stateGetter = {
get current() {
return currentState;
},
};

return {
hook: useMemoryLocation,
searchHook: useMemoryQuery,
stateHook: useMemoryState,
navigate,
history: record ? history : undefined,
reset: record ? reset : undefined,
state: stateGetter,
};
};
22 changes: 22 additions & 0 deletions packages/wouter/test/memory-location.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ test("should support `static` option", () => {

expectTypeOf(hook).toMatchTypeOf<BaseLocationHook>();
});

test("should support `state` option", () => {
const { state, navigate, stateHook } = memoryLocation({
state: { foo: "bar", count: 0 },
});

assertType<any>(state.current);
assertType<Function>(navigate);
assertType<Function>(stateHook);
});

test("should return state getter", () => {
const { state } = memoryLocation({ state: { test: true } });

assertType<{ readonly current: any }>(state);
});

test("should return stateHook", () => {
const { stateHook } = memoryLocation();

assertType<Function>(stateHook);
});
84 changes: 84 additions & 0 deletions packages/wouter/test/memory-location.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,87 @@ test("should have reset method that reset hook location", () => {

unmount();
});

test("should support initial state", () => {
const { state } = memoryLocation({ state: { foo: "bar" } });

expect(state.current).toStrictEqual({ foo: "bar" });
});

test("should have state as null by default", () => {
const { state } = memoryLocation();

expect(state.current).toBe(null);
});

test("should update state when navigating with state option", () => {
const { state, navigate } = memoryLocation();

expect(state.current).toBe(null);

navigate("/new-path", { state: { modal: "promo" } });

expect(state.current).toStrictEqual({ modal: "promo" });
});

test("should preserve state when navigating without state option", () => {
const { state, navigate } = memoryLocation({ state: { initial: true } });

expect(state.current).toStrictEqual({ initial: true });

navigate("/new-path");

// State should be preserved when not explicitly changed
expect(state.current).toStrictEqual({ initial: true });
});

test("should allow setting state to null explicitly", () => {
const { state, navigate } = memoryLocation({ state: { foo: "bar" } });

expect(state.current).toStrictEqual({ foo: "bar" });

navigate("/new-path", { state: null });

expect(state.current).toBe(null);
});

test("should return stateHook that subscribes to state changes", () => {
const { stateHook, navigate } = memoryLocation({ state: { count: 0 } });

const { result, unmount } = renderHook(() => stateHook());

expect(result.current).toStrictEqual({ count: 0 });

act(() => navigate("/somewhere", { state: { count: 1 } }));

expect(result.current).toStrictEqual({ count: 1 });

unmount();
});

test("should reset state to initial state when reset is called", () => {
const { state, navigate, reset } = memoryLocation({
record: true,
state: { initial: true },
});

navigate("/somewhere", { state: { modified: true } });

expect(state.current).toStrictEqual({ modified: true });

reset();

expect(state.current).toStrictEqual({ initial: true });
});

test("should work with navigate from hook return value", () => {
const { hook, state } = memoryLocation();

const { result, unmount } = renderHook(() => hook());

act(() => result.current[1]("/new-path", { state: { from: "hook" } }));

expect(state.current).toStrictEqual({ from: "hook" });

unmount();
});
14 changes: 12 additions & 2 deletions packages/wouter/types/memory-location.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ import {
SearchString,
} from "./location-hook.js";

type Navigate<S = any> = (
type Navigate = (
to: Path,
options?: { replace?: boolean; state?: S; transition?: boolean }
options?: { replace?: boolean; state?: any; transition?: boolean }
) => void;

type StateHook = () => any;

type StateGetter = {
readonly current: any;
};

type HookReturnValue = {
hook: BaseLocationHook;
searchHook: BaseSearchHook;
stateHook: StateHook;
navigate: Navigate;
state: StateGetter;
};
type StubHistory = { history: Path[]; reset: () => void };

Expand All @@ -22,10 +30,12 @@ export function memoryLocation(options?: {
searchPath?: SearchString;
static?: boolean;
record?: false;
state?: any;
}): HookReturnValue;
export function memoryLocation(options?: {
path?: Path;
searchPath?: SearchString;
static?: boolean;
record: true;
state?: any;
}): HookReturnValue & StubHistory;