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_query_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_query_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_query_params/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { atomWithQueryParams } from 'jotai-location';
import { useAtom } from 'jotai/react';
import React from 'react';

const pageAtom = atomWithQueryParams('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_query_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 />);
}
85 changes: 85 additions & 0 deletions src/atomWithQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { SetStateAction, WritableAtom } from 'jotai/vanilla';
import { atom } from 'jotai/vanilla';
import { atomWithLocation } from './atomWithLocation';

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

/**
* Creates a writable Jotai atom to manage a query parameter in the URL.
*
* @template T - The type of the query parameter value (e.g., string or number).
* @param key - The name of the query parameter to manage.
* @param defaultValue - The default value for the query parameter if not present in the URL.
* @returns A writable atom for reading and updating the query parameter.
*/
export const atomWithQueryParams = <T>(
key: string,
defaultValue: T,
): WritableAtom<T, [SetStateAction<T>], void> => {
/**
* Resolves the value of a query parameter based on its type.
*
* @param value - The raw string value from the URL (or `null` if absent).
* @returns The resolved value cast to type `T`.
*/
const resolveDefaultValue = (value: string | null | undefined): T => {
// If the value is null or undefined, return the default value.
if (value === null || value === undefined) {
return defaultValue;
}

// If the default value is a number, attempt to parse the value as a number.
if (typeof defaultValue === 'number') {
const parsed = Number(value);
return (Number.isNaN(parsed) ? defaultValue : parsed) as T;
}

// Otherwise, return the value as a string (or other compatible type).
return value as T;
};

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

// Use the resolver function to handle type casting and defaults.
return resolveDefaultValue(paramValue);
},
// Write function: updates the query parameter in the URL.
(_, set, value) => {
set(locationAtom, (prev) => {
// Clone the current search parameters to avoid mutating the original object.
const newSearchParams = new URLSearchParams(prev.searchParams);
const currentValue = newSearchParams.get(key);

let nextValue: string;

if (typeof value === 'function') {
// If the new value is a function, compute it based on the current value.
const resolvedDefault = resolveDefaultValue(currentValue);

nextValue = String(
(value as (curr: T | undefined) => T)(resolvedDefault),
);
} else {
// Otherwise, use the provided value directly.
nextValue = String(value);
}

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

// Return a new location state with the updated query parameters.
return { ...prev, searchParams: newSearchParams };
});
},
);
};

// Usage
// const pageAtom = atomWithQueryParams('page', 1);
// const userIdAtom = atomWithQueryParams('userId', "1");
// const nameAtom = atomWithQueryParams<string>('name', "John");
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 { atomWithQueryParams } from './atomWithQueryParams';
jiangtaste marked this conversation as resolved.
Show resolved Hide resolved