Skip to content

Commit

Permalink
allow the setHash option to be overridden on a per atom set basis (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
scamden authored Apr 25, 2024
1 parent 9aba6f6 commit 4666841
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 19 deletions.
68 changes: 68 additions & 0 deletions __tests__/atomWithHash_spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,74 @@ describe('atomWithHash', () => {
expect(window.location.search).toEqual('?q=foo');
expect(window.location.hash).toEqual('#count=2');
});
window.history.back();
await waitFor(() => {
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('');
expect(window.location.hash).toEqual('');
});
});

it('keeping current path only for one set', async () => {
const countAtom = atomWithHash('count', 0);

const Counter = () => {
const [count, setCount] = useAtom(countAtom);
useEffect(() => {
setCount(1, { setHash: 'replaceState' });
}, []);
return (
<>
<div>count: {count}</div>
<button type="button" onClick={() => setCount((c) => c + 1)}>
button
</button>
</>
);
};

window.history.pushState(null, '', '/?q=foo');
const { findByText, getByText } = render(
<StrictMode>
<Counter />
</StrictMode>,
);

await findByText('count: 1');
await waitFor(() => {
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('?q=foo');
expect(window.location.hash).toEqual('#count=1');
});
fireEvent.click(getByText('button'));
await findByText('count: 2');
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('?q=foo');
expect(window.location.hash).toEqual('#count=2');

window.history.pushState(null, '', '/another');
await waitFor(() => {
expect(window.location.pathname).toEqual('/another');
});

window.history.back();
await waitFor(() => {
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('?q=foo');
expect(window.location.hash).toEqual('#count=2');
});
window.history.back();
await waitFor(() => {
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('?q=foo');
expect(window.location.hash).toEqual('#count=1');
});
window.history.back();
await waitFor(() => {
expect(window.location.pathname).toEqual('/');
expect(window.location.search).toEqual('');
expect(window.location.hash).toEqual('');
});
});

it('should optimize value to prevent unnecessary re-renders', async () => {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"compile": "microbundle build -f modern,umd --globals react=React",
"postcompile": "cp dist/index.modern.mjs dist/index.modern.js && cp dist/index.modern.mjs.map dist/index.modern.js.map",
"test": "run-s eslint tsc-test jest",
"test:debug": "run-s eslint tsc-test jest:debug",
"eslint": "eslint --ext .js,.ts,.tsx .",
"jest": "jest",
"jest:debug" : "node --inspect ./node_modules/jest/bin/jest.js --runInBand",
"tsc-test": "tsc --project . --noEmit",
"examples:01_minimal": "DIR=01_minimal EXT=js webpack serve",
"examples:02_typescript": "DIR=02_typescript EXT=tsx webpack serve",
Expand Down
64 changes: 45 additions & 19 deletions src/atomWithHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,51 @@ const safeJSONParse = (initialValue: unknown) => (str: string) => {
}
};

export type SetHashOption =
| 'default'
| 'replaceState'
| ((searchParams: string) => void);

export type AtomWithHashSetOptions = {
setHash?: SetHashOption;
};

export const setHashWithPush = (searchParams: string) => {
window.location.hash = searchParams;
};

export const setHashWithReplace = (searchParams: string): void => {
window.history.replaceState(
window.history.state,
'',
`${window.location.pathname}${window.location.search}#${searchParams}`,
);
};

function getSetHashFn(setHashOption?: SetHashOption) {
if (setHashOption === 'replaceState') {
return setHashWithReplace;
}
if (typeof setHashOption === 'function') {
return setHashOption;
}
return setHashWithPush;
}

export function atomWithHash<Value>(
key: string,
initialValue: Value,
options?: {
serialize?: (val: Value) => string;
deserialize?: (str: string) => Value;
subscribe?: (callback: () => void) => () => void;
setHash?: 'default' | 'replaceState' | ((searchParams: string) => void);
setHash?: SetHashOption;
},
): WritableAtom<Value, [SetStateActionWithReset<Value>], void> {
): WritableAtom<
Value,
[SetStateActionWithReset<Value>, AtomWithHashSetOptions?],
void
> {
const serialize = options?.serialize || JSON.stringify;

const deserialize = options?.deserialize || safeJSONParse(initialValue);
Expand All @@ -36,22 +71,7 @@ export function atomWithHash<Value>(
window.removeEventListener('hashchange', callback);
};
});
const setHashOption = options?.setHash;
let setHash = (searchParams: string) => {
window.location.hash = searchParams;
};
if (setHashOption === 'replaceState') {
setHash = (searchParams) => {
window.history.replaceState(
window.history.state,
'',
`${window.location.pathname}${window.location.search}#${searchParams}`,
);
};
}
if (typeof setHashOption === 'function') {
setHash = setHashOption;
}

const isLocationAvailable =
typeof window !== 'undefined' && !!window.location;

Expand All @@ -77,7 +97,12 @@ export function atomWithHash<Value>(
});
return atom(
(get) => get(valueAtom),
(get, set, update: SetStateActionWithReset<Value>) => {
(
get,
set,
update: SetStateActionWithReset<Value>,
setOptions?: AtomWithHashSetOptions,
) => {
const nextValue =
typeof update === 'function'
? (update as (prev: Value) => Value | typeof RESET)(get(valueAtom))
Expand All @@ -91,6 +116,7 @@ export function atomWithHash<Value>(
set(strAtom, str);
searchParams.set(key, str);
}
const setHash = getSetHashFn(setOptions?.setHash ?? options?.setHash);
setHash(searchParams.toString());
},
);
Expand Down

0 comments on commit 4666841

Please sign in to comment.