Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for Search Params #39

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
27 changes: 27 additions & 0 deletions examples/05_search_params/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "jotai-location-example",
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/react": "latest",
"@types/react-dom": "latest",
"jotai": "latest",
"jotai-location": "latest",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"typescript": "latest"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
8 changes: 8 additions & 0 deletions examples/05_search_params/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>jotai-location example</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
22 changes: 22 additions & 0 deletions examples/05_search_params/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { atomWithSearchParams } from 'jotai-location';
import { useAtom } from 'jotai/react';
import React from 'react';

const pageAtom = atomWithSearchParams('page', 1);

const Page = () => {
const [page, setPage] = useAtom(pageAtom);
return (
<div>
<div>Page {page}</div>
<button type="button" onClick={() => setPage((c) => c + 1)}>
+1
</button>
<p>See the url hash, change it there</p>
</div>
);
};

const App = () => <Page />;

export default App;
9 changes: 9 additions & 0 deletions examples/05_search_params/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';

const ele = document.getElementById('app');
if (ele) {
createRoot(ele).render(<App />);
}
99 changes: 99 additions & 0 deletions src/atomWithSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { SetStateAction, WritableAtom } from 'jotai/vanilla';
import { atom } from 'jotai/vanilla';
import { atomWithLocation } from './atomWithLocation';

// Create an atom for managing location state, including search parameters.
const locationAtom = atomWithLocation();

/**
* Creates an atom that manages a single search parameter.
*
* The atom automatically infers the type of the search parameter based on the
* type of `defaultValue`.
*
* The atom's read function returns the current value of the search parameter.
* The atom's write function updates the search parameter in the URL.
*
* @param key - The key of the search parameter.
* @param defaultValue - The default value of the search parameter.
* @returns A writable atom that manages the search parameter.
*/
export const atomWithSearchParams = <T>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, then limit only to support string, number and boolean.

Suggested change
export const atomWithSearchParams = <T>(
export const atomWithSearchParams = <T extends string | number | boolean>(

and simply the code please?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried, but I can't figure out why this strange type is being returned in TypeScript. I haven't yet pinpointed the reason.

Atom:
const pageAtom = atomWithSearchParams("page", 1)

Expected:

const pageAtom: WritableAtom<number, [SetStateAction<number>], void>

Got:

const pageAtom: WritableAtom<1, [SetStateAction<1>], void>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case you need to do atomWithSearchParams<number>("page", 1) or atomWithSearchParams("page", 1 as number). Do you have any other issues with this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, union type doesn't work. We should overload the function.

key: string,
defaultValue: T,
): WritableAtom<T, [SetStateAction<T>], void> => {
/**
* Resolves the value of a search parameter based on the type of `defaultValue`.
*
* @param value - The raw value from the URL (could be `null` or `undefined`).
* @returns The resolved value matching the type of `defaultValue`.
*/
const resolveValue = (value: string | null | undefined): T => {
// If the value is null or undefined, return the default value.
if (value === null || value === undefined) {
return defaultValue;
}

// Determine the type of the default value and parse accordingly.
if (typeof defaultValue === 'number') {
return Number(value) as T;
jiangtaste marked this conversation as resolved.
Show resolved Hide resolved
}

if (typeof defaultValue === 'boolean') {
return (value === 'true') as T;
jiangtaste marked this conversation as resolved.
Show resolved Hide resolved
}

if (typeof defaultValue === 'string') {
return value as T;
jiangtaste marked this conversation as resolved.
Show resolved Hide resolved
}

// If the default value is an object, try to parse it as JSON.
return JSON.parse(value) as T;
jiangtaste marked this conversation as resolved.
Show resolved Hide resolved
};

const parseValue = (value: T): string => {
if (
typeof value !== 'number' &&
typeof value !== 'boolean' &&
typeof value !== 'string'
) {
// If the value is not a basic type, try to stringify it as JSON.
return JSON.stringify(value);
}
return String(value);
};

return atom<T, [SetStateAction<T>], void>(
// Read function: Retrieves the current value of the search parameter.
(get) => {
const { searchParams } = get(locationAtom);

// Resolve the value using the parsing logic.
return resolveValue(searchParams?.get(key));
},
// Write function: Updates the search parameter in the URL.
(_, set, value) => {
set(locationAtom, (prev) => {
// Create a new instance of URLSearchParams to avoid mutating the original.
const newSearchParams = new URLSearchParams(prev.searchParams);

let nextValue;

if (typeof value === 'function') {
// If the new value is a function, compute it based on the current value.
const currentValue = resolveValue(newSearchParams.get(key));
nextValue = (value as (curr: T) => T)(currentValue);
} else {
// Otherwise, use the provided value directly.
nextValue = value;
}

// Update the search parameter with the computed value.
newSearchParams.set(key, parseValue(nextValue));

// Return the updated location state with new search parameters.
return { ...prev, searchParams: newSearchParams };
});
},
);
};
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { atomWithLocation } from './atomWithLocation';
export { atomWithHash } from './atomWithHash';
export { atomWithLocation } from './atomWithLocation';
export { atomWithSearchParams } from './atomWithSearchParams';