Compare commits

...

2 commits

Author SHA1 Message Date
Fran Dios 3fdcf6815b feat: SPA redirect 2021-11-09 18:22:24 +09:00
Fran Dios 293a065ff7 feat: basic redirect support 2021-11-09 17:30:11 +09:00
4 changed files with 81 additions and 8 deletions

View file

@ -1,7 +1,7 @@
import React, {Suspense, useState} from 'react';
// @ts-ignore
import {createRoot} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import {BrowserRouter, Redirect} from 'react-router-dom';
import type {ClientHandler} from './types';
import {ErrorBoundary} from 'react-error-boundary';
import {HelmetProvider} from 'react-helmet-async';
@ -35,7 +35,8 @@ function Content({clientWrapper: ClientWrapper}: {clientWrapper: any}) {
pathname: window.location.pathname,
search: window.location.search,
});
const response = useServerResponse(serverState);
const response = useServerResponse(serverState).read();
return (
<ServerStateProvider
@ -46,8 +47,12 @@ function Content({clientWrapper: ClientWrapper}: {clientWrapper: any}) {
<HelmetProvider>
<BrowserRouter>
<ServerStateRouter />
{/* @ts-ignore */}
<ClientWrapper>{response.read()}</ClientWrapper>
{response.redirect ? (
<Redirect to={response.redirect} />
) : (
/* @ts-ignore */
<ClientWrapper>{response}</ClientWrapper>
)}
</BrowserRouter>
</HelmetProvider>
</QueryProvider>

View file

@ -20,6 +20,7 @@ import {ServerComponentResponse} from './framework/Hydration/ServerComponentResp
import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server';
import {dehydrate} from 'react-query/hydration';
import {getCacheControlHeader} from './framework/cache';
import type {ServerResponse} from 'http';
/**
* react-dom/unstable-fizz provides different entrypoints based on runtime:
@ -118,6 +119,14 @@ const renderHydrogen: ServerHandler = (App, hook) => {
componentResponse.cacheControlHeader
);
if (componentResponse.customHead) {
writeHeadToServerResponse(componentResponse, response);
if (response.statusCode >= 300 && response.statusCode < 400) {
// Redirect
return response.end();
}
}
if (!componentResponse.canStream()) return;
response.statusCode = didError ? 500 : 200;
@ -205,6 +214,15 @@ const renderHydrogen: ServerHandler = (App, hook) => {
* `template` and `script` tags inserted and rendered as part of the hydration response.
*/
onCompleteAll() {
if (componentResponse.customHead) {
writeHeadToServerResponse(componentResponse, response);
if (response.statusCode >= 300 && response.statusCode < 400) {
// Redirect: mock status to bypass fetch opaque responses
response.statusCode = 299;
return response.end();
}
}
// Tell React to start writing to the writer
startWriting();
@ -432,4 +450,23 @@ async function renderAppFromStringWithPrepass(
: body;
}
export function writeHeadToServerResponse(
{customHead = {}}: ServerComponentResponse,
serverResponse: ServerResponse
) {
if (customHead.headers) {
for (const [key, value] of Object.entries(customHead.headers)) {
serverResponse.setHeader(key, value);
}
}
if (customHead.statusText) {
serverResponse.statusMessage = customHead.statusText;
}
if (customHead.status) {
serverResponse.statusCode = customHead.status;
}
}
export default renderHydrogen;

View file

@ -35,10 +35,13 @@ function createFromFetch(fetchPromise: Promise<any>) {
if (!response.ok) {
throw new Error(`Hydration request failed: ${response.statusText}`);
}
return response.text();
})
.then((payload) => {
return convertHydrationResponseToReactComponents(payload);
// Mocked status to bypass fetch opaque responses
if (response.status === 299) {
return {redirect: response.headers.get('location')};
}
return response.text().then(convertHydrationResponseToReactComponents);
})
.catch((e) => {
console.error(e);

View file

@ -2,10 +2,18 @@ import {renderToString} from 'react-dom/server';
import {CacheOptions} from '../../types';
import {generateCacheControlHeader} from '../cache';
type Head = {
status?: number;
statusText?: string;
headers?: Record<string, any>;
};
export class ServerComponentResponse extends Response {
private wait = false;
private cacheOptions?: CacheOptions;
public customHead: Head | undefined;
/**
* Allow custom body to be a string or a Promise.
*/
@ -36,6 +44,26 @@ export class ServerComponentResponse extends Response {
return generateCacheControlHeader(options);
}
writeHead({status, statusText, headers}: Head = {}) {
this.customHead = this.customHead || {};
if (status) {
this.customHead.status = status;
}
if (statusText) {
this.customHead.statusText = statusText;
}
if (headers) {
this.customHead.headers = {...this.customHead.headers, ...headers};
}
}
redirect(status: number, location: string) {
this.writeHead({status, headers: {location}});
}
/**
* Send the response from a Server Component. Renders React components to string,
* and returns `null` to make React happy.