@graphcommerce/framer-next-pages 2.108.11 → 2.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [2.109.0](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/framer-next-pages@2.108.11...@graphcommerce/framer-next-pages@2.109.0) (2022-01-03)
7
+
8
+
9
+ ### Features
10
+
11
+ * **framer-next-pages:** reduce rerenders when navigating to a new page ([5cf3301](https://github.com/ho-nl/m2-pwa/commit/5cf330130bb3527057da015e3c4a6fa295d7262e))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [2.108.10](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/framer-next-pages@2.108.9...@graphcommerce/framer-next-pages@2.108.10) (2021-12-23)
7
18
 
8
19
 
package/README.md CHANGED
@@ -156,23 +156,9 @@ We have multiple hooks available to animate based on certain states, etc.
156
156
  ```tsx
157
157
  export default function MyComponent() {
158
158
  const { level, depth, direction } = usePageContext()
159
- const pageRouter = usePageRouter()
160
159
  }
161
160
  ```
162
161
 
163
- ### usePageRouter
164
-
165
- The pageRouter maintains state for the old page.
166
-
167
- E.g.:
168
-
169
- - `/my-regular-page`:
170
- - `useRouter().asPath === '/overlay'`
171
- - `usePageRouter().asPath === '/my-regular-page'` We maintain the state here!
172
- - `/overlay`:
173
- - `useRouter().asPath === '/overlay'`
174
- - `usePageRouter().asPath === '/overlay'`
175
-
176
162
  ### usePageContext().level
177
163
 
178
164
  If we have multiple pages layered on top of each other we get the level the page
@@ -226,7 +212,7 @@ count that shows us if we can go back
226
212
  ```tsx
227
213
  function MyComponent {
228
214
  const { backSteps } = usePageContext();
229
- const router = usePageRouter();
215
+ const router = useRouter();
230
216
  return <button onClick={backSteps > 0 && () => router.back()}>back</button>
231
217
  }
232
218
  ```
@@ -239,8 +225,8 @@ close the overlay. So we give the times it needs to go back.
239
225
  ```tsx
240
226
  function MyComponent {
241
227
  const { closeSteps } = usePageContext();
242
- const router = usePageRouter();
243
- return <button onClick={closeSteps > 0 && () => router.go(closeSteps * -1)}>close</button>
228
+ const go = useGo();
229
+ return <button onClick={closeSteps > 0 && () => go(closeSteps * -1)}>close</button>
244
230
  }
245
231
  ```
246
232
 
@@ -0,0 +1,52 @@
1
+ import { RouterContext } from 'next/dist/shared/lib/router-context'
2
+ import { AppPropsType } from 'next/dist/shared/lib/utils'
3
+ import { NextRouter, useRouter } from 'next/router'
4
+ import React, { useMemo } from 'react'
5
+ import { pageRouterContext } from '../context/pageRouterContext'
6
+ import { PageItem } from '../types'
7
+
8
+ // eslint-disable-next-line react/jsx-no-useless-fragment
9
+ const NoLayout: React.FC = ({ children }) => <>{children}</>
10
+
11
+ export type PageRendererProps = Omit<AppPropsType, 'router'> & {
12
+ Layout: React.ComponentType<AppPropsType>
13
+ layoutProps: AppPropsType
14
+ }
15
+
16
+ const PageRendererLayout = React.memo((props: PageItem) => {
17
+ const { PageComponent, layoutProps, pageProps, routerContext } = props
18
+
19
+ const Layout = PageComponent.pageOptions?.Layout ?? NoLayout
20
+
21
+ return (
22
+ <pageRouterContext.Provider value={routerContext}>
23
+ <Layout {...layoutProps}>
24
+ <PageComponent {...pageProps} />
25
+ </Layout>
26
+ </pageRouterContext.Provider>
27
+ )
28
+ })
29
+
30
+ export function PageRenderer(props: PageItem) {
31
+ const { routerContext } = props
32
+ const router = useRouter()
33
+
34
+ const { asPath, pathname, locale, query } = routerContext.pageInfo
35
+
36
+ const pageRouter = useMemo(() => {
37
+ const overrideProps = { asPath, pathname, locale, query }
38
+ return new Proxy<NextRouter>(router, {
39
+ get: (target, prop: string, receiver) =>
40
+ overrideProps[prop] ?? Reflect.get(target, prop, receiver),
41
+ })
42
+
43
+ // We don't want to re-render when the router changes
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, [asPath, locale, pathname, query])
46
+
47
+ return (
48
+ <RouterContext.Provider value={pageRouter}>
49
+ <PageRendererLayout {...props} />
50
+ </RouterContext.Provider>
51
+ )
52
+ }
@@ -1,27 +1,42 @@
1
1
  import { AnimatePresence } from 'framer-motion'
2
2
  import { requestIdleCallback, cancelIdleCallback } from 'next/dist/client/request-idle-callback'
3
3
  import { AppPropsType } from 'next/dist/shared/lib/utils'
4
- import type { NextRouter, Router } from 'next/router'
4
+ import { NextRouter, Router, useRouter } from 'next/router'
5
5
  import React, { useEffect, useRef, useState } from 'react'
6
+ import {} from 'react-dom'
6
7
  import { pageContext } from '../context/pageContext'
7
- import { createRouterProxy, pageRouterContext } from '../context/pageRouterContext'
8
8
  import type { PageComponent, PageItem, UpPage } from '../types'
9
9
  import Page from './Page'
10
+ import { PageRenderer } from './PageRenderer'
10
11
 
11
12
  function findPlainIdx(items: PageItem[]) {
12
13
  return items.reduce((acc, item, i) => (typeof item.overlayGroup === 'string' ? acc : i), -1)
13
14
  }
14
15
 
15
- type PagesProps = Omit<AppPropsType<NextRouter>, 'pageProps'> & {
16
+ type PagesProps = Omit<AppPropsType<NextRouter>, 'pageProps' | 'Component'> & {
16
17
  Component: PageComponent
17
- pageProps?: { up?: UpPage }
18
+ pageProps?: { up?: UpPage | null }
18
19
  }
19
20
 
20
- // eslint-disable-next-line react/jsx-no-useless-fragment
21
- const NoopLayout: React.FC = ({ children }) => <>{children}</>
21
+ function getPageInfo(router: NextRouter) {
22
+ const { asPath, pathname, query, locale } = router
23
+ return { asPath, pathname, query, locale }
24
+ }
25
+
26
+ // function useEarlyRerender() {
27
+ // const router = useRouter()
28
+ // const [url, set] = useState<string>()
29
+
30
+ // useEffect(() => {
31
+ // router.events.on('routeChangeStart', set)
32
+ // return () => router.events.off('routeChangeStart', set)
33
+ // }, [router.events])
34
+
35
+ // return url
36
+ // }
22
37
 
23
38
  export default function FramerNextPages(props: PagesProps) {
24
- const { router, Component, pageProps } = props
39
+ const { router, Component, pageProps: incomingProps } = props
25
40
 
26
41
  const items = useRef<PageItem[]>([])
27
42
  const idx = Number(global.window?.history.state?.idx ?? 0)
@@ -35,22 +50,31 @@ export default function FramerNextPages(props: PagesProps) {
35
50
 
36
51
  let activeItem: PageItem = items.current[idx]
37
52
 
38
- const mustRerender = () =>
39
- JSON.stringify(pageProps) !== JSON.stringify(items.current[idx]?.actualPageProps)
53
+ const currentItem = items.current[idx]
54
+
55
+ const mustRerender = () => {
56
+ const differentRouter =
57
+ JSON.stringify(currentItem?.routerContext.pageInfo) !== JSON.stringify(getPageInfo(router))
58
+ const differentProps = JSON.stringify(incomingProps) !== JSON.stringify(currentItem?.pageProps)
59
+ return differentRouter || differentProps
60
+ }
40
61
 
41
62
  if (!activeItem || mustRerender()) {
42
- const proxy = createRouterProxy(router)
63
+ const pageInfo = getPageInfo(router)
43
64
 
44
65
  activeItem = {
45
- children: <Component {...(pageProps as Record<string, unknown>)} />,
46
- currentRouter: proxy,
47
- Layout: Component.pageOptions?.Layout,
48
- layoutProps: Component.pageOptions?.layoutProps,
49
- actualPageProps: pageProps,
50
- sharedKey: Component.pageOptions?.sharedKey?.(proxy) ?? proxy.pathname,
66
+ PageComponent: Component,
67
+ layoutProps: { ...Component.pageOptions?.layoutProps, ...incomingProps },
68
+ pageProps: incomingProps,
69
+ sharedKey: Component.pageOptions?.sharedKey?.(pageInfo) ?? pageInfo.pathname,
51
70
  overlayGroup: Component.pageOptions?.overlayGroup,
52
71
  historyIdx: idx,
53
- up: Component.pageOptions?.up ?? pageProps?.up,
72
+ routerContext: {
73
+ pageInfo,
74
+ prevPage: items.current[idx - 1]?.routerContext,
75
+ prevUp: items.current[idx - 1]?.routerContext.up,
76
+ up: Component.pageOptions?.up ?? incomingProps?.up ?? undefined,
77
+ },
54
78
  }
55
79
  items.current[idx] = activeItem
56
80
  }
@@ -68,21 +92,24 @@ export default function FramerNextPages(props: PagesProps) {
68
92
  let cancel: number
69
93
  async function loadFallback() {
70
94
  try {
71
- const info = await (router as Router).getRouteInfo('/', '/', {}, '/', '/', {
72
- shallow: false,
73
- })
74
- const proxy = createRouterProxy(router, { asPath: '/', pathname: '/', query: {} })
95
+ // todo: implement fallback loading for up property
96
+ // const up = items.current[0].PageComponent.pageOptions?.up?.href ?? '/'
97
+ const up = '/'
98
+ const info = await (router as Router).getRouteInfo(up, up, {}, up, up, { shallow: false })
99
+
100
+ const pageInfo = { asPath: up, pathname: up, locale: router.locale, query: {} }
75
101
  const Fallback = info.Component as PageComponent
76
102
  const fbItem: PageItem = {
77
- children: <Fallback {...(info.props?.pageProps as Record<string, unknown>)} />,
78
- currentRouter: proxy,
79
- Layout: Fallback.pageOptions?.Layout,
80
- layoutProps: Fallback.pageOptions?.layoutProps,
81
- actualPageProps: info.props?.pageProps,
82
- sharedKey: Fallback.pageOptions?.sharedKey?.(proxy) ?? proxy.pathname,
103
+ PageComponent: Fallback,
104
+ layoutProps: { ...Fallback.pageOptions?.layoutProps, ...info.props?.pageProps },
105
+ pageProps: info.props?.pageProps,
106
+ sharedKey: Fallback.pageOptions?.sharedKey?.(pageInfo) ?? pageInfo.pathname,
83
107
  overlayGroup: Fallback.pageOptions?.overlayGroup,
84
108
  historyIdx: -1,
85
- up: Fallback.pageOptions?.up ?? info.props?.pageProps?.up,
109
+ routerContext: {
110
+ pageInfo,
111
+ up: Fallback.pageOptions?.up ?? info.props?.pageProps?.up,
112
+ },
86
113
  }
87
114
 
88
115
  cancel = requestIdleCallback(() => setFallback(fbItem))
@@ -118,57 +145,33 @@ export default function FramerNextPages(props: PagesProps) {
118
145
  if (!item || seen.has(item.sharedKey)) return false
119
146
  seen.add(item.sharedKey)
120
147
 
121
- if (
148
+ return !(
122
149
  typeof activeItem.overlayGroup === 'string' &&
123
150
  typeof item.overlayGroup === 'string' &&
124
151
  activeItem.overlayGroup !== item.overlayGroup
125
- ) {
126
- return false
127
- }
128
- return true
152
+ )
129
153
  })
130
154
  .reverse()
131
155
 
132
156
  return (
133
157
  <AnimatePresence initial={false}>
134
158
  {renderItems.map((item, itemIdx) => {
135
- const {
136
- children,
137
- historyIdx,
138
- sharedKey,
139
- Layout = NoopLayout,
140
- layoutProps,
141
- actualPageProps,
142
- currentRouter,
143
- overlayGroup,
144
- up,
145
- } = item
159
+ const { historyIdx, sharedKey, overlayGroup } = item
146
160
  const active = itemIdx === renderItems.length - 1
147
161
  const depth = itemIdx - (renderItems.length - 1)
148
-
149
162
  const closeIdx = renderItems[itemIdx - 1]?.historyIdx ?? -1
150
163
  const closeSteps = closeIdx > -1 ? historyIdx - closeIdx : 0
151
-
152
164
  const backSteps = historyIdx - closeIdx - 1
153
165
 
154
- const { currentRouter: prevRouter, up: prevUp } = items.current[historyIdx - 1] ?? {}
155
-
156
166
  return (
157
167
  <pageContext.Provider
158
168
  key={sharedKey}
159
- // Since we very carefully prevent rerenders of this component we can safely ignore the eslint error
169
+ // We're actually rerendering here but since the actual page renderer is memoized we can safely do this
160
170
  // eslint-disable-next-line react/jsx-no-constructed-context-values
161
171
  value={{ depth, active, direction, closeSteps, backSteps, historyIdx, overlayGroup }}
162
172
  >
163
173
  <Page active={active} historyIdx={historyIdx}>
164
- <pageRouterContext.Provider
165
- // eslint-disable-next-line react/jsx-no-constructed-context-values
166
- value={{ currentRouter, prevRouter, up, prevUp }}
167
- >
168
- <Layout {...actualPageProps} {...layoutProps}>
169
- {children}
170
- </Layout>
171
- </pageRouterContext.Provider>
174
+ <PageRenderer {...item} />
172
175
  </Page>
173
176
  </pageContext.Provider>
174
177
  )
@@ -1,39 +1,5 @@
1
- import { NextRouter } from 'next/router'
2
1
  import { createContext } from 'react'
3
- import { PageRouterContext, RouterProxy } from '../types'
2
+ import { PageContext } from '../types'
4
3
 
5
- export const pageRouterContext = createContext(undefined as unknown as PageRouterContext)
4
+ export const pageRouterContext = createContext(undefined as unknown as PageContext)
6
5
  pageRouterContext.displayName = 'PageRouterContext'
7
-
8
- type OverrideProps = Partial<Pick<NextRouter, 'asPath' | 'pathname' | 'query' | 'locale'>>
9
-
10
- export function createRouterProxy(router: NextRouter, override?: OverrideProps): RouterProxy {
11
- function go(delta: number) {
12
- if (delta >= 0) {
13
- console.error(`Called .go(${delta}), only negative numbers are allowed. Redirecting to home`)
14
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
15
- router.push('/', '/')
16
- return
17
- }
18
-
19
- const deltaAbs = Math.abs(delta)
20
- for (let i = 0; i < deltaAbs; i++) {
21
- router.back()
22
- }
23
- }
24
-
25
- // We create an object with the current stale properties
26
- const overrideProps = {
27
- asPath: router.asPath,
28
- pathname: router.pathname,
29
- query: router.query,
30
- locale: router.locale,
31
- go,
32
- ...override,
33
- }
34
-
35
- return new Proxy<RouterProxy>(router as RouterProxy, {
36
- get: (target, prop: string, receiver) =>
37
- overrideProps[prop] ?? Reflect.get(target, prop, receiver),
38
- })
39
- }
package/hooks/useGo.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { useRouter } from 'next/router'
2
+
3
+ export function useGo(delta: number) {
4
+ const { push, back } = useRouter()
5
+ return () => {
6
+ if (delta >= 0) {
7
+ // console.error(`Called .go(${delta}), only negative numbers are allowed. Redirecting to home`)
8
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
9
+ // push('/', '/')
10
+ return
11
+ }
12
+
13
+ const deltaAbs = Math.abs(delta)
14
+ for (let i = 0; i < deltaAbs; i++) back()
15
+ }
16
+ }
@@ -1,4 +1,4 @@
1
- import { usePageRouter } from './usePageRouter'
1
+ import { useRouter } from 'next/router'
2
2
  import { usePrevPageRouter } from './usePrevPageRouter'
3
3
 
4
4
  export type UseHistoryLink = { href: string }
@@ -8,12 +8,13 @@ type ClickEvent = { preventDefault: () => void }
8
8
  export function useHistoryLink(options: UseHistoryLink) {
9
9
  const { href } = options
10
10
  const prevRouter = usePrevPageRouter()
11
+ const router = useRouter()
11
12
 
12
13
  const onClick =
13
14
  href === prevRouter?.asPath
14
15
  ? (e: ClickEvent) => {
15
16
  e.preventDefault()
16
- prevRouter.back()
17
+ router.back()
17
18
  }
18
19
  : undefined
19
20
 
@@ -22,7 +23,7 @@ export function useHistoryLink(options: UseHistoryLink) {
22
23
 
23
24
  export function useHistoryGo(options: UseHistoryLink) {
24
25
  const { onClick, href } = useHistoryLink(options)
25
- const router = usePageRouter()
26
+ const router = useRouter()
26
27
 
27
28
  return () => {
28
29
  if (onClick) onClick({ preventDefault: () => {} })
@@ -1,24 +1,6 @@
1
- import { useContext } from 'react'
2
- import { pageRouterContext } from '../context/pageRouterContext'
3
- import { RouterProxy } from '../types'
1
+ import { useRouter } from 'next/router'
4
2
 
5
- /**
6
- * The pageRouter maintains state for the old page.
7
- *
8
- * E.g.:
9
- *
10
- * `/my-regular-page`:
11
- *
12
- * - `useRouter().asPath === '/overlay'`
13
- * - `usePageRouter().asPath === '/my-regular-page'` We maintain the state
14
- *
15
- * `/overlay`:
16
- *
17
- * - `useRouter().asPath === '/overlay'`
18
- * - `usePageRouter().asPath === '/overlay'`
19
- *
20
- * Adds an additional method: usePageRouter().go(-1)
21
- */
22
- export function usePageRouter(): RouterProxy {
23
- return useContext(pageRouterContext).currentRouter
3
+ export const usePageRouter = () => {
4
+ console.warn('usePageRouter does nothing, use next/router useRouter instead')
5
+ return useRouter()
24
6
  }
@@ -1,8 +1,7 @@
1
1
  import { useContext } from 'react'
2
2
  import { pageRouterContext } from '../context/pageRouterContext'
3
- import { RouterProxy } from '../types'
4
3
 
5
- /** Same as usePageRouter but gives back the router of the previous page. */
6
- export function usePrevPageRouter(): RouterProxy | undefined {
7
- return useContext(pageRouterContext).prevRouter
4
+ /** Same as useRouter but gives back the router of the previous page. */
5
+ export function usePrevPageRouter() {
6
+ return useContext(pageRouterContext).prevPage?.pageInfo
8
7
  }
package/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './context/pageContext'
4
4
  export * from './context/pageRouterContext'
5
5
  export type { PageOptions, PageComponent } from './types'
6
6
 
7
+ export * from './hooks/useGo'
7
8
  export * from './hooks/usePageContext'
8
9
  export * from './hooks/usePageRouter'
9
10
  export * from './hooks/useHistoryLink'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphcommerce/framer-next-pages",
3
- "version": "2.108.11",
3
+ "version": "2.109.0",
4
4
  "sideEffects": false,
5
5
  "scripts": {
6
6
  "dev": "tsc -W"
@@ -16,11 +16,11 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@graphcommerce/framer-utils": "^2.103.20"
19
+ "@graphcommerce/framer-utils": "^2.103.21"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@graphcommerce/browserslist-config-pwa": "^3.0.3",
23
- "@graphcommerce/eslint-config-pwa": "^3.1.9",
23
+ "@graphcommerce/eslint-config-pwa": "^3.1.10",
24
24
  "@graphcommerce/prettier-config-pwa": "^3.0.5",
25
25
  "@graphcommerce/typescript-config-pwa": "^3.1.2",
26
26
  "@playwright/test": "^1.17.1"
@@ -31,5 +31,5 @@
31
31
  "react": "^17.0.2",
32
32
  "react-dom": "^17.0.2"
33
33
  },
34
- "gitHead": "a9174584cfeb04807a72b6c609175c24e24ac818"
34
+ "gitHead": "bc5423d7547f8685db4cd8fc6d8f7a2a51ebed05"
35
35
  }
package/types.ts CHANGED
@@ -2,11 +2,11 @@ import { NextComponentType, NextPageContext } from 'next'
2
2
  import { NextRouter } from 'next/router'
3
3
  import React from 'react'
4
4
 
5
- export type RouterProxy = NextRouter & { go(delta: number): void; prevUpUrl: string }
5
+ type PageInfo = Pick<NextRouter, 'asPath' | 'query' | 'locale' | 'pathname'>
6
6
 
7
- export type PageRouterContext = {
8
- currentRouter: RouterProxy
9
- prevRouter?: RouterProxy
7
+ export type PageContext = {
8
+ pageInfo: PageInfo
9
+ prevPage?: PageContext
10
10
  up?: UpPage
11
11
  prevUp?: UpPage
12
12
  }
@@ -111,7 +111,7 @@ export type PageOptions<T extends Record<string, unknown> = Record<string, unkno
111
111
  * }
112
112
  * ```
113
113
  */
114
- sharedKey?: (router: NextRouter) => string | undefined
114
+ sharedKey?: (pageInfo: PageInfo) => string | undefined
115
115
 
116
116
  /**
117
117
  * Create a Layout to share a wrapping component between multiple routes.
@@ -124,7 +124,7 @@ export type PageOptions<T extends Record<string, unknown> = Record<string, unkno
124
124
  /** Pass props to the SharedComponent */
125
125
  layoutProps?: Partial<Omit<T, 'children'>>
126
126
 
127
- up?: UpPage
127
+ up?: UpPage | null
128
128
  }
129
129
 
130
130
  export type PageComponent<T = Record<string, unknown>> = NextComponentType<NextPageContext, T> & {
@@ -140,10 +140,10 @@ export type UpPage = { href: string; title: string }
140
140
  * @private
141
141
  */
142
142
  export type PageItem = {
143
- currentRouter: RouterProxy
144
- children: React.ReactNode
143
+ routerOverride?: Partial<NextRouter>
144
+ PageComponent: PageComponent
145
145
  historyIdx: number
146
146
  sharedKey: string
147
- actualPageProps?: Record<string, unknown>
148
- up?: UpPage
149
- } & Omit<PageOptions<Record<string, unknown>>, 'sharedKey'>
147
+ pageProps?: Record<string, unknown>
148
+ routerContext: PageContext
149
+ } & Omit<PageOptions<Record<string, unknown>>, 'sharedKey' | 'up'>
@@ -0,0 +1,15 @@
1
+ import { NextRouter } from 'next/router'
2
+
3
+ export type OverrideProps = Partial<Pick<NextRouter, 'asPath' | 'pathname' | 'query' | 'locale'>>
4
+
5
+ export function createRouterProxy(router: NextRouter, override?: OverrideProps): NextRouter {
6
+ // We create an object with the current stale properties
7
+ const { asPath, pathname, query, locale } = router
8
+
9
+ const overrideProps: OverrideProps = { asPath, pathname, query, locale, ...override }
10
+
11
+ return new Proxy<NextRouter>(router, {
12
+ get: (target, prop: string, receiver) =>
13
+ overrideProps[prop] ?? Reflect.get(target, prop, receiver),
14
+ })
15
+ }