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(packages/sui-react-initial-props): shallow routing poc #1737

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ContextFactoryParams } from './types'

export default (): ContextFactoryParams => ({
export default () => ({
appConfig: window.__APP_CONFIG__,
cookies: document.cookie,
isClient: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ContextFactoryParams } from './types'

export default (req: IncomingMessage.ServerRequest): ContextFactoryParams => ({
export default req => ({
appConfig: req.appConfig,
req,
cookies: req.headers.cookie,
Expand Down
4 changes: 4 additions & 0 deletions packages/sui-react-initial-props/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {default as createClientContextFactoryParams} from './createClientContextFactoryParams.js'
export {default as createServerContextFactoryParams} from './createServerContextFactoryParams.js'
export {default as loadPage} from './loadPage.js'
export {default as ssrComponentWithInitialProps} from './ssrComponentWithInitialProps.js'
4 changes: 0 additions & 4 deletions packages/sui-react-initial-props/src/index.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/sui-react-initial-props/src/initialPropsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {createContext} from 'react'

export default createContext({initialProps: {}})
3 changes: 0 additions & 3 deletions packages/sui-react-initial-props/src/initialPropsContext.ts

This file was deleted.

44 changes: 44 additions & 0 deletions packages/sui-react-initial-props/src/loadPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import {useContext} from 'react'

import InitialPropsContext from './initialPropsContext.js'
import withInitialProps from './withInitialProps.js'

const EMPTY_GET_INITIAL_PROPS = async () => ({})

const createUniversalPage =
routeInfo =>
async ({default: Page}) => {
// check if the Page page has a getInitialProps, if not put a resolve with an empty object
Page.getInitialProps = typeof Page.getInitialProps === 'function' ? Page.getInitialProps : EMPTY_GET_INITIAL_PROPS

// CLIENT
if (typeof window !== 'undefined') {
// let withInitialProps HOC handle client getInitialProps logic
return Promise.resolve(withInitialProps(Page))
}
// SERVER
// Create a component that gets the initialProps from context
// this context has been created on the `ssrWithComponentWithInitialProps`
const ServerPage = props => {
const {initialProps} = useContext(InitialPropsContext)
return <Page {...props} {...initialProps} />
}
// recover the displayName from the original page
ServerPage.displayName = Page.displayName
// detect if the page has getInitialProps and wrap it with the routeInfo
// if we don't have any getInitialProps, just use a empty function returning an empty object
ServerPage.getInitialProps = (context, req, res) => Page.getInitialProps({context, routeInfo, req, res})
// return the component to be used on the server
return ServerPage
}

// TODO: Remove this method on next major as it's using unnecessary contextFactory param
// and unnecesary calling done method instead relying on promises
export default (_, importPage) => async (routeInfo, done) => {
importPage()
.then(createUniversalPage(routeInfo))
.then(Page => {
done(null, Page)
})
}
49 changes: 0 additions & 49 deletions packages/sui-react-initial-props/src/loadPage.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { renderToNodeStream, renderToString } from 'react-dom/server'
import {renderToNodeStream, renderToString} from 'react-dom/server'

import InitialPropsContext from './initialPropsContext'
import { InitialProps, SsrComponentWithInitialPropsParams } from './types'
import InitialPropsContext from './initialPropsContext.js'

const hrTimeToMs = (diff: [number, number]): number => diff[0] * 1e3 + diff[1] * 1e-6
const hrTimeToMs = diff => diff[0] * 1e3 + diff[1] * 1e-6

export default async function ssrComponentWithInitialProps ({
export default async function ssrComponentWithInitialProps({
Target,
context,
req,
res,
renderProps,
useStream = false
}: SsrComponentWithInitialPropsParams): Promise<object> {
}) {
const startGetInitialProps = process.hrtime()
// use the getInitialProps from the page to retrieve the props to initialize
const { getInitialProps } =
renderProps.components[renderProps.components.length - 1]
const {getInitialProps} = renderProps.components[renderProps.components.length - 1]

const initialProps: InitialProps = await getInitialProps(context, req, res)
const initialProps = await getInitialProps(context, req, res)
const diffGetInitialProps = process.hrtime(startGetInitialProps)
const { __HTTP__: http } = initialProps
const {__HTTP__: http} = initialProps

if (http?.redirectTo !== undefined) {
return {
Expand All @@ -34,7 +32,7 @@ export default async function ssrComponentWithInitialProps ({

// Create App with Context with the initialProps
const AppWithContext = (
<InitialPropsContext.Provider value={{ initialProps }}>
<InitialPropsContext.Provider value={{initialProps}}>
<Target {...renderProps} initialProps={initialProps} />
</InitialPropsContext.Provider>
)
Expand All @@ -45,7 +43,7 @@ export default async function ssrComponentWithInitialProps ({
// start to calculate renderToString
const startRenderToString = process.hrtime()
// render with the needed action
const renderResponse = { [renderResponseKey]: renderAction(AppWithContext) }
const renderResponse = {[renderResponseKey]: renderAction(AppWithContext)}
// calculate the difference of time used rendering
const diffRenderToString = process.hrtime(startRenderToString)
// return all the info
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import { useContext, useEffect, useRef, useState } from 'react'
import {useContext, useEffect, useRef, useState} from 'react'

import SUIContext from '@s-ui/react-context'
import { RouteInfo } from '@s-ui/react-router/src/types'

import {
ClientPageComponent,
InitialProps,
WithInitialPropsComponent
} from './types'

const INITIAL_PROPS_KEY = '__INITIAL_PROPS__'

// used to store the last definition of ClientPage component so it can be reused
let latestClientPage: WithInitialPropsComponent
let latestClientPage

const getInitialPropsFromWindow = (): object | undefined => {
const getInitialPropsFromWindow = () => {
// if no window initial props, then do nothing
if (typeof window[INITIAL_PROPS_KEY] === 'undefined') return
// return retrieved props from window
return window[INITIAL_PROPS_KEY]
}

// extract needed info from props for routeInfo object
const createRouteInfoFromProps = ({ location, params, routes }: RouteInfo): RouteInfo => ({
const createRouteInfoFromProps = ({location, params, routes}) => ({
location,
params,
routes
Expand All @@ -38,88 +31,77 @@ const createRouteInfoFromProps = ({ location, params, routes }: RouteInfo): Rout
// and the very same component is matched by the router, so PageComponent just
// gets props updated. Also, since PageComponent keeps mounted it will receive
// an `isLoading` prop while getInitialProps is in progress.
export default (Page: ClientPageComponent): WithInitialPropsComponent => {
const { keepMounted = false } = Page
export default Page => {
// gather window initial props for this Page, if present
const windowInitialProps = getInitialPropsFromWindow()
// remove the variable of the window
window[INITIAL_PROPS_KEY] = null

// define Page wrapper component
const ClientPage: WithInitialPropsComponent = (props: RouteInfo & object) => {
const ClientPage = props => {
const initialPropsFromWindowRef = useRef(windowInitialProps)
// used to know if initialProps has been requested at least once
const requestedInitialPropsOnceRef = useRef(windowInitialProps != null)
// create routeInfo object from current props which are updated
const routeInfo = createRouteInfoFromProps(props)
// consume sui context from the context provider
const suiContext: object = useContext(SUIContext)
const suiContext = useContext(SUIContext)
// pathName from context is outdated, so we update it from routeInfo
const context = { ...suiContext, pathName: routeInfo.location.pathname }
const context = {...suiContext, pathName: routeInfo.location.pathname}

const [{ initialProps, isLoading }, setState] = useState(() => ({
const [{initialProps, isLoading}, setState] = useState(() => ({
initialProps: initialPropsFromWindowRef.current ?? {},
isLoading: initialPropsFromWindowRef.current == null
}))

useEffect(() => {
// check if got initial props from window, because then there's no need
// to request them again from client

if (initialPropsFromWindowRef.current != null) {
initialPropsFromWindowRef.current = undefined
} else {
// only update state if already request initial props
if (requestedInitialPropsOnceRef.current) {
setState({ initialProps, isLoading: true })
if (!routeInfo.location.state?.shallow) {
// only update state if already request initial props
if (requestedInitialPropsOnceRef.current) {
setState({initialProps, isLoading: true})
}

Page.getInitialProps({context, routeInfo})
.then(initialProps => {
const {__HTTP__: http} = initialProps

if (http?.redirectTo !== undefined) {
window.location = http.redirectTo
return
}

setState({initialProps, isLoading: false})
})
.catch(error => {
setState({initialProps: {error}, isLoading: false})
})
.finally(() => {
if (requestedInitialPropsOnceRef.current) return
requestedInitialPropsOnceRef.current = true
})
}

Page.getInitialProps({ context, routeInfo })
.then((initialProps: InitialProps) => {
const { __HTTP__: http } = initialProps

if (http?.redirectTo !== undefined) {
window.location = http.redirectTo
return
}

setState({ initialProps, isLoading: false })
})
.catch((error: Error) => {
setState({ initialProps: { error }, isLoading: false })
})
.finally(() => {
if (requestedInitialPropsOnceRef.current) return
requestedInitialPropsOnceRef.current = true
})
}
}, [routeInfo.location]) // eslint-disable-line react-hooks/exhaustive-deps

const renderPage = (): any => (
<Page {...initialProps} {...props} isLoading={isLoading} />
)

// if the page has a `keepMounted` property and already requested
// initialProps once, just keep rendering the page
if (keepMounted && requestedInitialPropsOnceRef.current) {
return renderPage()
}
const renderPage = () => <Page {...initialProps} {...props} isLoading={isLoading} />

const renderLoading = (): React.ElementType<any> | null => {
const renderLoading = () => {
// check if the page has a `renderLoading` method, if not, just render nothing
return (Page.renderLoading != null)
? Page.renderLoading({ context, routeInfo })
: null
return Page.renderLoading != null ? Page.renderLoading({context, routeInfo}) : null
}

return isLoading ? renderLoading() : renderPage()
}

// if `keepMounted` property is found and the component is the same one,
// we just reuse it instead of returning a new one
if (
keepMounted &&
Page.displayName === latestClientPage?.Page?.displayName
) {
if (Page.displayName === latestClientPage?.Page?.displayName) {
return latestClientPage
}

Expand Down
Loading