Skip to content
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
107 changes: 67 additions & 40 deletions packages/sui-react-initial-props/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
# sui-react-initial-props

> Make your React pages to get initial props asynchronously both client and server

## Motivation

**sui-react-initial-props** offers a way to make your easily your app isomorphic.
* Offers same parameters for your getInitialProps in the client in the server to make your app 100% universal.
* Avoid re-renders as other options like React-Transmit causing a longer time to respond, specially in the server.
* Minimal footprint by focusing on the really need stuf.

- Offers same parameters for your getInitialProps in the client in the server to make your app 100% universal.
- Avoid re-renders as other options like React-Transmit causing a longer time to respond, specially in the server.
- Minimal footprint by focusing on the really need stuf.

![example]

## Usage

```js
import loadPage from '@s-ui/react-initial-props/lib/loadPage'
// contextFactory is not used anymore but the param is needed to be present for compatibility reasons
const contextFactory = null

// Optional logger for error handling
const logger = {
error: (message, error) => {
console.error(message, error)
// Send to your logging service
}
}

// use the loadPage from the sui-react-initial-props
const loadHomePage = loadPage(contextFactory,
() => import(/* webpackChunkName: "HomePage" */ './pages/Home')
const loadHomePage = loadPage(
() => import(/* webpackChunkName: "HomePage" */ './pages/Home'),
logger // optional: for server-side error logging
)

export default (
Expand Down Expand Up @@ -101,7 +110,6 @@ Page.keepMounted = true
Page.renderLoading = () => <h1>Loading...</h1>
```


## Installation

```sh
Expand All @@ -116,65 +124,84 @@ Create the params for the contextFactory on the client

##### Response

Field | Type | Description
--- | --- | ---
cookies | `string` | All the cookies of the user
isClient | `boolean` | Useful to know in your contextFactory if you're in the client
pathName | `string` | Current path of the url requested
userAgent | `string` | Information of the browser, device and version in raw
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------- |
| cookies | `string` | All the cookies of the user |
| isClient | `boolean` | Useful to know in your contextFactory if you're in the client |
| pathName | `string` | Current path of the url requested |
| userAgent | `string` | Information of the browser, device and version in raw |

#### createServerContextFactoryParams({ req })

Create the params for the contextFactory on the server

##### Params

Field | Type | Description
--- | --- | ---
req | `object` | [Native Node Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) with any customized property added on your middleware
| Field | Type | Description |
| ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| req | `object` | [Native Node Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) with any customized property added on your middleware |

##### Response

Field | Type | Description
--- | --- | ---
cookies | `string` | All the cookies of the user
isClient | `boolean` | Useful to know in your contextFactory if you're in the client
pathName | `string` | Current path of the url requested
req | `object` | [Native Node Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) with any customized property added on your middleware
userAgent | `string` | Information of the browser, device and version in raw
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| cookies | `string` | All the cookies of the user |
| isClient | `boolean` | Useful to know in your contextFactory if you're in the client |
| pathName | `string` | Current path of the url requested |
| req | `object` | [Native Node Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) with any customized property added on your middleware |
| userAgent | `string` | Information of the browser, device and version in raw |

#### ssrComponentWithInitialProps({ Target, context, renderProps })

This method, retrieves the component page with the `getInitialProps` method, executes the async method and when it receives the info, then render to a string using the `Target` component and passing down the `context`.

##### Params

Field | Type | Description
--- | --- | ---
Target | `React Element` | React Element to be used for passing the context and render the app on it.
context | `object` | Context to be passed to the Target component and to the `getInitialProps`
renderProps | `object` | Props used by React Router with some useful info. We're extracting the pageComponent from it
| Field | Type | Description |
| ----------- | --------------- | -------------------------------------------------------------------------------------------- |
| Target | `React Element` | React Element to be used for passing the context and render the app on it. |
| context |  `object` | Context to be passed to the Target component and to the `getInitialProps` |
| renderProps | `object` | Props used by React Router with some useful info. We're extracting the pageComponent from it |

##### Response

The response is a promise resolved with two parameters. In addition, you can define an optional `__HTTP__` object in `initialProps` to allow server side redirects using SUI-SSR:

Field | Type | Description
--- | --- | ---
initialProps | `object` | Result of executing the `getInitialProps` of the pageComponent.
initialprops.__HTTP__ | `object` | An optional object containing a `redirectTo` key where an url might be included to allow 3XX server side redirects using [sui-ssr]. By default, redirect status code is 301, but you may set a valid `redirectStatusCode` option set in the file `@s-ui/ssr/status-codes`, an optional `httpCookie` key where you will define an object with the key/value of the `Http-Cookie` to be set from server and an optional `headers` key array of objects where you will define a custom response headers (see https://github.com/SUI-Components/sui/tree/master/packages/sui-ssr)
reactString | `string` | String with the renderized app ready to be sent.
| Field | Type | Description |
| --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| initialProps | `object` | Result of executing the `getInitialProps` of the pageComponent. |
| initialprops.**HTTP** | `object` | An optional object containing a `redirectTo` key where an url might be included to allow 3XX server side redirects using [sui-ssr]. By default, redirect status code is 301, but you may set a valid `redirectStatusCode` option set in the file `@s-ui/ssr/status-codes`, an optional `httpCookie` key where you will define an object with the key/value of the `Http-Cookie` to be set from server and an optional `headers` key array of objects where you will define a custom response headers (see https://github.com/SUI-Components/sui/tree/master/packages/sui-ssr) |
| reactString | `string` | String with the renderized app ready to be sent. |

#### loadPage(contextFactory, importPage)
#### loadPage(importPage, logger?)

Load the page asynchronously by using React Router and resolving the getInitialProps. On the client it prepare the component to show the `renderLoading` (if specified) of the component.
Load the page asynchronously by using React Router and resolving the getInitialProps. On the client it prepare the component to show the `renderLoading` (if specified) of the component. On the server, it wraps `getInitialProps` execution in a try-catch block to prevent crashes.

##### Params

Field | Type | Description
--- | --- | ---
contextFactory | `function` | Context factory method to create the context that will be used on the app.
importPage | `function` | Import the chunk of the page
| Field | Type | Description |
| ---------- | ------------------- | ---------------------------------------------------------------------------------------------------------- |
| importPage | `function` | Import the chunk of the page |
| logger | `object` (optional) | Optional logger object with an `error(message: string, error: Error)` method for server-side error logging |

##### Error Handling

When `getInitialProps` throws an error on the server:

- The error is caught and logged using the provided `logger` (if available)
- An object `{error: 'Error loading the page'}` with the error message is returned instead of propagating the error
- This prevents the SSR process from crashing

Example logger implementation:

```js
const logger = {
error: (message, error) => {
console.error(message, error)
// Send to Sentry, DataDog, etc.
}
}
```

## Contributing

Expand Down
43 changes: 25 additions & 18 deletions packages/sui-react-initial-props/src/loadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
import {useContext} from 'react'

import InitialPropsContext from './initialPropsContext'
import {
type ClientPageComponent,
type DoneImportingPageCallback,
type ReactRouterTypes,
type WithInitialPropsComponent
} from './types'
import {type ClientPageComponent, type ReactRouterTypes, type WithInitialPropsComponent} from './types'
import withInitialProps from './withInitialProps'

const EMPTY_GET_INITIAL_PROPS = async (): Promise<object> => ({})

interface Logger {
error: (message: string, error: Error) => void
}

const createUniversalPage =
(routeInfo: ReactRouterTypes.RouteInfo) =>
(routeInfo: ReactRouterTypes.RouteInfo, logger?: Logger) =>
async ({default: Page}: {default: ClientPageComponent}) => {
// 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
Expand All @@ -23,13 +22,15 @@ const createUniversalPage =
// 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: WithInitialPropsComponent = (props: object) => {
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
Expand All @@ -38,18 +39,24 @@ const createUniversalPage =
context: object,
req: IncomingMessage.ServerRequest,
res: IncomingMessage.ClientResponse
) => await Page.getInitialProps({context, routeInfo, req, res})
) => {
try {
return await Page.getInitialProps({context, routeInfo, req, res})
} catch (error) {
const message = 'Error executing getInitialProps on server'

logger?.error?.(message, error as Error)

return {error: error instanceof Error ? error.message : message}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just throw an error so we do not change the previous behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, maybe it's better to log the error and then throw it instead of returning a new object, I'm doing some tests locally before

}
}

// 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 (_: any, importPage: () => Promise<any>) =>
async (routeInfo: ReactRouterTypes.RouteInfo, done: DoneImportingPageCallback) => {
importPage()
.then(createUniversalPage(routeInfo))
.then(Page => {
done(null, Page)
})
}
export default (importPage: () => Promise<any>, logger?: Logger) => async (routeInfo: ReactRouterTypes.RouteInfo) => {
await importPage()

return createUniversalPage(routeInfo, logger)
}
Loading