Skip to content

Commit

Permalink
add support for Query Params
Browse files Browse the repository at this point in the history
  • Loading branch information
jiangtaste committed Nov 24, 2024
1 parent 8e7194d commit 525cf48
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 1 deletion.
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';
import { atom } from 'jotai';
import { atomWithLocation } from 'jotai-location';

// 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 (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).searchParams;
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 (prev: 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';

0 comments on commit 525cf48

Please sign in to comment.