Skip to content

Commit

Permalink
Merge pull request #7 from rambler-digital-solutions/getters-order
Browse files Browse the repository at this point in the history
feat: get meta data after the initial data fetching
  • Loading branch information
andrepolischuk authored Nov 8, 2023
2 parents c9caf4d + 3100e44 commit a3a1c4e
Show file tree
Hide file tree
Showing 13 changed files with 83 additions and 49 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,33 @@ MainPage.getMetaData = () => ({
export default MainPage
```

Getting the meta data is called after the data fetching, so the fetched data enriches the context passed to the `getMetaData`

```tsx
import React from 'react'
import {PageComponent} from '@rambler-tech/react-toolkit/client'

const MainPage: PageComponent = () => (
<div>
<h1>Main page</h1>
<p>...</p>
</div>
)

MainPage.getInitialData = async () => {
const {someProp} = await api.getSomeProp()

return {someProp}
}

MainPage.getMetaData = ({data}) => ({
title: `Main page: ${data.someProp}`,
description: '...'
})

export default MainPage
```

### Redirects

```tsx
Expand Down
1 change: 1 addition & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type {
MetaData,
GetInitialData,
GetMetaData,
Loader,
PageComponent,
LazyPageComponent,
PageRoute
Expand Down
17 changes: 9 additions & 8 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ export interface InitialData extends Record<string, any> {
statusCode?: number
}

/** Get initial data for a page */
export type GetInitialData = (
context: Context
) => InitialData | void | Promise<InitialData | void>

/** Meta data interface */
export interface MetaData extends Record<string, any> {}

/** Page data loader */
export interface Loader<T, C = any> {
(context: Context & C): T | void | Promise<T | void>
}

/** Get initial data for a page */
export type GetInitialData = Loader<InitialData>

/** Get meta data for a page */
export type GetMetaData = (
context: Context
) => MetaData | void | Promise<MetaData | void>
export type GetMetaData = Loader<MetaData, {data: InitialData}>

/**
* Page component
Expand Down
42 changes: 26 additions & 16 deletions src/components/lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import type {
Context,
PageComponent,
LazyPageComponent,
Loader,
MetaData,
InitialData
} from '../common/types'

/** Component factory */
export type ComponentFactory = () => Promise<{default: PageComponent}>
export interface ComponentFactory {
(): Promise<{default: PageComponent}>
}

/** Data factory */
export interface DataFactory<T, C = any> {
(component: PageComponent, context: Context & C): ReturnType<Loader<T, C>>
}

/**
* Lazy component wrapper
Expand All @@ -23,32 +31,34 @@ export type ComponentFactory = () => Promise<{default: PageComponent}>
* const AboutPage = lazy(() => import('./pages/about'))
* ```
*/
export const lazy = (factory: ComponentFactory): LazyPageComponent => {
export const lazy = (componentFactory: ComponentFactory): LazyPageComponent => {
let promise: ReturnType<ComponentFactory>

const onceFactory: ComponentFactory = () => {
promise ??= factory()
promise ??= componentFactory()

return promise
}

const Component = reactLazy(onceFactory) as LazyPageComponent

Component.getMetaData = async (
context: Context
): Promise<MetaData | void> => {
const {default: component} = await onceFactory()
function loaderFactory<T, C = any>(
dataFactory: DataFactory<T, C>
): Loader<T, C> {
return async (context: Context & C) => {
const {default: Component} = await onceFactory()

return component.getMetaData?.(context) ?? Promise.resolve()
return dataFactory(Component, context) ?? Promise.resolve()
}
}

Component.getInitialData = async (
context: Context
): Promise<InitialData | void> => {
const {default: component} = await onceFactory()
const Component = reactLazy(onceFactory) as LazyPageComponent

Component.getMetaData = loaderFactory<MetaData, {data: InitialData}>(
({getMetaData}, context) => getMetaData?.(context)
)

return component.getInitialData?.(context) ?? Promise.resolve()
}
Component.getInitialData = loaderFactory<InitialData>(
({getInitialData}, context) => getInitialData?.(context)
)

return Component
}
8 changes: 4 additions & 4 deletions src/components/loader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('get initial and meta data', async () => {

expect(matched.match?.pathname).toBe(pathname)
expect(matched.data).toEqual({message: 'Hello'})
expect(matched.meta).toEqual({title: 'Get data'})
expect(matched.meta).toEqual({title: 'Get data: Hello'})
})

test('get async initial and meta data', async () => {
Expand All @@ -30,7 +30,7 @@ test('get async initial and meta data', async () => {

expect(matched.match?.pathname).toBe(pathname)
expect(matched.data).toEqual({message: 'Hello'})
expect(matched.meta).toEqual({title: 'Get async data'})
expect(matched.meta).toEqual({title: 'Get async data: Hello'})
})

test('get component with parametrized path', async () => {
Expand All @@ -39,7 +39,7 @@ test('get component with parametrized path', async () => {

expect(matched.match?.pathname).toBe(pathname)
expect(matched.data).toEqual({message: 'Hello'})
expect(matched.meta).toEqual({title: 'Param'})
expect(matched.meta).toEqual({title: 'Param: Hello'})
})

test('get component with static import', async () => {
Expand All @@ -48,7 +48,7 @@ test('get component with static import', async () => {

expect(matched.match?.pathname).toBe(pathname)
expect(matched.data).toEqual({message: 'Hello'})
expect(matched.meta).toEqual({title: 'Static import'})
expect(matched.meta).toEqual({title: 'Static import: Hello'})
})

test('get component without data', async () => {
Expand Down
9 changes: 2 additions & 7 deletions src/components/loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,9 @@ export const loadRouteData = async ({
const {
route: {Component}
} = match
const promises = []

promises.push(
Component.getInitialData?.({...context, match}) ?? Promise.resolve(),
Component.getMetaData?.({...context, match}) ?? Promise.resolve()
)

const [data, meta] = await Promise.all(promises)
const data = await Component.getInitialData?.({...context, match})
const meta = await Component.getMetaData?.({...context, data, match})

return {data, meta, match}
}
4 changes: 2 additions & 2 deletions src/server/stream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ test('get page with status code 200', async () => {
await renderToStream({req, res, routes, foo: {bar: 'baz'}})

expect(res.status).toBeCalledWith(200)
expect(res.data).toContain('<title>Home</title>')
expect(res.data).toContain('<title>Home: Hello</title>')
expect(res.data).toContain('<h1>Home</h1>')
expect(res.data).toContain(JSON.stringify({message: 'Hello'}))
expect(res.data).not.toContain(JSON.stringify({bar: 'baz'}))
Expand Down Expand Up @@ -79,7 +79,7 @@ test('get page with custom document', async () => {
await renderToStream({req, res, routes, ...assets, Document})

expect(res.status).toBeCalledWith(200)
expect(res.data).toContain('<title>Home</title>')
expect(res.data).toContain('<title>Home: Hello</title>')
expect(res.data).toContain('<link rel="manifest" href="/manifest.json"/>')
expect(res.data).toContain('<link href="/app.css" rel="stylesheet"/>')
expect(res.data).toContain('<script src="/app.js" defer=""></script>')
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/get-async-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const GetAsyncData: PageComponent = () => <h1>Get async data</h1>

GetAsyncData.getMetaData = async () => ({
title: 'Get async data'
GetAsyncData.getMetaData = async ({data}) => ({
title: 'Get async data: ' + data.message
})

GetAsyncData.getInitialData = async () => ({
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/get-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const GetData: PageComponent = () => <h1>Get data</h1>

GetData.getMetaData = () => ({
title: 'Get data'
GetData.getMetaData = ({data}) => ({
title: 'Get data: ' + data.message
})

GetData.getInitialData = () => ({
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const HomePage: PageComponent = () => <h1>Home</h1>

HomePage.getMetaData = async () => ({
title: 'Home'
HomePage.getMetaData = async ({data}) => ({
title: 'Home: ' + data.message
})

HomePage.getInitialData = async () => ({
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const NotFound: PageComponent = () => <h1>Not found</h1>

NotFound.getMetaData = () => ({
title: '404'
NotFound.getMetaData = ({data}) => ({
title: `${data.statusCode}`
})

NotFound.getInitialData = () => ({
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/param.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const Param: PageComponent = () => <h1>Param</h1>

Param.getMetaData = async () => ({
title: 'Param'
Param.getMetaData = async ({data}) => ({
title: 'Param: ' + data.message
})

Param.getInitialData = async ({match}) => ({
Expand Down
4 changes: 2 additions & 2 deletions src/test/components/static-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {PageComponent} from '../../client'

const StaticImport: PageComponent = () => <h1>Static import</h1>

StaticImport.getMetaData = async () => ({
title: 'Static import'
StaticImport.getMetaData = async ({data}) => ({
title: 'Static import: ' + data.message
})

StaticImport.getInitialData = async () => ({
Expand Down

0 comments on commit a3a1c4e

Please sign in to comment.