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: expose hash from useRouter #746

Merged
merged 24 commits into from
Jul 25, 2024
Merged

Conversation

pmelab
Copy link
Contributor

@pmelab pmelab commented Jun 13, 2024

What was changed?

Expose the current url hash via UNSTABLE_useRouter. On the server, its always an empty string, but gets populated on the client after the initial render.

Why was it changed?

Otherwise inconvenient useEffect-gymnastics are necessary to get the current hash reliably into a client component.

Notes

In the process of writing tests I found another potential issue with searchParams.

https://github.com/dai-shi/waku/pull/746/files#diff-a8dab8847d02ba73c6b66e738495556ddda5d2dd01b70322aae006f7669ea237R6-R11

I'm not sure if we should move the double-creation of URLSearchParams into the useRouter hook. Given some advice, I can try to fix that as well.

Copy link

vercel bot commented Jun 13, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
waku ✅ Ready (Inspect) Visit Preview Jul 25, 2024 2:48pm

Copy link

codesandbox-ci bot commented Jun 13, 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.

Copy link
Owner

@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 the suggestion!

packages/waku/src/router/common.ts Outdated Show resolved Hide resolved
export default function TestRouter() {
const router = useRouter_UNSTABLE();
// TODO: Why is this necessary? On the client `searchParams`
// is a URLSearchParams object, but on the server it's an array.
Copy link
Owner

Choose a reason for hiding this comment

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

I didn't know it's an array on the server. How can it be??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, but without that, it breaks with get is not a function. And the debugger showed me an array. Maybe node version differences?

Copy link
Owner

Choose a reason for hiding this comment

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

Can you dig into it further? If it's a bug in Waku, we would like to fix it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, took me some time to get back to this. I was not able to pinpoint whats the problem. This is what I have found.

At this point, searchParams is still an URLSearchParams object:

const input = getInputString(pathname);
const body = createElement(
ServerRouter as FunctionComponent<
Omit<ComponentProps<typeof ServerRouter>, 'children'>
>,
{ route: { path: pathname, searchParams } },
componentIds.reduceRight(
(acc: ReactNode, id) => createElement(Slot, { id, fallback: acc }, acc),
null,
),
);
return { input, body };

But when ServerRouter gets rendered, it has become []:

export function ServerRouter({
children,
route,
}: {
children: ReactNode;
route: RouteProps;
}) {
return createElement(
Fragment,
null,
createElement(
RouterContext.Provider,
{
value: {
route,
changeRoute: notAvailableInServer('changeRoute'),
prefetchRoute: notAvailableInServer('prefetchRoute'),
},
},
children,
),
);
}

My guess is that the route definition gets serialized by react when passed into the ServerRouter client component and the empty URLSearchParams map gets turned into []?

With ddb4086 I removed the additional construction of URLSearchParams to see if it also happens on CI, which it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dai-shi : i tried to find out more, but basically thats it. An empty URLSearchParams object gets passed as a prop into createElement and inside the ServerRouter its an empty array. It has to be reacts prop-handling. Maybe it would be safer to stringify searchParams?

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, you are absolutely right.

repro:

$ npm create waku@next
$ cd ...
$ npm i
$ cat foo.cjs
const { renderToPipeableStream } = require('react-server-dom-webpack/server');

const data = new URLSearchParams();

renderToPipeableStream(data).pipe(process.stdout);
$ node --conditions react-server foo.cjs
0:[]

I will open another PR to make it a string and see how it goes.

Copy link
Owner

Choose a reason for hiding this comment

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

@pmelab
Copy link
Contributor Author

pmelab commented Jun 14, 2024

To be transparent, this is the code that we save in the consumer:

  const router = useRouter();
  const [hash, setHash] = useState('');
  useEffect(() => {
    setHash(window.location.hash);
  }, [router]);

Its not gigantic, but useEffect has a bad reputation lately.

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Copy link
Owner

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

Sorry for the delay.
It looks good overall. Comments below.

export default function TestRouter() {
const router = useRouter_UNSTABLE();
// TODO: Why is this necessary? On the client `searchParams`
// is a URLSearchParams object, but on the server it's an array.
Copy link
Owner

Choose a reason for hiding this comment

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

Can you dig into it further? If it's a bug in Waku, we would like to fix it.

packages/waku/src/router/client.ts Outdated Show resolved Hide resolved
Comment on lines 324 to 328
...parseRoute(new URL(window.location.href)),
// Make sure there is no hash on the initial route,
// so it matches the server-rendered version. Otherwise
// there might be hydration errors.
hash: '',
Copy link
Owner

Choose a reason for hiding this comment

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

It looks a bit hacky. Maybe we make parseRoute to accept an option?

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
dai-shi added a commit that referenced this pull request Jul 24, 2024
#746 revealed that `URLSearchParams` doesn't work.

(In the near future, I will also rework on fetchRSC to eliminate it.)
@dai-shi
Copy link
Owner

dai-shi commented Jul 24, 2024

@pmelab Hi, can you merge main?

@dai-shi dai-shi mentioned this pull request Jul 25, 2024
76 tasks
Copy link
Owner

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

2nd review

packages/waku/src/router/client.ts Outdated Show resolved Hide resolved
@@ -301,8 +305,15 @@ const InnerRouter = ({ routerData }: { routerData: RouterData }) => {
const refetch = useRefetch();

const [route, setRoute] = useState(() =>
parseRoute(new URL(window.location.href)),
parseRoute(new URL(window.location.href), true),
Copy link
Owner

Choose a reason for hiding this comment

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

So, ignoreHash is used only here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, without this, there are hydration errors, because the server has no hash, but the first client render does.

Copy link
Owner

Choose a reason for hiding this comment

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

Oh, I see. Can you leave some comments in the code?
I might refactor things around here in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added some explanations there

packages/waku/src/router/client.ts Outdated Show resolved Hide resolved
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Copy link
Owner

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

3rd review.

// Update the route post-load to include the current hash.
useEffect(() => {
setRoute(parseRoute(new URL(window.location.href)));
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Owner

Choose a reason for hiding this comment

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

Hm, I wonder why we need the exception.


// Update the route post-load to include the current hash.
useEffect(() => {
setRoute(parseRoute(new URL(window.location.href)));
Copy link
Owner

Choose a reason for hiding this comment

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

This actually triggers re-renders every time, even if the hash is ''. We should probably improve it.

@dai-shi
Copy link
Owner

dai-shi commented Jul 25, 2024

working on it.

Copy link
Owner

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

Should be good.

@dai-shi dai-shi merged commit d921631 into dai-shi:main Jul 25, 2024
28 checks passed
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.

2 participants