diff --git a/README.md b/README.md index e999048..3008d83 100644 --- a/README.md +++ b/README.md @@ -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( - + ); - // respond with prerendered html + if (ssrContext.redirectTo) { + // encountered redirect + res.redirect(ssrContext.redirectTo); + } else { + // respond with prerendered html + } }; ``` diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js index 795abed..67c123e 100644 --- a/packages/wouter/src/index.js +++ b/packages/wouter/src/index.js @@ -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 hrefs: (x) => x, }; @@ -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; }; diff --git a/packages/wouter/test/ssr.test.tsx b/packages/wouter/test/ssr.test.tsx index add2273..5eb20f3 100644 --- a/packages/wouter/test/ssr.test.tsx +++ b/packages/wouter/test/ssr.test.tsx @@ -12,6 +12,7 @@ import { Redirect, useSearch, useLocation, + SsrContext, } from "wouter"; describe("server-side rendering", () => { @@ -73,6 +74,20 @@ describe("server-side rendering", () => { expect(rendered).toBe(""); }); + it("update ssr context", () => { + const context: SsrContext = {}; + const App = () => ( + + + + + + ); + + renderToStaticMarkup(); + expect(context.redirectTo).toBe("/foo"); + }); + describe("rendering with given search string", () => { it("is empty when not specified", () => { const PrintSearch = () => <>{useSearch()}; diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts index 370e206..0ea7f6a 100644 --- a/packages/wouter/types/router.d.ts +++ b/packages/wouter/types/router.d.ts @@ -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; @@ -32,5 +38,6 @@ export type RouterOptions = { parser?: Parser; ssrPath?: Path; ssrSearch?: SearchString; + ssrContext?: SsrContext; hrefs?: HrefsFormatter; };