preact-router icon indicating copy to clipboard operation
preact-router copied to clipboard

Match and Link don't work server side with render-to-string

Open cjbest opened this issue 8 years ago • 9 comments
trafficstars

I'm using preact-router in a component that can be rendered client or server side.

Server side, I set the url= prop on the router, and it makes the correct children get rendered. But Match (and therefore Link) don't take this into account. Simple repro:

import {h} from 'preact';
import Router from 'preact-router';
import Match from 'preact-router/match';
import render from 'preact-render-to-string';

const vdom = (
    <Router url="/foo">
        <div path="/foo">
            Path works
            <Match path="/foo">
                { ({matches, path, url}) => (
                    <div>
                        <p>Matches: {matches || '?'}</p>
                        <p> Path: {path || '?'}</p>
                        <p>Url: {url || '?'}</p>
                    </div>
                ) }
            </Match>
        </div>
        <div default>
            Path doesn't work.
        </div>
    </Router>
);

console.log(render(vdom));

formatted output:

<div path="/foo" url="/foo" matches="[object Object]">Path works
    <div>
        <p>Matches: ?</p>
        <p> Path: ?</p>
        <p>Url: ?</p>
    </div>
</div>

Would be nice if either:

  1. The URL from the router would bubble down or
  2. There was some other way to set the URL that worked server side

Am I missing an obvious right way to do this? If not, if somebody has a strong idea of how this should work I could offer a PR

cjbest avatar Jul 16 '17 20:07 cjbest

Agreed! It would be nice if the URL bubbled down for sure. The only workaround I'm aware of right now is to pass a custom history to Router to tell is the location:

const customHistory = {
  getCurrentLocation: () => "/foo"
};
const vdom = (
    <Router url="/foo" history={customHistory}>
        <div path="/foo">
            Path works
            <Match path="/foo">
                { ({matches, path, url}) => (
                    <div>
                        <p>Matches: {matches || '?'}</p>
                        <p> Path: {path || '?'}</p>
                        <p>Url: {url || '?'}</p>
                    </div>
                ) }
            </Match>
        </div>
        <div default>
            Path doesn't work.
        </div>
    </Router>
);

developit avatar Jul 17 '17 01:07 developit

Thanks, this is a big help! I had to tweak it a little to make it work:

const customHistory = {
  location: {pathname: "/foo"} // <-- CHANGED
};
const vdom = (
    <Router url="/foo" history={customHistory}>
        <div path="/foo">
            Path works
            <Match path="/foo">
                { ({matches, path, url}) => (
                    <div>
                        <p>Matches: {matches || '?'}</p>
                        <p> Path: {path || '?'}</p>
                        <p>Url: {url || '?'}</p>
                    </div>
                ) }
            </Match>
        </div>
        <div default>
            Path doesn't work.
        </div>
    </Router>
);

But that does the trick!

cjbest avatar Aug 05 '17 22:08 cjbest

Awesome! Let's keep this issue open to see if we can make it work without the custom history.

developit avatar Sep 08 '17 22:09 developit

Hey guys,

Stumbled upon this, just now. Seems like this should've not work, to start with. There's a customHistory.listen which is needed here: https://github.com/developit/preact-router/blob/master/src/index.js#L191 and missing from the above examples. Trying to wrap my head around it, can you guys clear it up? Do I need the .listen to be defined? or maybe the listen method should be optional?

Cheers!

cristianbote avatar Nov 27 '17 12:11 cristianbote

cc @developit sorry for the ping 😄

cristianbote avatar Dec 14 '17 14:12 cristianbote

.listen() can just be an empty function - SSR is a single-pass render, so listening can't respond anyway:

const customHistory = {
  location: { pathname: "/foo" },
  listen: () => {}
};

developit avatar Dec 25 '17 02:12 developit

And shouldn't even need that, given you won't hit componentDidMount for a static render?

cjbest avatar Dec 26 '17 14:12 cjbest

Hey guys, thanks for getting back!

@developit did that in the end, but the problem that I'm having right now, with undom and your vdom serialising gist, is that the activeClassName is not set on the first render.

@cjbest indeed with preact-render-to-string you won't have that called, but with undom, you pretty much hit it.

Gonna do some more digging and see if I can zero in the issue.

Thanks again!

cristianbote avatar Dec 26 '17 18:12 cristianbote

Alright, so I think I have some answers. I have the following:

<div>
  <NavigationWithLinks links={['/', '/my-page']} />
  <Router history={myCustomHistory}>
    <MyHomePage />
    <MyPage />
  </Router>
</div>

I think, the Link when going over the matches, does not have the customHistory yet, since that is set, globally, via the Router constructor.

I used the Link component outside of the Router. Is this not ok? I tried to avoid re-rendering/re-instantiating Link components, but I don't think that would happen, ever, since preact reuses the components. Paint flashing in devtools and console.logs proved it 😃.

Now, would it make sense to be able to use Link outside the Router? Wondering if we could have a container component, that could wrap your App element and that would set the global custom history? Does it make sense?

<CustomHistoryContainer history={history}>
  <NavigationWithLinks links={['/', '/my-page']} />
  <Router>
    <MyHomePage />
    <MyPage />
  </Router>
</CustomHistoryContainer>

Looking forward, cheers!

cristianbote avatar Dec 26 '17 22:12 cristianbote