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
Open

Conversation

jiangtaste
Copy link

@jiangtaste jiangtaste commented Nov 24, 2024

Motivation:
Recoil is no longer actively maintained, and I have recently transitioned to using Jotai. I needed a solution for URL persistence with query parameters similar to what recoil-sync provides, so I implemented this feature in Jotai.

Use cases:
I’ve included a simple example under the examples/05_search_params, which demonstrates how to use the new functionality:

const pageAtom = atomWithSearchParams("key", "defaultValue");

This will result in a URL like: ?key=defaultValue.

Implementation approach:
The implementation builds on Jotai's atomWithLocation from the jotai-location library, combined with Jotai’s basic read-write derived atoms to manage the state and sync it with URL query parameters.

@jiangtaste jiangtaste closed this Nov 24, 2024
Copy link

codesandbox-ci bot commented Nov 24, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@jiangtaste jiangtaste reopened this Nov 24, 2024
@dai-shi
Copy link
Member

dai-shi commented Nov 25, 2024

Can you add a description about motivation, use cases, implementation approach, and so forth?

@jiangtaste

This comment was marked as duplicate.

@dai-shi
Copy link
Member

dai-shi commented Nov 27, 2024

Re: #39 (comment)

Thanks. Can you also put them in the first commend of this PR?

My little concern is that the implementation is on top of atomWithLocation. I'm not sure if it's good or not. That said, the API of atomWithQueryParams("key", "defaultValue"); is straightforward, and we can refactor the implementation later if necessary.

I think to avoid confusion with #20, we should probably rename atomWithQueryParams to atomWithSearchParams.

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution! It looks good overall.

src/index.ts Outdated Show resolved Hide resolved
@jiangtaste
Copy link
Author

I’ve added them to the first comment of the PR.

Renaming atomWithQueryParams to atomWithSearchParams avoids confusion with #20 and keeps things clear. Thanks!

* @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.

src/atomWithSearchParams.ts Outdated Show resolved Hide resolved
src/atomWithSearchParams.ts Outdated Show resolved Hide resolved
src/atomWithSearchParams.ts Outdated Show resolved Hide resolved
@jiangtaste jiangtaste changed the title add support for Query Params add support for Search Params Nov 28, 2024
enhance runtime type validation for atomWithSearchParams
Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

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

We decided to support only primitive values. For other values, TS shouldn't accept it, and JS should raise an error.

* @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.

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?

*/
const resolveValue = (value: string | null | undefined): T => {
// If the value is null, undefined, or not a string, return the default value.
if (value === null || value === undefined || typeof value !== 'string') {
Copy link
Member

Choose a reason for hiding this comment

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

Just a preference, but let's do this:

Suggested change
if (value === null || value === undefined || typeof value !== 'string') {
if (typeof value !== 'string') {

return defaultValue;
}

try {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need try?

// Determine the type of the default value and parse accordingly.
if (typeof defaultValue === 'number') {
if (value === '') {
console.warn(
Copy link
Member

Choose a reason for hiding this comment

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

This can happen. So, at least it should be DEV-only warning.

return parsed as T;
}

console.warn(`Expected a number for key "${key}", got "${value}".`);
Copy link
Member

Choose a reason for hiding this comment

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

Same for this.

Comment on lines +69 to +81
// Handle object types (assume JSON if defaultValue is an object)
if (typeof defaultValue === 'object') {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === 'object') {
return parsed as T;
}
console.warn(`Expected an object for key "${key}", got "${value}".`);
return defaultValue;
}
} catch (err) {
console.error(`Failed to resolve value for key "${key}":`, err);
return defaultValue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove handling object. It's not type safe.

Comment on lines +83 to +85
// Fallback to default value for unsupported types
console.warn(`Unsupported defaultValue type for key "${key}".`);
return defaultValue;
Copy link
Member

Choose a reason for hiding this comment

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

Let's throw an error. We shouldn't reach here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants