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

feat(atom-with-hash): allow optional setHash #4

Merged
merged 3 commits into from
Dec 1, 2022

Conversation

V-Mann-Nick
Copy link
Contributor

Problem

Changing the hash using window.location can be in conflict with the next router. To reproduce this issue within a next app:

  1. Use atomWithHash somewhere in the app using { replaceState: true }
  2. Navigate to the page with atomWithHash (using next router)
  3. Trigger a change that will invoke setting the hash via atomWithHash
  4. Navigate in browser going back in history
  5. Try and navigate in browser going forward
  6. Will behave unexpectedly changing the URL, but not changing the page

This might also be unexpected behavior from the perspective of the next router. I'm not entirely sure. But I can imagine other routers possibly also having problems with this.

Solution

Changing the setHash function fixed this issue so my proposed solution using this commit's changes are:

import Router from 'next/router'
import { atomWithHash } from 'jotai-location'

atomWithHash('key', 'default', {
  subscribe: (callback) => {
    Router.events.on('hashChangeComplete', callback)
    return () => {
      Router.events.off('hashChangeComplete', callback)
    }
  },
  setHash: (searchParams) => {
    Router.replace({ hash: searchParams.toString() })
  },
})

What I don't like about these changes

I'm not entirely happy with this API as using replaceState with setHash makes replaceState unused. But for now I wanted to make minimal changes.

what do you think?

@codesandbox-ci
Copy link

codesandbox-ci bot commented Nov 24, 2022

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.

Latest deployment of this branch, based on commit 864ff8a:

Sandbox Source
React Configuration
React Typescript Configuration

@dai-shi
Copy link
Member

dai-shi commented Nov 24, 2022

Hi, thanks so much for making suggestions for this library!
I wasn't aware the issue with nextjs. Wonder if there are some other nextjs users out there.
Thought replaceState option was suggested by one of nextjs users.
Anyway, if it doesn't work, we are happy to add an option to support such cases.

But for now I wanted to make minimal changes.

I think we can be creative to have new ideas.
Maybe:

setHash?: 'default' | 'replaceState' | ((params: URLSearchParams) => void)

params can be string.

We still could have replaceState option with a deprecation message for a while before completely removing it.

Would be nice to know how you and others think.

Let me pin some users who has contributed to atomWithHash before in https://github.com/pmndrs/jotai/pulls?q=atomWithHash.
@notsidney @austinwoon @jakkku

@V-Mann-Nick
Copy link
Contributor Author

V-Mann-Nick commented Nov 24, 2022

I think I've figured out what might be going wrong with the Next router when setting the hash in the default setHash.

When replaceState = false

Here the hash is set with window.location.hash = hash which will trigger the hashchange event. I would assume that the next router is listening to that and in this case it works as expected.

When replaceState = true

Here the hash is set with window.history.replaceState. This unfortunately does not cause a hashchange event as described here (this is the docs for pushState but the behavior is the same as this Stackoverflow answer says).

I would assume it has something to do with this behavior. Not sure.

Regarding your API proposal

I think that would make sense and would make the connection between setHash and replaceState more clear. If others like this, I can make the appropriate changes.

Regarding my example

The above example using with the Next router was incomplete and will break in case searchParams.toString() is empty.
My altered example with the next router would be:

import Router from 'next/router'
import { atomWithHash } from 'jotai-location'

atomWithHash('key', 'default', {
  subscribe: (callback) => {
    Router.events.on('hashChangeComplete', callback)
    return () => {
      Router.events.off('hashChangeComplete', callback)
    }
  },
  setHash: (searchParams) => {
    const hash = searchParams.toString()
    Router.replace({ pathname: Router.pathname, query: Router.query, hash })
  },
})

@austinwoon
Copy link

austinwoon commented Nov 25, 2022

I think I've figured out what might be going wrong with the Next router when setting the hash in the default setHash.

When replaceState = false

Here the hash is set with window.location.hash = hash which will trigger the hashchange event. I would assume that the next router is listening to that and in this case it works as expected.

When replaceState = true

Here the hash is set with window.history.replaceState. This unfortunately does not cause a hashchange event as described here (this is the docs for pushState but the behavior is the same as this Stackoverflow answer says).

I would assume it has something to do with this behavior. Not sure.

Regarding your API proposal

I think that would make sense and would make the connection between setHash and replaceState more clear. If others like this, I can make the appropriate changes.

Regarding my example

The above example using with the Next router was incomplete and will break in case searchParams.toString() is empty. My altered example with the next router would be:

import Router from 'next/router'
import { atomWithHash } from 'jotai-location'

atomWithHash('key', 'default', {
  subscribe: (callback) => {
    Router.events.on('hashChangeComplete', callback)
    return () => {
      Router.events.off('hashChangeComplete', callback)
    }
  },
  setHash: (searchParams) => {
    const hash = searchParams.toString()
    Router.replace({ pathname: Router.pathname, query: Router.query, hash })
  },
})

Nothing much to add but just that this checks out. Looks like NextJS's router logic subscribes to their own hashChangeComplete event when changes are made to circumvent the missing event on replaceState. Router.replace emitshashChangeComplete after changing the hash.

I like dai-shi's signature and his idea of keeping replaceState as a separate option before removing it.

@dai-shi
Copy link
Member

dai-shi commented Nov 28, 2022

Sounds good.
@V-Mann-Nick Would you like to work on it?

@V-Mann-Nick
Copy link
Contributor Author

Sure will do. I will make the edits to this PR by end of week.

## Problem

Changing the hash using window.location can be in conflict with the next
router. To reproduce this issue within a next app:

1. Use `atomWithHash` somewhere in the app using `{ replaceState: true }`
2. Navigate to the page with `atomWithHash` (using next router)
3. Trigger a change that will invoke setting the hash via `atomWithHash`
3. Navigate in browser going back in history
4. Try and navigate in browser going forward
5. Will behave unexpectedly changing the URL, but not changing the page

This might also be unexpected behavior from the perspective of the next
router. I'm not entirely sure. But I can imagine other routers possibly
also having problems with this.

## Solution

Changing the `setHash` function fixed this issue so my proposed solution
using these commit's changes are:

```ts
import Router from 'next/router';
import { atomWithHash } from 'jotai-location';

atomWithHash('key', 'default', {
  subscribe: (callback) => {
    Router.events.on('hashChangeComplete', callback);
    return () => {
      Router.events.off('hashChangeComplete', callback);
    };
  },
  setHash: (searchParams) => {
    Router.replace({
      pathname: Router.pathname,
      query: Router.query,
      hash: searchParams,
    });
  },
});
```

## What I don't like about these changes

I'm not entirely happy with this API as using `replaceState` with
`setHash` makes `replaceState` unused. But for now I wanted to make
minimal changes.
As [proposed](jotaijs#4 (comment))
by @dai-shi the `replaceState` option will get a deprecation warning in
favor of using the `setHash` option which will default to pushing to
history with an option to replace history or a custom setHash function.

```ts
setHash?: 'default' | 'replaceState' | ((searchParams: string) => void)
```
@V-Mann-Nick V-Mann-Nick force-pushed the atom-with-hash-optional-set-hash branch from 420b18c to 28f3f6f Compare November 30, 2022 10:44
@V-Mann-Nick
Copy link
Contributor Author

@dai-shi I've made the changes as proposed.

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.

Looks very good!

src/atomWithHash.ts Show resolved Hide resolved
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
@dai-shi dai-shi merged commit df8060a into jotaijs:main Dec 1, 2022
@dai-shi
Copy link
Member

dai-shi commented Dec 1, 2022

Published: https://www.npmjs.com/package/jotai-location/v/0.3.0

Thanks for your contribution! Please feel free to work on any other improvements in coding, making examples, and documenting, if you are interested.

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.

None yet

3 participants