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: support server redirects #491

Open
wants to merge 1 commit into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,13 +818,20 @@ import { Router } from "wouter";

const handleRequest = (req, res) => {
// top-level Router is mandatory in SSR mode
// pass an optional context object to handle redirects on the server
const ssrContext = {};
const prerendered = renderToString(
<Router ssrPath={req.path} ssrSearch={req.search}>
<Router ssrPath={req.path} ssrSearch={req.search} ssrContext={ssrContext}>
<App />
</Router>
);

// respond with prerendered html
if (ssrContext.redirectTo) {
// encountered redirect
res.redirect(ssrContext.redirectTo);
} else {
// respond with prerendered html
}
};
```

Expand Down
7 changes: 7 additions & 0 deletions packages/wouter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const defaultRouter = {
// this option is used to override the current location during SSR
ssrPath: undefined,
ssrSearch: undefined,
// optional context to track render state during SSR
ssrContext: undefined,
// customizes how `href` props are transformed for <Link />
hrefs: (x) => x,
};
Expand Down Expand Up @@ -319,11 +321,16 @@ export const Redirect = (props) => {
const { to, href = to } = props;
const [, navigate] = useLocation();
const redirect = useEvent(() => navigate(to || href, props));
const { ssrContext } = useRouter();

// redirect is guaranteed to be stable since it is returned from useEvent
useIsomorphicLayoutEffect(() => {
redirect();
}, []); // eslint-disable-line react-hooks/exhaustive-deps

if (ssrContext) {
ssrContext.redirectTo = to;
}

return null;
};
15 changes: 15 additions & 0 deletions packages/wouter/test/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Redirect,
useSearch,
useLocation,
SsrContext,
} from "wouter";

describe("server-side rendering", () => {
Expand Down Expand Up @@ -73,6 +74,20 @@ describe("server-side rendering", () => {
expect(rendered).toBe("");
});

it("update ssr context", () => {
const context: SsrContext = {};
const App = () => (
<Router ssrPath="/" ssrContext={context}>
<Route path="/">
<Redirect to="/foo" />
</Route>
</Router>
);

renderToStaticMarkup(<App />);
expect(context.redirectTo).toBe("/foo");
});

describe("rendering with given search string", () => {
it("is empty when not specified", () => {
const PrintSearch = () => <>{useSearch()}</>;
Expand Down
7 changes: 7 additions & 0 deletions packages/wouter/types/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export interface RouterObject {
readonly hrefs: HrefsFormatter;
}

// state captured during SSR render
export type SsrContext = {
// if a redirect was encountered, this will be populated with the path
redirectTo?: Path;
};

// basic options to construct a router
export type RouterOptions = {
hook?: BaseLocationHook;
Expand All @@ -32,5 +38,6 @@ export type RouterOptions = {
parser?: Parser;
ssrPath?: Path;
ssrSearch?: SearchString;
ssrContext?: SsrContext;
hrefs?: HrefsFormatter;
};
Loading