├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── blank.yml │ └── config.yml └── workflows │ ├── ci.yml │ ├── discord.yml │ └── formatting.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── biome.json ├── examples ├── README.md ├── apollo │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── assets │ │ └── logo.svg │ ├── layouts │ │ ├── HeadDefault.tsx │ │ ├── LayoutDefault.tsx │ │ └── style.css │ ├── package.json │ ├── pages │ │ ├── +ApolloClient.ts │ │ ├── +config.ts │ │ ├── _error │ │ │ └── +Page.tsx │ │ └── index │ │ │ ├── +Page.tsx │ │ │ ├── Counter.tsx │ │ │ └── Countries.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── full │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── assets │ │ ├── logo-new.svg │ │ └── logo.svg │ ├── components │ │ ├── ClientOnlyComponent.tsx │ │ ├── Counter.tsx │ │ └── Link.tsx │ ├── package.json │ ├── pages │ │ ├── +Head.tsx │ │ ├── +Layout.tsx │ │ ├── +config.ts │ │ ├── +react.client.ts │ │ ├── +react.server.ts │ │ ├── _error │ │ │ └── +Page.tsx │ │ ├── client-only │ │ │ └── +Page.tsx │ │ ├── images │ │ │ └── +Page.tsx │ │ ├── index │ │ │ ├── +Page.tsx │ │ │ └── +config.ts │ │ ├── star-wars │ │ │ ├── @id │ │ │ │ ├── +Page.tsx │ │ │ │ └── +data.tsx │ │ │ ├── index │ │ │ │ ├── +Page.tsx │ │ │ │ └── +data.ts │ │ │ └── types.ts │ │ ├── starship │ │ │ ├── +Layout.tsx │ │ │ ├── +Page.tsx │ │ │ ├── +config.ts │ │ │ ├── reviews │ │ │ │ └── +Page.tsx │ │ │ └── spec │ │ │ │ └── +Page.tsx │ │ ├── streaming │ │ │ └── +Page.tsx │ │ ├── style.css │ │ └── without-ssr │ │ │ ├── +Page.tsx │ │ │ └── +config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── minimal │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── package.json │ ├── pages │ │ ├── +config.js │ │ ├── Layout.css │ │ ├── Layout.jsx │ │ ├── about │ │ │ └── +Page.jsx │ │ └── index │ │ │ ├── +Page.jsx │ │ │ └── Counter.jsx │ └── vite.config.js ├── query │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── assets │ │ └── logo.svg │ ├── layouts │ │ ├── HeadDefault.tsx │ │ ├── LayoutDefault.tsx │ │ └── style.css │ ├── package.json │ ├── pages │ │ ├── +config.ts │ │ └── index │ │ │ ├── +Page.tsx │ │ │ ├── @id │ │ │ ├── +Page.tsx │ │ │ └── Movie.tsx │ │ │ ├── Counter.tsx │ │ │ ├── Movies.tsx │ │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.ts ├── redux │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview-ssg.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── components │ │ ├── Counter.tsx │ │ ├── Counter │ │ │ └── fetchCountInit.ts │ │ └── Link.tsx │ ├── layouts │ │ ├── LayoutDefault.tsx │ │ ├── logo.svg │ │ └── style.css │ ├── package.json │ ├── pages │ │ ├── +config.ts │ │ ├── +redux.ts │ │ ├── about │ │ │ ├── +Page.tsx │ │ │ ├── +config.ts │ │ │ ├── +data.ts │ │ │ └── +onData.ts │ │ └── index │ │ │ ├── +Page.tsx │ │ │ ├── +data.ts │ │ │ ├── +onData.ts │ │ │ └── TodoList.tsx │ ├── store │ │ ├── createStore.ts │ │ ├── hooks.ts │ │ └── slices │ │ │ ├── count.ts │ │ │ └── todos.ts │ ├── tsconfig.json │ └── vite.config.ts └── zustand │ ├── .gitignore │ ├── .test-dev.test.ts │ ├── .test-preview.test.ts │ ├── .testRun.ts │ ├── README.md │ ├── assets │ └── logo.svg │ ├── components │ └── Counter.tsx │ ├── layouts │ ├── HeadDefault.tsx │ ├── LayoutDefault.tsx │ └── style.css │ ├── package.json │ ├── pages │ ├── +Layout.tsx │ ├── +config.ts │ ├── _error │ │ └── +Page.tsx │ ├── about │ │ └── +Page.tsx │ └── index │ │ ├── +Page.tsx │ │ ├── +data.ts │ │ └── TodoList.tsx │ ├── store.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages ├── vike-react-antd │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── Wrapper.server.tsx │ ├── config.ts │ ├── onAfterRenderHtml.ts │ ├── onBeforeRenderHtml.ts │ ├── package.json │ └── tsconfig.json ├── vike-react-apollo │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── integration │ │ │ ├── +config.ts │ │ │ ├── Transport.tsx │ │ │ └── Wrapper.tsx │ │ ├── utils │ │ │ └── assert.ts │ │ └── withFallback.tsx │ └── tsconfig.json ├── vike-react-chakra │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── Wrapper.tsx │ ├── config.ts │ ├── eject.config.js │ ├── package.json │ └── tsconfig.json ├── vike-react-query │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── integration │ │ │ ├── +config.ts │ │ │ ├── FallbackErrorBoundary.tsx │ │ │ ├── StreamedHydration.tsx │ │ │ └── Wrapper.tsx │ │ ├── withFallback.spec.tsx │ │ └── withFallback.tsx │ ├── tsconfig.json │ └── vitest.config.ts ├── vike-react-redux │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── Wrapper.tsx │ ├── config.ts │ ├── onAfterRenderHtml.server.ts │ ├── onBeforeRenderClient.client.ts │ ├── onCreatePageContext.server.ts │ ├── package.json │ └── tsconfig.json ├── vike-react-styled-components │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── Wrapper.server.tsx │ ├── config.ts │ ├── onAfterRenderHtml.ts │ ├── onBeforeRenderHtml.ts │ ├── package.json │ └── tsconfig.json ├── vike-react-styled-jsx │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── Wrapper.server.tsx │ ├── config.ts │ ├── onAfterRenderHtml.ts │ ├── onBeforeRenderHtml.ts │ ├── package.json │ └── tsconfig.json ├── vike-react-zustand │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── context.ts │ │ ├── getOrCreateStore.ts │ │ ├── index.ts │ │ ├── integration │ │ │ ├── config.ts │ │ │ └── types.d.ts │ │ ├── plugin │ │ │ ├── babelTransformer.ts │ │ │ └── index.ts │ │ ├── types.ts │ │ ├── utils │ │ │ ├── assert.ts │ │ │ ├── assignDeep.ts │ │ │ ├── getGlobalObject.ts │ │ │ └── sanitizeForSerialization.ts │ │ └── withPageContext.ts │ └── tsconfig.json └── vike-react │ ├── .gitignore │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── README.md │ ├── eject.config.js │ ├── package.json │ ├── src │ ├── components │ │ ├── ClientOnly.tsx │ │ ├── Config │ │ │ ├── Config-client.ts │ │ │ └── Config-server.ts │ │ └── Head │ │ │ ├── Head-client.ts │ │ │ └── Head-server.ts │ ├── config.ts │ ├── helpers │ │ └── clientOnly.tsx │ ├── hooks │ │ ├── useConfig │ │ │ ├── configsCumulative.ts │ │ │ ├── useConfig-client.ts │ │ │ └── useConfig-server.ts │ │ ├── useData.tsx │ │ └── usePageContext.tsx │ ├── index.ts │ ├── integration │ │ ├── Loading.css │ │ ├── Loading.tsx │ │ ├── applyHeadSettings.tsx │ │ ├── getHeadSetting.tsx │ │ ├── getPageElement.tsx │ │ ├── onRenderClient.tsx │ │ ├── onRenderHtml.tsx │ │ ├── resolveReactOptions.ts │ │ └── ssrEffect.ts │ ├── types │ │ ├── Config.ts │ │ └── PageContext.ts │ └── utils │ │ ├── assert.ts │ │ ├── callCumulativeHooks.ts │ │ ├── getGlobalObject.ts │ │ ├── getTagAttributesString.ts │ │ ├── includes.ts │ │ ├── isCallable.ts │ │ ├── isNotFalse.ts │ │ ├── isNotNullish.ts │ │ ├── isObject.ts │ │ ├── isReactElement.ts │ │ ├── isType.ts │ │ ├── objectEntries.ts │ │ └── objectKeys.ts │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vikejs 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank.yml: -------------------------------------------------------------------------------- 1 | name: "Blank issue" 2 | description: Don't use this unless you are a Vike maintainer. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | # **Don't use this** unless you are a Vike maintainer. Create a new issue at [Vike's main repository `github.com/vikejs/vike`](https://github.com/vikejs/vike/issues/new/choose) instead. 8 | - type: textarea 9 | attributes: 10 | label: Description 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "💥 Bug" 4 | url: https://github.com/vikejs/vike/issues/new?template=bug.yml 5 | about: "Report a bug. (Redirects to Vike's main repository.)" 6 | - name: "🚀 Feature" 7 | url: https://github.com/vikejs/vike/issues/new?template=feature.yml 8 | about: "Suggest a new feature. (Redirects to Vike's main repository.)" 9 | - name: "✨ Polish" 10 | url: https://github.com/vikejs/vike/issues/new?template=polish.yaml 11 | about: "Unclear API or docs? Let us know — we'll polish Vike's DX. (Redirects to Vike's main repository.)" 12 | - name: "🙏 Help & Questions" 13 | url: https://github.com/vikejs/vike/discussions/new?category=help-questions 14 | about: "Get official help from Vike maintainers." 15 | - name: "🗨️ Other" 16 | url: https://github.com/vikejs/vike/discussions/new?category=other 17 | about: "Anything else: ideas, feedback, complaints, appreciation, ..." 18 | - name: "💬 Chit-chat" 19 | url: https://discord.com/invite/hfHhnJyVg8 20 | about: "Casual conversation with the community." 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | test: 6 | # Prevent workflow being run twice, https://github.com/orgs/community/discussions/57827#discussioncomment-6579237 7 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 8 | 9 | name: ${{ matrix.cmd }} (${{ matrix.os }}) 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | # Don't cancel other matrix operations if one fails 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | - windows-latest 20 | cmd: 21 | - pnpm run test:e2e 22 | - pnpm run test:units 23 | - pnpm run test:types 24 | # `exclude` docs & alternatives: 25 | # - https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude 26 | # - https://stackoverflow.com/questions/68994484/how-to-skip-a-configuration-of-a-matrix-with-github-actions/68994907#68994907 27 | # - https://stackoverflow.com/questions/66025220/paired-values-in-github-actions-matrix/68940067#68940067 28 | exclude: 29 | - os: windows-latest 30 | cmd: pnpm run test:types 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: pnpm/action-setup@v4 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | # TODO/eventually: try using the cache again 39 | # The cache breaks playwright installation, see https://github.com/vikejs/vike-vue/pull/119 40 | # cache: "pnpm" 41 | 42 | - run: pnpm install 43 | - run: pnpm exec playwright install chromium 44 | - run: pnpm run build 45 | 46 | - run: ${{ matrix.cmd }} 47 | -------------------------------------------------------------------------------- /.github/workflows/discord.yml: -------------------------------------------------------------------------------- 1 | name: Discord Notification 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['CI', 'Docs'] 6 | types: [completed] 7 | 8 | jobs: 9 | notify: 10 | # 'skipped' means the `if: ` condition wasn't fulfilled 11 | if: ${{ github.event.workflow_run.conclusion != 'skipped' }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Push the notification using Discord webhook 15 | env: 16 | # 🟢 has poor compatibility: https://www.unicompat.com/1F7E2 17 | CI_STATUS: ${{ github.event.workflow_run.conclusion == 'success' && '✅' || github.event.workflow_run.conclusion == 'failure' && '🔴' || github.event.workflow_run.conclusion }} 18 | REPO_NAME: ${{ github.event.repository.name }} 19 | GIT_BRANCH: ${{ github.event.workflow_run.head_branch }} 20 | WORKFLOW_NAME: ${{ github.event.workflow_run.name }} 21 | WORKFLOW_URL: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.html_url || '' }} 22 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 23 | # Playground: https://gist.github.com/brillout/da1ab713bda6e3c0d2ad909c1a4e80f2 24 | run: | 25 | curl \ 26 | -i \ 27 | -H "Accept: application/json" \ 28 | -H "Content-Type:application/json" \ 29 | -X POST \ 30 | --data "{\"content\":\"$CI_STATUS $REPO_NAME $WORKFLOW_NAME $GIT_BRANCH $WORKFLOW_URL\",\"username\":\"GitHub\",\"avatar_url\":\"https://i.imgur.com/OOtUMJD.png\"}" \ 31 | $DISCORD_WEBHOOK 32 | - name: Debug/inspect 33 | env: 34 | EVENT: ${{ toJSON( github.event ) }} 35 | run: echo $EVENT 36 | -------------------------------------------------------------------------------- /.github/workflows/formatting.yml: -------------------------------------------------------------------------------- 1 | name: 'Check formatting' 2 | on: 3 | push: 4 | jobs: 5 | check_formatting: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: pnpm/action-setup@v4 10 | - run: pnpm install 11 | - run: pnpm run format:check 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .pnpm-debug.log 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=deep 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | tabWidth: 2 3 | singleQuote: true 4 | printWidth: 120 5 | trailingComma: all 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions welcome! 2 | 3 | **1. Install** 4 | 5 | ```shell 6 | git clone git@github.com:vikejs/vike-react 7 | cd vike-react/ 8 | pnpm install 9 | ``` 10 | 11 | > [!NOTE] 12 | > See [System requirements](#system-requirements) for how to install pnpm. 13 | 14 | **2. Build** 15 | 16 | Build all `packages/*`: 17 | 18 | ```shell 19 | pnpm build 20 | ``` 21 | 22 | **3. Develop** 23 | 24 | Watch & re-build upon modifications: 25 | 26 | ```shell 27 | cd packages/vike-react # or any other packages/* 28 | pnpm dev 29 | ``` 30 | 31 | In a second shell: 32 | 33 | ```shell 34 | cd examples/full/ # or any other examples/* 35 | pnpm dev 36 | ``` 37 | 38 | That's it. You can now test your modifications. 39 | 40 | > [!WARNING] 41 | > After changing the source code of `vike-react(-*)`, make sure to **always clear Vite's client cache** with `$ rm -rf examples/full/node_modules/.vite/`. Otherwise you'll get a version mismatch between the server (using the latest build) and the client (using the previous cached build). 42 | 43 | > [!WARNING] 44 | > When switching Git branches, make sure to **run `pnpm reset`** at the monorepo root: it will re-install and re-build everything. It's required when switching to a branch that, for example, requires another Vike version. 45 | 46 |
47 | 48 | ## System requirements 49 | 50 | - Node.js `>=16.0.0` 51 | - pnpm `>=9.0.0` 52 | 53 | > [!NOTE] 54 | > To install [pnpm](https://pnpm.io) run: 55 | > ```shell 56 | > npm install -g pnpm 57 | > ``` 58 | > (Or see [pnpm Docs > Installation](https://pnpm.io/installation) for alternative methods.) 59 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Romuald Brillout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Source code of following Vike React extensions: [Vike Docs > Extensions > React](https://vike.dev/extensions#react). 2 | 3 | See [packages/](packages/). 4 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["dist/", "package.json"] 5 | }, 6 | "formatter": { 7 | "indentWidth": 2, 8 | "indentStyle": "space" 9 | }, 10 | "javascript": { 11 | "formatter": { 12 | "semicolons": "asNeeded", 13 | "lineWidth": 120, 14 | "quoteStyle": "single", 15 | "trailingCommas": "all" 16 | } 17 | }, 18 | "linter": { 19 | "enabled": false 20 | }, 21 | "vcs": { 22 | "enabled": true, 23 | "clientKind": "git" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > For more examples, see [Bati](https://batijs.dev) which generates `vike-react` apps. 3 | -------------------------------------------------------------------------------- /examples/apollo/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/apollo/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/apollo/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/apollo/.testRun.ts: -------------------------------------------------------------------------------- 1 | export { testRun } 2 | 3 | import { test, expect, run, fetchHtml, page, getServerUrl, autoRetry } from '@brillout/test-e2e' 4 | 5 | function testRun(cmd: `pnpm run ${'dev' | 'preview'}`) { 6 | run(cmd) 7 | 8 | const content = 'United States' 9 | const loading = 'Loading contries...' 10 | test('HTML', async () => { 11 | const html = await fetchHtml('/') 12 | expect(getTitle(html)).toBe('My Vike + React App') 13 | // fetchHtml() awaits the stream 14 | expect(html).toContain(content) 15 | }) 16 | test('DOM', async () => { 17 | await page.goto(getServerUrl() + '/') 18 | const body = await page.textContent('body') 19 | // Playwright seems to await the HTML stream 20 | expect(body).not.toContain(loading) 21 | expect(body).toContain(content) 22 | await testCounter() 23 | }) 24 | } 25 | 26 | function getTitle(html: string) { 27 | const title = html.match(/(.*?)<\/title>/i)?.[1] 28 | return title 29 | } 30 | 31 | async function testCounter() { 32 | // autoRetry() for awaiting client-side code loading & executing 33 | await autoRetry( 34 | async () => { 35 | expect(await page.textContent('button')).toBe('Counter 0') 36 | await page.click('button') 37 | expect(await page.textContent('button')).toContain('Counter 1') 38 | }, 39 | { timeout: 5 * 1000 }, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /examples/apollo/README.md: -------------------------------------------------------------------------------- 1 | Example of using `vike-react-apollo`. 2 | 3 | > [!NOTE] 4 | > For more examples, see [Bati](https://batijs.dev) which generates `vike-react` apps. 5 | 6 | ```bash 7 | git clone git@github.com:vikejs/vike-react 8 | cd vike-react/examples/apollo/ 9 | npm install 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/apollo/layouts/HeadDefault.tsx: -------------------------------------------------------------------------------- 1 | export default HeadDefault 2 | 3 | import React from 'react' 4 | import logoUrl from '../assets/logo.svg' 5 | 6 | function HeadDefault() { 7 | return ( 8 | <> 9 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 10 | <meta name="description" content="Demo showcasing Vike + React" /> 11 | <link rel="icon" href={logoUrl} /> 12 | </> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/apollo/layouts/LayoutDefault.tsx: -------------------------------------------------------------------------------- 1 | export default LayoutDefault 2 | 3 | import './style.css' 4 | import React from 'react' 5 | import logoUrl from '../assets/logo.svg' 6 | 7 | function LayoutDefault({ children }: { children: React.ReactNode }) { 8 | return ( 9 | <div 10 | style={{ 11 | display: 'flex', 12 | maxWidth: 900, 13 | margin: 'auto', 14 | }} 15 | > 16 | <Sidebar> 17 | <Logo /> 18 | </Sidebar> 19 | <Content>{children}</Content> 20 | </div> 21 | ) 22 | } 23 | 24 | function Sidebar({ children }: { children: React.ReactNode }) { 25 | return ( 26 | <div 27 | id="sidebar" 28 | style={{ 29 | padding: 20, 30 | flexShrink: 0, 31 | display: 'flex', 32 | flexDirection: 'column', 33 | lineHeight: '1.8em', 34 | borderRight: '2px solid #eee', 35 | }} 36 | > 37 | {children} 38 | </div> 39 | ) 40 | } 41 | 42 | function Content({ children }: { children: React.ReactNode }) { 43 | return ( 44 | <div id="page-container"> 45 | <div 46 | id="page-content" 47 | style={{ 48 | padding: 20, 49 | paddingBottom: 50, 50 | minHeight: '100vh', 51 | }} 52 | > 53 | {children} 54 | </div> 55 | </div> 56 | ) 57 | } 58 | 59 | function Logo() { 60 | return ( 61 | <div 62 | style={{ 63 | marginTop: 20, 64 | marginBottom: 10, 65 | }} 66 | > 67 | <a href="/"> 68 | <img src={logoUrl} height={64} width={64} /> 69 | </a> 70 | </div> 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /examples/apollo/layouts/style.css: -------------------------------------------------------------------------------- 1 | /* Links */ 2 | a { 3 | text-decoration: none; 4 | } 5 | #sidebar a { 6 | padding: 2px 10px; 7 | margin-left: -10px; 8 | } 9 | #sidebar a.is-active { 10 | background-color: #eee; 11 | } 12 | 13 | /* Reset */ 14 | body { 15 | margin: 0; 16 | font-family: sans-serif; 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Page Transition Anmiation */ 23 | #page-content { 24 | opacity: 1; 25 | transition: opacity 0.3s ease-in-out; 26 | } 27 | body.page-is-transitioning #page-content { 28 | opacity: 0; 29 | } 30 | -------------------------------------------------------------------------------- /examples/apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "build": "vike build", 5 | "preview": "vike build && vike preview" 6 | }, 7 | "dependencies": { 8 | "@types/react": "^19.0.10", 9 | "@types/react-dom": "^19.0.4", 10 | "@vitejs/plugin-react": "^4.3.4", 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0", 13 | "typescript": "^5.8.3", 14 | "vike": "^0.4.230", 15 | "vike-react": "0.6.4", 16 | "vike-react-apollo": "0.1.2", 17 | "@apollo/client": "^3.10.8", 18 | "@apollo/client-react-streaming": "^0.11.2", 19 | "graphql": "^16.9.0", 20 | "vite": "^6.2.5" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /examples/apollo/pages/+ApolloClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache } from '@apollo/client-react-streaming' 2 | import type { PageContext } from 'vike/types' 3 | 4 | // Same config but with artificial delay: https://gist.github.com/brillout/7d7db0fd6ce55b3b5e8f7ec893eeda01 5 | export default (pageContext: PageContext) => 6 | new ApolloClient({ 7 | uri: 'https://countries.trevorblades.com', 8 | cache: new InMemoryCache(), 9 | }) 10 | -------------------------------------------------------------------------------- /examples/apollo/pages/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | import Layout from '../layouts/LayoutDefault' 3 | import Head from '../layouts/HeadDefault' 4 | import vikeReact from 'vike-react/config' 5 | import vikeReactApollo from 'vike-react-apollo/config' 6 | 7 | // Default configs (can be overridden by pages) 8 | export default { 9 | Layout, 10 | Head, 11 | // <title> 12 | title: 'My Vike + React App', 13 | extends: [vikeReact, vikeReactApollo], 14 | passToClient: ['routeParams'], 15 | } satisfies Config 16 | -------------------------------------------------------------------------------- /examples/apollo/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { usePageContext } from 'vike-react/usePageContext' 5 | 6 | function Page() { 7 | const { is404 } = usePageContext() 8 | if (is404) { 9 | return ( 10 | <> 11 | <h1>404 Page Not Found</h1> 12 | <p>This page could not be found.</p> 13 | </> 14 | ) 15 | } else { 16 | return ( 17 | <> 18 | <h1>500 Internal Server Error</h1> 19 | <p>Something went wrong.</p> 20 | </> 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/apollo/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { Counter } from './Counter' 5 | import { Countries } from './Countries' 6 | 7 | function Page() { 8 | return ( 9 | <> 10 | <h1>My Vike + React app</h1> 11 | This page is: 12 | <ul> 13 | <li>Rendered to HTML.</li> 14 | <li> 15 | Interactive while loading. <Counter /> 16 | </li> 17 | </ul> 18 | <div> 19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ullamcorper neque magna, a dapibus turpis 20 | volutpat eget. Praesent et aliquam nisi. Integer congue nec ligula et sollicitudin. 21 | </div> 22 | <br /> 23 | <Countries /> 24 | </> 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /examples/apollo/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React, { useState } from 'react' 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0) 7 | 8 | return <button onClick={() => setCount((count) => count + 1)}>Counter {count}</button> 9 | } 10 | -------------------------------------------------------------------------------- /examples/apollo/pages/index/Countries.tsx: -------------------------------------------------------------------------------- 1 | export { Countries } 2 | 3 | import { gql, useSuspenseQuery } from '@apollo/client/index.js' 4 | import React from 'react' 5 | import { withFallback } from 'vike-react-apollo' 6 | 7 | const Countries = withFallback(() => { 8 | const { data } = useSuspenseQuery<{ countries: { code: string; name: string }[] }>(gql` 9 | { 10 | countries { 11 | code 12 | name 13 | } 14 | } 15 | `) 16 | 17 | return ( 18 | <ul> 19 | {data.countries.map((country) => ( 20 | <li key={country.code}>{country.name}</li> 21 | ))} 22 | </ul> 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /examples/apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/apollo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | import { UserConfig } from 'vite' 4 | 5 | export default { 6 | plugins: [react(), vike()], 7 | // Seems like Apollo is heavy? Or is there a way to reduce the size of our Apollo imports? 8 | build: { chunkSizeWarningLimit: 600 }, 9 | } satisfies UserConfig 10 | -------------------------------------------------------------------------------- /examples/full/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/full/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/full/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/full/README.md: -------------------------------------------------------------------------------- 1 | Full-fledged example of using `vike-react`. 2 | 3 | > [!NOTE] 4 | > For more examples, see [Bati](https://batijs.dev) which generates `vike-react` apps. 5 | 6 | ```bash 7 | git clone git@github.com:vikejs/vike-react 8 | cd vike-react/examples/full/ 9 | npm install 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/full/components/ClientOnlyComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const location = window.document.location 4 | 5 | export default function ClientOnlyComponent() { 6 | // Will be printed only in the browser: 7 | console.log('Rendering the ClientOnlyComponent') 8 | 9 | return ( 10 | <div style={{ background: '#eee' }}> 11 | <div>Only loaded in the browser</div> 12 | <div>window.location.href: {location.href}</div> 13 | </div> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/full/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React, { useState } from 'react' 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0) 7 | return ( 8 | <button type="button" onClick={() => setCount((count) => count + 1)}> 9 | Counter {count} 10 | </button> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/full/components/Link.tsx: -------------------------------------------------------------------------------- 1 | export { Link } 2 | 3 | import { usePageContext } from 'vike-react/usePageContext' 4 | import React from 'react' 5 | 6 | function Link({ href, children }: { href: string; children: string }) { 7 | const pageContext = usePageContext() 8 | const { urlPathname } = pageContext 9 | const isActive = href === '/' ? urlPathname === href : urlPathname.startsWith(href) 10 | return ( 11 | <a href={href} className={isActive ? 'is-active' : undefined}> 12 | {children} 13 | </a> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/full/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "build": "vike build", 5 | "preview": "vike build && vike preview" 6 | }, 7 | "dependencies": { 8 | "@types/react": "^19.0.10", 9 | "@types/react-dom": "^19.0.4", 10 | "@vitejs/plugin-react": "^4.3.4", 11 | "node-fetch": "^3.3.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0", 14 | "react-streaming": "^0.4.2", 15 | "typescript": "^5.8.3", 16 | "vike": "^0.4.230", 17 | "vike-react": "0.6.4", 18 | "vite": "^6.2.5" 19 | }, 20 | "type": "module" 21 | } 22 | -------------------------------------------------------------------------------- /examples/full/pages/+Head.tsx: -------------------------------------------------------------------------------- 1 | export { Head } 2 | 3 | import React from 'react' 4 | import logoUrl from '../assets/logo.svg' 5 | 6 | function Head() { 7 | return ( 8 | <> 9 | <link rel="icon" href={logoUrl} /> 10 | </> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/full/pages/+Layout.tsx: -------------------------------------------------------------------------------- 1 | export { Layout } 2 | 3 | import './style.css' 4 | import React from 'react' 5 | import logoUrl from '../assets/logo.svg' 6 | import { Link } from '../components/Link' 7 | 8 | function Layout({ children }: { children: React.ReactNode }) { 9 | return ( 10 | <div 11 | style={{ 12 | display: 'flex', 13 | maxWidth: 900, 14 | margin: 'auto', 15 | }} 16 | > 17 | <Sidebar> 18 | <Logo /> 19 | <Link href="/">Welcome</Link> 20 | <Link href="/star-wars">Data Fetching</Link> 21 | <Link href="/streaming">HTML Streaming</Link> 22 | <Link href="/without-ssr">Without SSR</Link> 23 | <Link href="/starship">Nested Layout</Link> 24 | <Link href="/client-only">Client Only</Link> 25 | <Link href="/images">useConfig()</Link> 26 | </Sidebar> 27 | <Content>{children}</Content> 28 | </div> 29 | ) 30 | } 31 | 32 | function Sidebar({ children }: { children: React.ReactNode }) { 33 | return ( 34 | <div 35 | id="sidebar" 36 | style={{ 37 | padding: 20, 38 | flexShrink: 0, 39 | display: 'flex', 40 | flexDirection: 'column', 41 | lineHeight: '1.8em', 42 | borderRight: '2px solid #eee', 43 | }} 44 | > 45 | {children} 46 | </div> 47 | ) 48 | } 49 | 50 | function Content({ children }: { children: React.ReactNode }) { 51 | return ( 52 | <div id="page-container"> 53 | <div 54 | id="page-content" 55 | style={{ 56 | padding: 20, 57 | paddingBottom: 50, 58 | minHeight: '100vh', 59 | }} 60 | > 61 | {children} 62 | </div> 63 | </div> 64 | ) 65 | } 66 | 67 | function Logo() { 68 | return ( 69 | <div 70 | style={{ 71 | marginTop: 20, 72 | marginBottom: 10, 73 | }} 74 | > 75 | <a href="/"> 76 | <img src={logoUrl} height={64} width={64} /> 77 | </a> 78 | </div> 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/full/pages/+config.ts: -------------------------------------------------------------------------------- 1 | export { config } 2 | 3 | import type { Config } from 'vike/types' 4 | import vikeReact from 'vike-react/config' 5 | 6 | // Default configs (can be overridden by pages) 7 | const config = { 8 | // <title> 9 | title: 'My Vike + React App', 10 | // https://vike.dev/stream 11 | stream: true, 12 | // https://vike.dev/ssr - this line can be removed since `true` is the default 13 | ssr: true, 14 | bodyAttributes: { class: 'dark' }, 15 | viewport: 999, 16 | // https://vike.dev/extends 17 | extends: vikeReact, 18 | } satisfies Config 19 | -------------------------------------------------------------------------------- /examples/full/pages/+react.client.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | 3 | export default { 4 | hydrateRootOptions: { 5 | identifierPrefix: 'some-id-client-prefix', 6 | }, 7 | } satisfies Config['react'] 8 | -------------------------------------------------------------------------------- /examples/full/pages/+react.server.ts: -------------------------------------------------------------------------------- 1 | import type { Config, PageContextServer } from 'vike/types' 2 | 3 | export default (_pageContext: PageContextServer) => 4 | ({ 5 | renderToStringOptions: { 6 | identifierPrefix: 'some-id-server-prefix', 7 | }, 8 | }) satisfies Config['react'] 9 | -------------------------------------------------------------------------------- /examples/full/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { usePageContext } from 'vike-react/usePageContext' 5 | 6 | function Page() { 7 | const { is404 } = usePageContext() 8 | if (is404) { 9 | return ( 10 | <> 11 | <h1>404 Page Not Found</h1> 12 | <p>This page could not be found.</p> 13 | </> 14 | ) 15 | } else { 16 | return ( 17 | <> 18 | <h1>500 Internal Server Error</h1> 19 | <p>Something went wrong.</p> 20 | </> 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/full/pages/client-only/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { Counter } from '../../components/Counter' 5 | import { clientOnly } from 'vike-react/clientOnly' 6 | 7 | const ClientOnlyComponent = clientOnly(() => import('../../components/ClientOnlyComponent')) 8 | 9 | function Page() { 10 | return ( 11 | <> 12 | <h1>My Vike + React app</h1> 13 | This page is: 14 | <ul> 15 | <li>Rendered to HTML.</li> 16 | <li> 17 | Interactive. <Counter /> 18 | </li> 19 | </ul> 20 | <ClientOnlyComponent fallback="Loading the ClientOnlyComponent..." /> 21 | </> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/full/pages/images/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | import { Head } from 'vike-react/Head' 5 | import logoOld from '../../assets/logo.svg' 6 | import logoNew from '../../assets/logo-new.svg' 7 | import { Counter } from '../../components/Counter' 8 | 9 | function Page() { 10 | return ( 11 | <> 12 | <p> 13 | Page showcasing an <code><Image></code> component that adds/teleports structured data ( 14 | <code><script type="application/ld+json"></code>) to <code><head></code>, see HTML. 15 | </p> 16 | <div> 17 | New logo: <Image src={logoNew} author="brillout" /> 18 | </div> 19 | <br /> 20 | <div> 21 | Old logo: <Image src={logoOld} author="Romuald Brillout" /> 22 | </div> 23 | <br /> 24 | <Counter /> 25 | </> 26 | ) 27 | } 28 | 29 | function Image({ src, author }: { src: string; author: string }) { 30 | return ( 31 | <> 32 | <img src={src} height={48} style={{ verticalAlign: 'middle', marginLeft: 10 }} /> 33 | <Head> 34 | <script 35 | type="application/ld+json" 36 | dangerouslySetInnerHTML={{ 37 | __html: JSON.stringify({ 38 | '@context': 'https://schema.org/', 39 | contentUrl: { src }, 40 | creator: { 41 | '@type': 'Person', 42 | name: author, 43 | }, 44 | }), 45 | }} 46 | ></script> 47 | </Head> 48 | </> 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/full/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React, { useId } from 'react' 4 | import { Counter } from '../../components/Counter' 5 | import image from '../../assets/logo-new.svg' 6 | import { Config } from 'vike-react/Config' 7 | 8 | function Page() { 9 | // Will be printed on the server and in the browser: 10 | console.log('Rendering the landing page') 11 | 12 | const id = useId() 13 | console.log(id) 14 | 15 | return ( 16 | <> 17 | <Config image={image}></Config> 18 | <h1>My Vike + React app</h1> 19 | This page is: 20 | <ul> 21 | <li>Rendered to HTML.</li> 22 | <li> 23 | Interactive. <Counter /> 24 | </li> 25 | </ul> 26 | </> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /examples/full/pages/index/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | 3 | export default { 4 | stream: false, 5 | } satisfies Config 6 | -------------------------------------------------------------------------------- /examples/full/pages/star-wars/@id/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import type { Data } from './+data' 5 | import { useData } from 'vike-react/useData' 6 | 7 | function Page() { 8 | const movie = useData<Data>() 9 | return ( 10 | <> 11 | <h1>{movie.title}</h1> 12 | Release Date: {movie.release_date} 13 | <br /> 14 | Director: {movie.director} 15 | <br /> 16 | Producer: {movie.producer} 17 | </> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/full/pages/star-wars/@id/+data.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data } 3 | export type Data = Awaited<ReturnType<typeof data>> 4 | 5 | import { useConfig } from 'vike-react/useConfig' 6 | import type { PageContextServer } from 'vike/types' 7 | import type { MovieDetails } from '../types' 8 | import React from 'react' 9 | 10 | const data = async (pageContext: PageContextServer) => { 11 | const config = useConfig() 12 | 13 | const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`) 14 | let movie = (await response.json()) as MovieDetails 15 | 16 | const { title } = movie 17 | config({ 18 | title, // <title> 19 | Head: ( 20 | <> 21 | <meta name="description" content={`Star Wars Movie ${title} from ${movie.director}`} /> 22 | </> 23 | ), 24 | }) 25 | 26 | // We remove data we don't need because the data is passed to the client; we should 27 | // minimize what is sent over the network. 28 | movie = minimize(movie) 29 | 30 | return movie 31 | } 32 | 33 | function minimize(movie: MovieDetails): MovieDetails { 34 | const { id, title, release_date, director, producer } = movie 35 | movie = { id, title, release_date, director, producer } 36 | return movie 37 | } 38 | -------------------------------------------------------------------------------- /examples/full/pages/star-wars/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import type { Data } from './+data' 5 | import { useData } from 'vike-react/useData' 6 | 7 | function Page() { 8 | const movies = useData<Data>() 9 | return ( 10 | <> 11 | <h1>Star Wars Movies</h1> 12 | <ol> 13 | {movies.map(({ id, title, release_date }) => ( 14 | <li key={id}> 15 | <a href={`/star-wars/${id}`}>{title}</a> ({release_date}) 16 | </li> 17 | ))} 18 | </ol> 19 | <p> 20 | Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>. 21 | </p> 22 | </> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /examples/full/pages/star-wars/index/+data.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/data 2 | export { data } 3 | export type Data = Awaited<ReturnType<typeof data>> 4 | 5 | import fetch from 'node-fetch' 6 | import { useConfig } from 'vike-react/useConfig' 7 | import type { Movie, MovieDetails } from '../types' 8 | import image from '../../../assets/logo-new.svg' 9 | 10 | const data = async () => { 11 | const config = useConfig() 12 | 13 | const response = await fetch('https://brillout.github.io/star-wars/api/films.json') 14 | const moviesData = (await response.json()) as MovieDetails[] 15 | 16 | const n = moviesData.length 17 | config({ 18 | title: `${n} Star Wars Movies`, // <title> 19 | description: `All the ${n} movies from the Star Wars franchise`, // <meta name="description"> 20 | image, // <meta property="og:image"> 21 | }) 22 | 23 | // We remove data we don't need because the data is passed to the client; we should 24 | // minimize what is sent over the network. 25 | const movies = minimize(moviesData) 26 | 27 | return movies 28 | } 29 | 30 | function minimize(movies: MovieDetails[]): Movie[] { 31 | return movies.map((movie) => { 32 | const { title, release_date, id } = movie 33 | return { title, release_date, id } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /examples/full/pages/star-wars/types.ts: -------------------------------------------------------------------------------- 1 | export type Movie = { 2 | id: string 3 | title: string 4 | release_date: string 5 | } 6 | export type MovieDetails = Movie & { 7 | director: string 8 | producer: string 9 | } 10 | -------------------------------------------------------------------------------- /examples/full/pages/starship/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h2>Overview</h2> 9 | <p>The Starship will, at term, repalce all SpaceX's rocket models.</p> 10 | <p>The mission: Make life multi planetary.</p> 11 | <p>Starship drastically reduces the cost of sending payload to space, ensuring SpaceX's financial prosperity.</p> 12 | </> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/full/pages/starship/+config.ts: -------------------------------------------------------------------------------- 1 | export { config } 2 | 3 | import type { Config } from 'vike/types' 4 | 5 | const config = { 6 | // https://vike.dev/keepScrollPosition 7 | keepScrollPosition: true, 8 | } satisfies Config 9 | -------------------------------------------------------------------------------- /examples/full/pages/starship/reviews/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h2>Reviews</h2> 9 | <p>"The Starship brought me and my family to Mars safely." -- Anonymous Family</p> 10 | <p>"A handful of Starships was enough to set up SkyNet. It worked like a charm." -- Skynet Research</p> 11 | </> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/full/pages/starship/spec/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h2>Spec</h2> 9 | <pre> 10 | {[ 11 | 'HEIGHT 50 m / 164 ft', 12 | 'DIAMETER 9 m / 30 ft', 13 | 'PROPELLANT CAPACITY 1200 t / 2.6 Mlb', 14 | 'THRUST 1500 tf / 3.2Mlbf', 15 | 'PAYLOAD CAPACITY 100-150 t orbit dependent', 16 | ].join('\n')} 17 | </pre> 18 | </> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /examples/full/pages/streaming/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React, { Suspense } from 'react' 4 | import { useAsync } from 'react-streaming' 5 | import { Counter } from '../../components/Counter' 6 | 7 | function Page() { 8 | return ( 9 | <> 10 | <h1>Star Wars Movies</h1> 11 | <p> 12 | Same as <code>/star-wars</code> page, but showcasing <a href="https://vike.dev/streaming">HTML Streaming</a> and{' '} 13 | <a href="https://vike.dev/streaming#progressive-rendering">Progressive Rendering</a>. (Note how the interactive 14 | counter works before the data finished loading.) 15 | </p> 16 | <Counter /> 17 | <Suspense fallback={<p>Loading...</p>}> 18 | <MovieList /> 19 | </Suspense> 20 | </> 21 | ) 22 | } 23 | 24 | function MovieList() { 25 | const movies = useAsync(['star-wars-movies'], async () => { 26 | const response = await fetch('https://star-wars.brillout.com/api/films.json') 27 | // Simulate slow network 28 | await new Promise((r) => setTimeout(r, 3 * 1000)) 29 | const movies: Movie[] = (await response.json()).results 30 | return movies 31 | }) 32 | 33 | return ( 34 | <ol> 35 | {movies.map((movies, index) => ( 36 | <li key={index}> 37 | {movies.title} ({movies.release_date}) 38 | </li> 39 | ))} 40 | </ol> 41 | ) 42 | } 43 | 44 | export type Movie = { 45 | title: string 46 | release_date: string 47 | } 48 | -------------------------------------------------------------------------------- /examples/full/pages/style.css: -------------------------------------------------------------------------------- 1 | /* Links */ 2 | a { 3 | text-decoration: none; 4 | } 5 | #sidebar a { 6 | padding: 2px 10px; 7 | margin-left: -10px; 8 | } 9 | #sidebar a.is-active { 10 | background-color: #eee; 11 | } 12 | 13 | /* Reset */ 14 | body { 15 | margin: 0; 16 | font-family: sans-serif; 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Page Transition Anmiation */ 23 | #page-content { 24 | opacity: 1; 25 | transition: opacity 0.3s ease-in-out; 26 | } 27 | body.page-is-transitioning #page-content { 28 | opacity: 0; 29 | } 30 | 31 | /* Inline code blocks */ 32 | code { 33 | font-family: monospace; 34 | background-color: #eaeaea; 35 | padding: 3px 5px; 36 | border-radius: 4px; 37 | } 38 | -------------------------------------------------------------------------------- /examples/full/pages/without-ssr/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { Counter } from '../../components/Counter' 5 | 6 | function Page() { 7 | // Will be printed only in the browser: 8 | console.log('Rendering page without SSR') 9 | 10 | return ( 11 | <> 12 | <h1>Without SSR</h1> 13 | This page is rendered only in the browser: 14 | <ul> 15 | <li> 16 | It's interactive. <Counter /> 17 | </li> 18 | <li>It isn't rendered to HTML.</li> 19 | </ul> 20 | </> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/full/pages/without-ssr/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | 3 | export default { 4 | // https://vike.dev/ssr 5 | ssr: false, 6 | title: 'No SSR', 7 | } satisfies Config 8 | -------------------------------------------------------------------------------- /examples/full/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/full/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | import { UserConfig } from 'vite' 4 | 5 | export default { 6 | plugins: [react(), vike()], 7 | } satisfies UserConfig 8 | -------------------------------------------------------------------------------- /examples/minimal/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/minimal/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/minimal/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/minimal/.testRun.ts: -------------------------------------------------------------------------------- 1 | export { testRunClassic as testRun } 2 | 3 | import { test, expect, run, fetchHtml, page, getServerUrl, autoRetry } from '@brillout/test-e2e' 4 | 5 | function testRunClassic(cmd: 'pnpm run dev' | 'pnpm run preview') { 6 | run(cmd) 7 | 8 | test('page content is rendered to HTML', async () => { 9 | const html = await fetchHtml('/') 10 | expect(html).toContain('<h1>Welcome</h1>') 11 | }) 12 | 13 | test('page is rendered to the DOM and interactive', async () => { 14 | await page.goto(getServerUrl() + '/') 15 | await page.click('a[href="/"]') 16 | expect(await page.textContent('h1')).toBe('Welcome') 17 | await testCounter() 18 | }) 19 | 20 | test('about page', async () => { 21 | await page.click('a[href="/about"]') 22 | await autoRetry(async () => { 23 | expect(await page.textContent('h1')).toBe('About') 24 | }) 25 | expect(await page.textContent('p')).toBe('Example of using Vike.') 26 | const html = await fetchHtml('/about') 27 | expect(html).toContain('<h1>About</h1>') 28 | }) 29 | } 30 | 31 | async function testCounter(currentValue = 0) { 32 | // autoRetry() in case page just got client-side navigated 33 | await autoRetry( 34 | async () => { 35 | const btn = page.locator('button', { hasText: 'Counter' }) 36 | expect(await btn.textContent()).toBe(`Counter ${currentValue}`) 37 | }, 38 | { timeout: 5 * 1000 }, 39 | ) 40 | // autoRetry() in case page isn't hydrated yet 41 | await autoRetry( 42 | async () => { 43 | const btn = page.locator('button', { hasText: 'Counter' }) 44 | await btn.click() 45 | expect(await btn.textContent()).toBe(`Counter ${currentValue + 1}`) 46 | }, 47 | { timeout: 5 * 1000 }, 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /examples/minimal/README.md: -------------------------------------------------------------------------------- 1 | Minimal example of using `vike-react`. 2 | 3 | > [!NOTE] 4 | > For more examples, see [Bati](https://batijs.dev) which generates `vike-react` apps. 5 | 6 | ```bash 7 | git clone git@github.com:vikejs/vike-react 8 | cd vike-react/examples/minimal/ 9 | npm install 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "build": "vike build", 5 | "preview": "vike build && vike preview" 6 | }, 7 | "dependencies": { 8 | "@vitejs/plugin-react": "4.2.1", 9 | "react": "^19.0.0", 10 | "react-dom": "^19.0.0", 11 | "vike": "^0.4.230", 12 | "vike-react": "0.6.4", 13 | "vite": "^6.2.5" 14 | }, 15 | "type": "module" 16 | } 17 | -------------------------------------------------------------------------------- /examples/minimal/pages/+config.js: -------------------------------------------------------------------------------- 1 | export { config } 2 | 3 | import vikeReact from 'vike-react/config' 4 | import { Layout } from './Layout' 5 | 6 | const config = { 7 | // https://vike.dev/Layout 8 | Layout: Layout, 9 | // https://vike.dev/extends 10 | extends: vikeReact, 11 | } 12 | -------------------------------------------------------------------------------- /examples/minimal/pages/Layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | a { 9 | text-decoration: none; 10 | } 11 | 12 | .navitem { 13 | padding: 3px; 14 | } 15 | -------------------------------------------------------------------------------- /examples/minimal/pages/Layout.jsx: -------------------------------------------------------------------------------- 1 | export { Layout } 2 | 3 | import React from 'react' 4 | import './Layout.css' 5 | 6 | function Layout({ children }) { 7 | return ( 8 | <PageLayout> 9 | <Sidebar> 10 | <a className="navitem" href="/"> 11 | Home 12 | </a> 13 | <a className="navitem" href="/about"> 14 | About 15 | </a> 16 | </Sidebar> 17 | <Content>{children}</Content> 18 | </PageLayout> 19 | ) 20 | } 21 | 22 | function PageLayout({ children }) { 23 | return ( 24 | <div 25 | style={{ 26 | display: 'flex', 27 | maxWidth: 900, 28 | margin: 'auto', 29 | }} 30 | > 31 | {children} 32 | </div> 33 | ) 34 | } 35 | 36 | function Sidebar({ children }) { 37 | return ( 38 | <div 39 | style={{ 40 | padding: 20, 41 | paddingTop: 42, 42 | flexShrink: 0, 43 | display: 'flex', 44 | flexDirection: 'column', 45 | alignItems: 'center', 46 | lineHeight: '1.8em', 47 | }} 48 | > 49 | {children} 50 | </div> 51 | ) 52 | } 53 | 54 | function Content({ children }) { 55 | return ( 56 | <div 57 | style={{ 58 | padding: 20, 59 | paddingBottom: 50, 60 | borderLeft: '2px solid #eee', 61 | minHeight: '100vh', 62 | }} 63 | > 64 | {children} 65 | </div> 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /examples/minimal/pages/about/+Page.jsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | 5 | function Page() { 6 | return ( 7 | <> 8 | <h1>About</h1> 9 | <p>Example of using Vike.</p> 10 | </> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/minimal/pages/index/+Page.jsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { Counter } from './Counter' 5 | 6 | function Page() { 7 | return ( 8 | <> 9 | <h1>Welcome</h1> 10 | This page is: 11 | <ul> 12 | <li>Rendered to HTML.</li> 13 | <li> 14 | Interactive. <Counter /> 15 | </li> 16 | </ul> 17 | </> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/minimal/pages/index/Counter.jsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React, { useState } from 'react' 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0) 7 | return ( 8 | <button type="button" onClick={() => setCount((count) => count + 1)}> 9 | Counter {count} 10 | </button> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/minimal/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | 4 | export default { 5 | plugins: [react(), vike()], 6 | } 7 | -------------------------------------------------------------------------------- /examples/query/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/query/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/query/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/query/.testRun.ts: -------------------------------------------------------------------------------- 1 | export { testRun } 2 | 3 | import { test, expect, run, page, getServerUrl, autoRetry } from '@brillout/test-e2e' 4 | 5 | function testRun(cmd: `pnpm run ${'dev' | 'preview'}`) { 6 | run(cmd) 7 | 8 | const content = 'Return of the Jedi' 9 | const loading = 'Loading movies...' 10 | const titleDefault = 'My Vike + React App' 11 | const titleOverriden = '6 movies' 12 | const titleAsScript = `<script>document.title = "${titleOverriden}"</script>` 13 | const description = '<meta name="description" content="List of 6 Star Wars movies."/>' 14 | test('HTML (as user)', async () => { 15 | const html = await fetchAsUser('/') 16 | expect(html).toContain(content) 17 | expect(html).toContain(loading) 18 | expect(html).toContain(titleAsScript) 19 | expect(getTitle(html)).toBe(titleDefault) 20 | expect(html.split('<title>').length).toBe(2) 21 | expect(html).not.toContain(description) 22 | }) 23 | test('HTML (as bot)', async () => { 24 | const html = await fetchAsBot('/') 25 | expect(html).toContain(content) 26 | expect(html).not.toContain(loading) 27 | expect(html).not.toContain(titleAsScript) 28 | expect(getTitle(html)).toBe(titleOverriden) 29 | expect(html.split('<title>').length).toBe(2) 30 | expect(html).toContain(description) 31 | }) 32 | test('DOM', async () => { 33 | await page.goto(getServerUrl() + '/') 34 | const body = await page.textContent('body') 35 | // Playwright seems to await the HTML stream 36 | expect(body).not.toContain(loading) 37 | expect(body).toContain(content) 38 | await testCounter() 39 | }) 40 | } 41 | 42 | function getTitle(html: string) { 43 | const title = html.match(/<title>(.*?)<\/title>/i)?.[1] 44 | return title 45 | } 46 | 47 | async function testCounter() { 48 | // autoRetry() for awaiting client-side code loading & executing 49 | await autoRetry( 50 | async () => { 51 | expect(await page.textContent('button')).toBe('Counter 0') 52 | await page.click('button') 53 | expect(await page.textContent('button')).toContain('Counter 1') 54 | }, 55 | { timeout: 5 * 1000 }, 56 | ) 57 | } 58 | 59 | async function fetchAsBot(pathname: string) { 60 | return await fetchHtml(pathname, 'curl/8.5.0') 61 | } 62 | async function fetchAsUser(pathname: string) { 63 | return await fetchHtml(pathname, 'chrome') 64 | } 65 | async function fetchHtml(pathname: string, userAgent: string) { 66 | const response = await fetch(getServerUrl() + pathname, { headers: { ['User-Agent']: userAgent } }) 67 | const html = await response.text() 68 | return html 69 | } 70 | -------------------------------------------------------------------------------- /examples/query/README.md: -------------------------------------------------------------------------------- 1 | Example of using `vike-react-query`. 2 | 3 | > [!NOTE] 4 | > For more examples, see [Bati](https://batijs.dev) which generates `vike-react` apps. 5 | 6 | ```bash 7 | git clone git@github.com:vikejs/vike-react 8 | cd vike-react/examples/query/ 9 | npm install 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/query/layouts/HeadDefault.tsx: -------------------------------------------------------------------------------- 1 | export default HeadDefault 2 | 3 | import React from 'react' 4 | import logoUrl from '../assets/logo.svg' 5 | 6 | function HeadDefault() { 7 | return ( 8 | <> 9 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 10 | <link rel="icon" href={logoUrl} /> 11 | </> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/query/layouts/LayoutDefault.tsx: -------------------------------------------------------------------------------- 1 | export default LayoutDefault 2 | 3 | import './style.css' 4 | import React from 'react' 5 | import logoUrl from '../assets/logo.svg' 6 | 7 | function LayoutDefault({ children }: { children: React.ReactNode }) { 8 | return ( 9 | <div 10 | style={{ 11 | display: 'flex', 12 | maxWidth: 900, 13 | margin: 'auto', 14 | }} 15 | > 16 | <Sidebar> 17 | <Logo /> 18 | </Sidebar> 19 | <Content>{children}</Content> 20 | </div> 21 | ) 22 | } 23 | 24 | function Sidebar({ children }: { children: React.ReactNode }) { 25 | return ( 26 | <div 27 | id="sidebar" 28 | style={{ 29 | padding: 20, 30 | flexShrink: 0, 31 | display: 'flex', 32 | flexDirection: 'column', 33 | lineHeight: '1.8em', 34 | borderRight: '2px solid #eee', 35 | }} 36 | > 37 | {children} 38 | </div> 39 | ) 40 | } 41 | 42 | function Content({ children }: { children: React.ReactNode }) { 43 | return ( 44 | <div id="page-container"> 45 | <div 46 | id="page-content" 47 | style={{ 48 | padding: 20, 49 | paddingBottom: 50, 50 | minHeight: '100vh', 51 | }} 52 | > 53 | {children} 54 | </div> 55 | </div> 56 | ) 57 | } 58 | 59 | function Logo() { 60 | return ( 61 | <div 62 | style={{ 63 | marginTop: 20, 64 | marginBottom: 10, 65 | }} 66 | > 67 | <a href="/"> 68 | <img src={logoUrl} height={64} width={64} /> 69 | </a> 70 | </div> 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /examples/query/layouts/style.css: -------------------------------------------------------------------------------- 1 | /* Links */ 2 | a { 3 | text-decoration: none; 4 | } 5 | #sidebar a { 6 | padding: 2px 10px; 7 | margin-left: -10px; 8 | } 9 | #sidebar a.is-active { 10 | background-color: #eee; 11 | } 12 | 13 | /* Reset */ 14 | body { 15 | margin: 0; 16 | font-family: sans-serif; 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Page Transition Anmiation */ 23 | #page-content { 24 | opacity: 1; 25 | transition: opacity 0.3s ease-in-out; 26 | } 27 | body.page-is-transitioning #page-content { 28 | opacity: 0; 29 | } 30 | -------------------------------------------------------------------------------- /examples/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "preview": "vike build && vike preview" 5 | }, 6 | "dependencies": { 7 | "@types/react": "^19.0.10", 8 | "@types/react-dom": "^19.0.4", 9 | "@vitejs/plugin-react": "^4.3.4", 10 | "react": "^19.0.0", 11 | "react-dom": "^19.0.0", 12 | "typescript": "^5.8.3", 13 | "vike": "^0.4.230", 14 | "vike-react": "0.6.4", 15 | "vike-react-query": "0.1.4", 16 | "@tanstack/react-query": "^5.20.1", 17 | "vite": "^6.2.5" 18 | }, 19 | "type": "module" 20 | } 21 | -------------------------------------------------------------------------------- /examples/query/pages/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | import Layout from '../layouts/LayoutDefault' 3 | import Head from '../layouts/HeadDefault' 4 | import vikeReact from 'vike-react/config' 5 | import vikeReactQuery from 'vike-react-query/config' 6 | 7 | // Default configs (can be overridden by pages) 8 | export default { 9 | Layout, 10 | Head, 11 | // <title> 12 | title: 'My Vike + React App', 13 | extends: [vikeReact, vikeReactQuery], 14 | passToClient: ['routeParams'], 15 | } satisfies Config 16 | -------------------------------------------------------------------------------- /examples/query/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | import { Counter } from './Counter' 5 | import { Movies } from './Movies' 6 | 7 | function Page() { 8 | return ( 9 | <> 10 | <h1>My Vike + React app</h1> 11 | This page is: 12 | <ul> 13 | <li>Rendered to HTML.</li> 14 | <li> 15 | Interactive while loading. <Counter /> 16 | </li> 17 | </ul> 18 | <Movies /> 19 | </> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/query/pages/index/@id/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | import { usePageContext } from 'vike-react/usePageContext' 5 | import { Movie } from './Movie' 6 | 7 | function Page() { 8 | const pageContext = usePageContext() 9 | const id = pageContext.routeParams!['id'] 10 | return <Movie id={id} /> 11 | } 12 | -------------------------------------------------------------------------------- /examples/query/pages/index/@id/Movie.tsx: -------------------------------------------------------------------------------- 1 | export { Movie } 2 | 3 | import React from 'react' 4 | import { withFallback } from 'vike-react-query' 5 | import { useConfig } from 'vike-react/useConfig' 6 | import { useSuspenseQuery } from '@tanstack/react-query' 7 | import { MovieDetails } from '../types' 8 | 9 | const Movie = withFallback( 10 | ({ id }: { id: string }) => { 11 | const config = useConfig() 12 | const result = useSuspenseQuery({ 13 | queryKey: ['movie', id], 14 | queryFn: () => getStarWarsMovie(id), 15 | // Disabled to showcase error fallback 16 | retry: false, 17 | }) 18 | 19 | const { title, release_date } = result.data 20 | config({ 21 | title, // <title> 22 | Head: ( 23 | <> 24 | <meta name="description" content={`Star Wars Movie ${title} from ${result.data.director}`} /> 25 | </> 26 | ), 27 | }) 28 | 29 | return ( 30 | <> 31 | <h1>Star Wars Movies</h1> 32 | <ul> 33 | <li> 34 | Title: <b>{title}</b> 35 | </li> 36 | <li> 37 | Release date: <b>{release_date}</b> 38 | </li> 39 | </ul> 40 | <p> 41 | Source: <a href="https://star-wars.brillout.com">star-wars.brillout.com</a>. 42 | </p> 43 | </> 44 | ) 45 | }, 46 | { 47 | Loading: ({ id }) => `Loading movie ${id}`, 48 | // Try commenting out the error fallback 49 | Error: ({ id, error, retry }) => ( 50 | <> 51 | <div>Loading movie {id} failed</div> 52 | <div>{error.message}</div> 53 | <button onClick={() => retry()}>Try again</button> 54 | </> 55 | ), 56 | }, 57 | ) 58 | 59 | async function getStarWarsMovie(id: string): Promise<MovieDetails> { 60 | await new Promise((r) => setTimeout(r, 500)) 61 | 62 | if (Math.random() > 0.4) { 63 | throw new Error('Failed to fetch') 64 | } 65 | 66 | const response = await fetch(`https://star-wars.brillout.com/api/films/${id}.json`) 67 | return response.json() 68 | } 69 | -------------------------------------------------------------------------------- /examples/query/pages/index/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React, { useState } from 'react' 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0) 7 | 8 | return <button onClick={() => setCount((count) => count + 1)}>Counter {count}</button> 9 | } 10 | -------------------------------------------------------------------------------- /examples/query/pages/index/Movies.tsx: -------------------------------------------------------------------------------- 1 | export { Movies } 2 | 3 | import React from 'react' 4 | import { withFallback } from 'vike-react-query' 5 | import { useSuspenseQuery } from '@tanstack/react-query' 6 | import { navigate } from 'vike/client/router' 7 | import { MovieDetails } from './types' 8 | import { Config } from 'vike-react/Config' 9 | import { Head } from 'vike-react/Head' 10 | 11 | const Movies = withFallback(() => { 12 | const result = useSuspenseQuery({ 13 | queryKey: ['movies'], 14 | queryFn: getStarWarsMovies, 15 | }) 16 | 17 | const movies = result.data 18 | const onNavigate = (id: string) => { 19 | navigate(`/${id}`) 20 | } 21 | 22 | return ( 23 | <> 24 | <Config title={`${movies.length} movies`} /> 25 | <Head> 26 | <meta name="description" content={`List of ${movies.length} Star Wars movies.`} /> 27 | </Head> 28 | <h1>Star Wars Movies</h1> 29 | <ol> 30 | {movies.map(({ id, title, release_date }) => ( 31 | <li key={id}> 32 | <button onClick={() => onNavigate(id)}>{title}</button> ({release_date}) 33 | </li> 34 | ))} 35 | </ol> 36 | <p> 37 | Source: <a href="https://star-wars.brillout.com">star-wars.brillout.com</a>. 38 | </p> 39 | </> 40 | ) 41 | }, 'Loading movies...') 42 | 43 | async function getStarWarsMovies(): Promise<MovieDetails[]> { 44 | // Simulate slow network 45 | await new Promise((r) => setTimeout(r, 2000)) 46 | 47 | const response = await fetch('https://star-wars.brillout.com/api/films.json') 48 | let movies: MovieDetails[] = ((await response.json()) as any).results 49 | movies = movies.map((movie: MovieDetails, i: number) => ({ 50 | ...movie, 51 | id: String(i + 1), 52 | })) 53 | return movies 54 | } 55 | -------------------------------------------------------------------------------- /examples/query/pages/index/types.ts: -------------------------------------------------------------------------------- 1 | export type MovieDetails = { 2 | id: string 3 | title: string 4 | release_date: string 5 | director: string 6 | producer: string 7 | } 8 | -------------------------------------------------------------------------------- /examples/query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/query/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | import { UserConfig } from 'vite' 4 | 5 | export default { 6 | plugins: [react(), vike()], 7 | } satisfies UserConfig 8 | -------------------------------------------------------------------------------- /examples/redux/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/redux/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/redux/.test-preview-ssg.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview:ssg') 3 | -------------------------------------------------------------------------------- /examples/redux/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/redux/.testRun.ts: -------------------------------------------------------------------------------- 1 | export { testRun } 2 | 3 | import { test, expect, run, page, getServerUrl, autoRetry, fetchHtml } from '@brillout/test-e2e' 4 | 5 | function testRun(cmd: `pnpm run ${'dev' | 'preview' | 'preview:ssg'}`) { 6 | run(cmd) 7 | 8 | test('count', async () => { 9 | await page.goto(getServerUrl() + '/') 10 | await testCounter() 11 | await clientSideNavigation() 12 | await fullPageReload() 13 | }) 14 | async function clientSideNavigation() { 15 | await page.click('a:has-text("About")') 16 | await page.waitForFunction(() => (window as any)._vike.fullyRenderedUrl === '/about') 17 | await testCounter(1) 18 | await page.click('a:has-text("Welcome")') 19 | await page.waitForFunction(() => (window as any)._vike.fullyRenderedUrl === '/') 20 | await testCounter(2) 21 | } 22 | async function fullPageReload() { 23 | await page.goto(getServerUrl() + '/about') 24 | await testCounter() 25 | await page.goto(getServerUrl() + '/') 26 | await testCounter() 27 | } 28 | 29 | test('todos - initial list', async () => { 30 | await page.goto(getServerUrl() + '/') 31 | await expectInitialList() 32 | }) 33 | async function expectInitialList() { 34 | const buyApples = 'Buy apples' 35 | const nodeVerison = `Node.js ${process.version}` 36 | { 37 | const html = await fetchHtml('/') 38 | expect(html).toContain(`<li>${buyApples}</li>`) 39 | expect(html).toContain(nodeVerison) 40 | } 41 | { 42 | const bodyText = await page.textContent('body') 43 | expect(bodyText).toContain(buyApples) 44 | expect(bodyText).toContain(nodeVerison) 45 | expect(await getNumberOfItems()).toBe(2) 46 | } 47 | } 48 | 49 | test('todos - add to-do', async () => { 50 | await page.fill('input[type="text"]', 'Buy bananas') 51 | await page.click('button[type="submit"]') 52 | const expectBananas = async () => { 53 | await autoRetry(async () => { 54 | expect(await getNumberOfItems()).toBe(3) 55 | }) 56 | expect(await page.textContent('body')).toContain('Buy bananas') 57 | } 58 | await expectBananas() 59 | 60 | await testCounter() 61 | await clientSideNavigation() 62 | await expectBananas() 63 | 64 | // Full page reload 65 | await fullPageReload() 66 | await expectInitialList() 67 | }) 68 | } 69 | 70 | async function getNumberOfItems() { 71 | return await page.evaluate(() => document.querySelectorAll('#todo-list li').length) 72 | } 73 | 74 | async function testCounter(inc: 0 | 1 | 2 = 0) { 75 | const counterInitValue = 42 76 | const currentValue = counterInitValue + inc 77 | // autoRetry() in case page just got client-side navigated 78 | await autoRetry( 79 | async () => { 80 | const btn = page.locator('button', { hasText: 'Counter' }) 81 | expect(await btn.textContent()).toBe(`Counter ${currentValue}`) 82 | }, 83 | { timeout: 5 * 1000 }, 84 | ) 85 | // autoRetry() in case page isn't hydrated yet 86 | await autoRetry( 87 | async () => { 88 | const btn = page.locator('button', { hasText: 'Counter' }) 89 | await btn.click() 90 | expect(await btn.textContent()).toBe(`Counter ${currentValue + 1}`) 91 | }, 92 | { timeout: 5 * 1000 }, 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /examples/redux/README.md: -------------------------------------------------------------------------------- 1 | Example of using `vike-react-redux`. 2 | 3 | ```bash 4 | git clone git@github.com:vikejs/vike-react 5 | cd vike-react/examples/redux/ 6 | npm install 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/redux/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React from 'react' 4 | import { useAppDispatch, useAppSelector } from '../store/hooks' 5 | import { increment, selectCount } from '../store/slices/count' 6 | 7 | function Counter() { 8 | const dispatch = useAppDispatch() 9 | const count = useAppSelector(selectCount) 10 | return ( 11 | <button type="button" onClick={() => dispatch(increment())}> 12 | Counter {count} 13 | </button> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/redux/components/Counter/fetchCountInit.ts: -------------------------------------------------------------------------------- 1 | export { fetchCountInit } 2 | 3 | // Pretending the value is fetched over the network 4 | async function fetchCountInit() { 5 | return 42 6 | } 7 | -------------------------------------------------------------------------------- /examples/redux/components/Link.tsx: -------------------------------------------------------------------------------- 1 | export { Link } 2 | 3 | import { usePageContext } from 'vike-react/usePageContext' 4 | import React from 'react' 5 | 6 | function Link({ href, children }: { href: string; children: string }) { 7 | const pageContext = usePageContext() 8 | const { urlPathname } = pageContext 9 | const isActive = href === '/' ? urlPathname === href : urlPathname.startsWith(href) 10 | return ( 11 | <a href={href} className={isActive ? 'is-active' : undefined}> 12 | {children} 13 | </a> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/redux/layouts/LayoutDefault.tsx: -------------------------------------------------------------------------------- 1 | export default LayoutDefault 2 | 3 | import './style.css' 4 | import React from 'react' 5 | import logoUrl from './logo.svg' 6 | import { Link } from '../components/Link' 7 | 8 | function LayoutDefault({ children }: { children: React.ReactNode }) { 9 | return ( 10 | <div 11 | style={{ 12 | display: 'flex', 13 | maxWidth: 900, 14 | margin: 'auto', 15 | }} 16 | > 17 | <Sidebar> 18 | <Logo /> 19 | <Link href="/">Welcome</Link> 20 | <Link href="/about">About</Link> 21 | </Sidebar> 22 | <Content>{children}</Content> 23 | </div> 24 | ) 25 | } 26 | 27 | function Sidebar({ children }: { children: React.ReactNode }) { 28 | return ( 29 | <div 30 | id="sidebar" 31 | style={{ 32 | padding: 20, 33 | flexShrink: 0, 34 | display: 'flex', 35 | flexDirection: 'column', 36 | lineHeight: '1.8em', 37 | borderRight: '2px solid #eee', 38 | }} 39 | > 40 | {children} 41 | </div> 42 | ) 43 | } 44 | 45 | function Content({ children }: { children: React.ReactNode }) { 46 | return ( 47 | <div id="page-container"> 48 | <div 49 | id="page-content" 50 | style={{ 51 | padding: 20, 52 | paddingBottom: 50, 53 | minHeight: '100vh', 54 | }} 55 | > 56 | {children} 57 | </div> 58 | </div> 59 | ) 60 | } 61 | 62 | function Logo() { 63 | return ( 64 | <div 65 | style={{ 66 | marginTop: 20, 67 | marginBottom: 10, 68 | }} 69 | > 70 | <a href="/"> 71 | <img src={logoUrl} height={64} width={64} /> 72 | </a> 73 | </div> 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /examples/redux/layouts/style.css: -------------------------------------------------------------------------------- 1 | /* Links */ 2 | a { 3 | text-decoration: none; 4 | } 5 | #sidebar a { 6 | padding: 2px 10px; 7 | margin-left: -10px; 8 | } 9 | #sidebar a.is-active { 10 | background-color: #eee; 11 | } 12 | 13 | /* Reset */ 14 | body { 15 | margin: 0; 16 | font-family: sans-serif; 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Page Transition Anmiation */ 23 | #page-content { 24 | opacity: 1; 25 | transition: opacity 0.3s ease-in-out; 26 | } 27 | body.page-is-transitioning #page-content { 28 | opacity: 0; 29 | } 30 | -------------------------------------------------------------------------------- /examples/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "build": "vike build", 5 | "preview": "vike build && vike preview", 6 | "preview:ssg": "vike build --prerender && vike preview --prerender" 7 | }, 8 | "dependencies": { 9 | "@reduxjs/toolkit": "^2.6.1", 10 | "@types/react": "^19.0.10", 11 | "@types/react-dom": "^19.0.4", 12 | "@vitejs/plugin-react": "^4.3.4", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0", 15 | "react-redux": "^9.2.0", 16 | "typescript": "^5.8.3", 17 | "vike": "^0.4.230", 18 | "vike-react": "0.6.4", 19 | "vike-react-redux": "0.1.0", 20 | "vite": "^6.2.5" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /examples/redux/pages/+config.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../layouts/LayoutDefault' 2 | import vikeReact from 'vike-react/config' 3 | import vikeReactRedux from 'vike-react-redux/config' 4 | import type { Config } from 'vike/types' 5 | 6 | export default { 7 | Layout, 8 | extends: [vikeReact, vikeReactRedux], 9 | } satisfies Config 10 | -------------------------------------------------------------------------------- /examples/redux/pages/+redux.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from '../store/createStore' 2 | export default { createStore } 3 | -------------------------------------------------------------------------------- /examples/redux/pages/about/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Counter } from '../../components/Counter' 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 | <h1>About</h1> 8 | <p>The counter value is the same as on the Welcome page.</p> 9 | <Counter /> 10 | </> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/redux/pages/about/+config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | 3 | export default { 4 | ssr: false, 5 | } satisfies Config 6 | -------------------------------------------------------------------------------- /examples/redux/pages/about/+data.ts: -------------------------------------------------------------------------------- 1 | // Environment: server 2 | export { data } 3 | export type Data = Awaited<ReturnType<typeof data>> 4 | 5 | import { fetchCountInit } from '../../components/Counter/fetchCountInit' 6 | import type { PageContextServer } from 'vike/types' 7 | 8 | async function data(pageContext: PageContextServer) { 9 | const countInitial = await fetchCountInit() 10 | return { 11 | countInitial, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/redux/pages/about/+onData.ts: -------------------------------------------------------------------------------- 1 | // Environment: server, client 2 | export { onData } 3 | 4 | import type { PageContext } from 'vike/types' 5 | import type { Data } from './+data' 6 | import { initializeCount } from '../../store/slices/count' 7 | 8 | function onData(pageContext: PageContext & { data?: Data }) { 9 | const { store } = pageContext 10 | store.dispatch(initializeCount(pageContext.data!.countInitial)) 11 | 12 | // Saving KBs: we don't need pageContext.data (we use the store instead) 13 | // - If we don't delete pageContext.data then Vike sends pageContext.data to the client-side 14 | // - This optimization only works if the page is SSR'd: if the page is pre-rendered then don't do this 15 | if (!pageContext.isPrerendering) delete pageContext.data 16 | } 17 | -------------------------------------------------------------------------------- /examples/redux/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Counter } from '../../components/Counter' 3 | import { TodoList } from './TodoList' 4 | 5 | export default function Page() { 6 | return ( 7 | <> 8 | <h1>My Vike app</h1> 9 | This page is: 10 | <ul> 11 | <li>Rendered to HTML.</li> 12 | <li> 13 | Interactive. <Counter /> 14 | </li> 15 | </ul> 16 | <TodoList /> 17 | </> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/redux/pages/index/+data.ts: -------------------------------------------------------------------------------- 1 | // Environment: server 2 | export { data } 3 | export type Data = Awaited<ReturnType<typeof data>> 4 | 5 | import { fetchCountInit } from '../../components/Counter/fetchCountInit' 6 | import type { PageContextServer } from 'vike/types' 7 | 8 | async function data(pageContext: PageContextServer) { 9 | const [countInitial, todoItemsInitial] = await Promise.all([fetchCountInit(), fetchTodosInit()]) 10 | return { countInitial, todoItemsInitial } 11 | } 12 | 13 | // Pretending the list is fetched over the network 14 | async function fetchTodosInit() { 15 | return [ 16 | // 17 | { text: 'Buy apples' }, 18 | { text: `Update Node.js ${process.version} to latest version` }, 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/redux/pages/index/+onData.ts: -------------------------------------------------------------------------------- 1 | // Environment: server, client 2 | export { onData } 3 | 4 | import type { PageContext } from 'vike/types' 5 | import type { Data } from './+data' 6 | import { initializeCount } from '../../store/slices/count' 7 | import { initializeTodos } from '../../store/slices/todos' 8 | 9 | function onData(pageContext: PageContext & { data?: Data }) { 10 | const { store } = pageContext 11 | store.dispatch(initializeTodos(pageContext.data!.todoItemsInitial)) 12 | store.dispatch(initializeCount(pageContext.data!.countInitial)) 13 | 14 | // Saving KBs: we don't need pageContext.data (we use the store instead) 15 | // - If we don't delete pageContext.data then Vike sends pageContext.data to the client-side 16 | // - This optimization only works if the page is SSR'd: if the page is pre-rendered then don't do this 17 | if (!pageContext.isPrerendering) delete pageContext.data 18 | } 19 | -------------------------------------------------------------------------------- /examples/redux/pages/index/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import React from 'react' 4 | import { useAppDispatch, useAppSelector } from '../../store/hooks' 5 | import { addTodo, selectTodos } from '../../store/slices/todos' 6 | 7 | export function TodoList() { 8 | const [newTodo, setNewTodo] = useState('') 9 | const dispatch = useAppDispatch() 10 | const todoItems = useAppSelector(selectTodos) 11 | return ( 12 | <> 13 | <h2>To-Do</h2> 14 | <ul id="todo-list"> 15 | {todoItems.map((todoItem, index) => ( 16 | // biome-ignore lint: 17 | <li key={index}>{todoItem.text}</li> 18 | ))} 19 | </ul> 20 | <div> 21 | <form 22 | onSubmit={async (ev) => { 23 | ev.preventDefault() 24 | dispatch(addTodo(newTodo)) 25 | setNewTodo('') 26 | }} 27 | > 28 | <input type="text" onChange={(ev) => setNewTodo(ev.target.value)} value={newTodo} /> 29 | <button type="submit">Add to-do</button> 30 | </form> 31 | </div> 32 | </> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/redux/store/createStore.ts: -------------------------------------------------------------------------------- 1 | export { createStore } 2 | export type AppStore = ReturnType<typeof createStore> 3 | export type RootState = ReturnType<AppStore['getState']> 4 | export type AppDispatch = AppStore['dispatch'] 5 | 6 | import type { PageContext } from 'vike/types' 7 | import { combineReducers, configureStore } from '@reduxjs/toolkit' 8 | import { countReducer } from './slices/count' 9 | import { todosReducer } from './slices/todos' 10 | const reducer = combineReducers({ count: countReducer, todos: todosReducer }) 11 | 12 | function createStore(pageContext: PageContext) { 13 | const preloadedState = pageContext.isClientSide ? pageContext.redux?.ssrState : undefined 14 | return configureStore({ reducer, preloadedState }) 15 | } 16 | -------------------------------------------------------------------------------- /examples/redux/store/hooks.ts: -------------------------------------------------------------------------------- 1 | // This file serves as a central hub for re-exporting pre-typed Redux hooks. 2 | import { useDispatch, useSelector, useStore } from 'react-redux' 3 | import type { AppDispatch, AppStore, RootState } from './createStore' 4 | 5 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 6 | export const useAppDispatch = useDispatch.withTypes<AppDispatch>() 7 | export const useAppSelector = useSelector.withTypes<RootState>() 8 | export const useAppStore = useStore.withTypes<AppStore>() 9 | -------------------------------------------------------------------------------- /examples/redux/store/slices/count.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | const initialState = { countValue: 0 } 5 | 6 | const countSlice = createSlice({ 7 | name: 'count', 8 | initialState, 9 | reducers: { 10 | increment: (state) => { 11 | state.countValue += 1 12 | }, 13 | decrement: (state) => { 14 | state.countValue -= 1 15 | }, 16 | initializeCount: (state, action: PayloadAction<number>) => { 17 | if (state.countValue !== 0) return 18 | state.countValue = action.payload 19 | }, 20 | }, 21 | selectors: { 22 | selectCount: (state) => state.countValue, 23 | }, 24 | }) 25 | 26 | export const countReducer = countSlice.reducer 27 | export const { selectCount } = countSlice.selectors 28 | export const { increment, decrement, initializeCount } = countSlice.actions 29 | -------------------------------------------------------------------------------- /examples/redux/store/slices/todos.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import type { PayloadAction } from '@reduxjs/toolkit' 3 | 4 | type Todo = { text: string } 5 | const initialState = { todoItems: [] as Todo[] } 6 | 7 | const todosSlice = createSlice({ 8 | name: 'todos', 9 | initialState, 10 | reducers: { 11 | addTodo: (state, action: PayloadAction<string>) => { 12 | state.todoItems.push({ text: action.payload }) 13 | }, 14 | initializeTodos: (state, action: PayloadAction<Todo[]>) => { 15 | if (state.todoItems.length > 0) return 16 | state.todoItems = action.payload 17 | }, 18 | }, 19 | selectors: { 20 | selectTodos: (state) => state.todoItems, 21 | }, 22 | }) 23 | 24 | export const todosReducer = todosSlice.reducer 25 | export const { selectTodos } = todosSlice.selectors 26 | export const { addTodo, initializeTodos } = todosSlice.actions 27 | -------------------------------------------------------------------------------- /examples/redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/redux/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | import { UserConfig } from 'vite' 4 | 5 | export default { 6 | plugins: [react(), vike()], 7 | } satisfies UserConfig 8 | -------------------------------------------------------------------------------- /examples/zustand/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /examples/zustand/.test-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run dev') 3 | -------------------------------------------------------------------------------- /examples/zustand/.test-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { testRun } from './.testRun' 2 | testRun('pnpm run preview') 3 | -------------------------------------------------------------------------------- /examples/zustand/README.md: -------------------------------------------------------------------------------- 1 | Example of using `vike-react-zustand`. 2 | 3 | ```bash 4 | git clone git@github.com:vikejs/vike-react 5 | cd vike-react/examples/zustand/ 6 | npm install 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/zustand/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | export { Counter } 2 | 3 | import React from 'react' 4 | import { useCounterStore } from '../store' 5 | 6 | function Counter() { 7 | const { counter, setCounter } = useCounterStore() 8 | 9 | return <button onClick={() => setCounter(counter + 1)}>Counter {counter}</button> 10 | } 11 | -------------------------------------------------------------------------------- /examples/zustand/layouts/HeadDefault.tsx: -------------------------------------------------------------------------------- 1 | export default HeadDefault 2 | 3 | import React from 'react' 4 | 5 | function HeadDefault() { 6 | return ( 7 | <> 8 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 9 | </> 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /examples/zustand/layouts/LayoutDefault.tsx: -------------------------------------------------------------------------------- 1 | export default LayoutDefault 2 | 3 | import './style.css' 4 | import React from 'react' 5 | import logoUrl from '../assets/logo.svg' 6 | 7 | function LayoutDefault({ children }: { children: React.ReactNode }) { 8 | return ( 9 | <div 10 | style={{ 11 | display: 'flex', 12 | maxWidth: 900, 13 | margin: 'auto', 14 | }} 15 | > 16 | <Sidebar> 17 | <Logo /> 18 | </Sidebar> 19 | <Content>{children}</Content> 20 | </div> 21 | ) 22 | } 23 | 24 | function Sidebar({ children }: { children: React.ReactNode }) { 25 | return ( 26 | <div 27 | id="sidebar" 28 | style={{ 29 | padding: 20, 30 | flexShrink: 0, 31 | display: 'flex', 32 | flexDirection: 'column', 33 | lineHeight: '1.8em', 34 | borderRight: '2px solid #eee', 35 | }} 36 | > 37 | {children} 38 | </div> 39 | ) 40 | } 41 | 42 | function Content({ children }: { children: React.ReactNode }) { 43 | return ( 44 | <div id="page-container"> 45 | <div 46 | id="page-content" 47 | style={{ 48 | padding: 20, 49 | paddingBottom: 50, 50 | minHeight: '100vh', 51 | }} 52 | > 53 | {children} 54 | </div> 55 | </div> 56 | ) 57 | } 58 | 59 | function Logo() { 60 | return ( 61 | <div 62 | style={{ 63 | marginTop: 20, 64 | marginBottom: 10, 65 | }} 66 | > 67 | <a href="/"> 68 | <img src={logoUrl} height={64} width={64} /> 69 | </a> 70 | </div> 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /examples/zustand/layouts/style.css: -------------------------------------------------------------------------------- 1 | /* Links */ 2 | a { 3 | text-decoration: none; 4 | } 5 | #sidebar a { 6 | padding: 2px 10px; 7 | margin-left: -10px; 8 | } 9 | #sidebar a.is-active { 10 | background-color: #eee; 11 | } 12 | 13 | /* Reset */ 14 | body { 15 | margin: 0; 16 | font-family: sans-serif; 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Page Transition Anmiation */ 23 | #page-content { 24 | opacity: 1; 25 | transition: opacity 0.3s ease-in-out; 26 | } 27 | body.page-is-transitioning #page-content { 28 | opacity: 0; 29 | } 30 | -------------------------------------------------------------------------------- /examples/zustand/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vike dev", 4 | "build": "vike build", 5 | "preview": "vike build && vike preview", 6 | "test": "tsc --noEmit" 7 | }, 8 | "dependencies": { 9 | "@types/react": "^19.0.10", 10 | "@types/react-dom": "^19.0.4", 11 | "@vitejs/plugin-react": "^4.3.4", 12 | "immer": "^10.0.3", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0", 15 | "typescript": "^5.8.3", 16 | "vike": "^0.4.223", 17 | "vike-react": "0.6.4", 18 | "vike-react-zustand": "0.1.0", 19 | "vite": "^6.2.5", 20 | "zustand": "^5.0.3" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /examples/zustand/pages/+Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Layout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 | <div id="page-content"> 6 | <nav> 7 | <a href="/">Welcome</a> <a href="/about">About</a> 8 | </nav> 9 | {children} 10 | </div> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/zustand/pages/+config.ts: -------------------------------------------------------------------------------- 1 | export { config } 2 | 3 | import type { Config } from 'vike/types' 4 | import vikeReact from 'vike-react/config' 5 | import vikeReactZustand from 'vike-react-zustand/config' 6 | 7 | // Default configs (can be overridden by pages) 8 | const config = { 9 | // <title> 10 | title: 'My Vike + React App', 11 | // https://vike.dev/stream 12 | stream: true, 13 | // https://vike.dev/ssr - this line can be removed since `true` is the default 14 | ssr: true, 15 | // https://vike.dev/extends 16 | extends: [vikeReact, vikeReactZustand], 17 | } satisfies Config 18 | -------------------------------------------------------------------------------- /examples/zustand/pages/_error/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | 5 | function Page({ is404, errorInfo }: { is404: boolean; errorInfo?: string }) { 6 | if (is404) { 7 | return ( 8 | <> 9 | <h1>404 Page Not Found</h1> 10 | <p>This page could not be found.</p> 11 | <p>{errorInfo}</p> 12 | </> 13 | ) 14 | } else { 15 | return ( 16 | <> 17 | <h1>500 Internal Server Error</h1> 18 | <p>Something went wrong.</p> 19 | </> 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/zustand/pages/about/+Page.tsx: -------------------------------------------------------------------------------- 1 | export default Page 2 | 3 | import React from 'react' 4 | import { Counter } from '../../components/Counter' 5 | 6 | function Page() { 7 | return ( 8 | <> 9 | <> 10 | <h1>About</h1> 11 | <p>The counter value is the same as on the Welcome page.</p> 12 | <Counter /> 13 | </> 14 | </> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/zustand/pages/index/+Page.tsx: -------------------------------------------------------------------------------- 1 | export { Page } 2 | 3 | import React from 'react' 4 | import { Counter } from '../../components/Counter' 5 | import { TodoList } from './TodoList' 6 | 7 | function Page() { 8 | return ( 9 | <> 10 | <h1>Welcome</h1> 11 | This page is: 12 | <ul> 13 | <li>Rendered to HTML.</li> 14 | <li> 15 | Interactive while loading. <Counter /> 16 | </li> 17 | </ul> 18 | <TodoList /> 19 | </> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/zustand/pages/index/+data.ts: -------------------------------------------------------------------------------- 1 | // Environment: server 2 | export { data } 3 | export type Data = Awaited<ReturnType<typeof data>> 4 | 5 | import type { PageContextServer } from 'vike/types' 6 | 7 | async function data(pageContext: PageContextServer) { 8 | const todoItemsInitial = await fetchTodosInit() 9 | return { todoItemsInitial } 10 | } 11 | 12 | // Pretending the list is fetched over the network 13 | async function fetchTodosInit() { 14 | return [ 15 | // 16 | { text: 'Buy apples' }, 17 | { text: `Update Node.js ${process.version} to latest version` }, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /examples/zustand/pages/index/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import React from 'react' 4 | import { useTodoStore } from '../../store' 5 | import { useStoreVanilla } from 'vike-react-zustand' 6 | 7 | export function TodoList() { 8 | const [newTodo, setNewTodo] = useState('') 9 | const { todoItems, addTodo } = useTodoStore() 10 | const storeVanilla = useStoreVanilla(useTodoStore) 11 | useEffect( 12 | () => 13 | storeVanilla.subscribe((state) => { 14 | console.log(JSON.stringify(state.todoItems)) 15 | }), 16 | [], 17 | ) 18 | 19 | return ( 20 | <> 21 | <h2>To-Do</h2> 22 | <ul id="todo-list"> 23 | {todoItems.map((todoItem, index) => ( 24 | // biome-ignore lint: 25 | <li key={index}>{todoItem.text}</li> 26 | ))} 27 | </ul> 28 | <div> 29 | <form 30 | onSubmit={async (ev) => { 31 | ev.preventDefault() 32 | addTodo({ text: newTodo }) 33 | setNewTodo('') 34 | }} 35 | > 36 | <input type="text" onChange={(ev) => setNewTodo(ev.target.value)} value={newTodo} /> 37 | <button type="submit">Add to-do</button> 38 | </form> 39 | </div> 40 | </> 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /examples/zustand/store.ts: -------------------------------------------------------------------------------- 1 | export { useCounterStore } 2 | export { useTodoStore } 3 | 4 | import { create, withPageContext } from 'vike-react-zustand' 5 | import { immer } from 'zustand/middleware/immer' 6 | import type { Data } from './pages/index/+data' 7 | 8 | interface CounterStore { 9 | counter: number 10 | setCounter: (value: number) => void 11 | } 12 | const useCounterStore = create<CounterStore>()( 13 | immer((set, get) => ({ 14 | setCounter(value) { 15 | set((state) => { 16 | state.counter = value 17 | }) 18 | }, 19 | counter: Math.floor(10000 * Math.random()), 20 | })), 21 | ) 22 | 23 | type Todo = { text: string } 24 | interface TodoStore { 25 | todoItems: Todo[] 26 | addTodo: (todo: Todo) => void 27 | } 28 | const useTodoStore = create<TodoStore>()( 29 | withPageContext((pageContext) => 30 | immer((set, get) => ({ 31 | todoItems: (pageContext.data as Data).todoItemsInitial, 32 | addTodo(todo) { 33 | set((state) => { 34 | state.todoItems.push(todo) 35 | }) 36 | }, 37 | })), 38 | ), 39 | ) 40 | -------------------------------------------------------------------------------- /examples/zustand/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/zustand/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import vike from 'vike/plugin' 3 | import type { UserConfig } from 'vite' 4 | 5 | export default { 6 | plugins: [react(), vike()], 7 | } satisfies UserConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "========= Build": "", 4 | "build": "pnpm --recursive --filter {packages/*} run build", 5 | "========= Dev": "", 6 | "dev": "cd ./packages/vike-react/ && pnpm run dev", 7 | "========= Test": "", 8 | "test": "pnpm run test:units && pnpm run test:e2e && pnpm run test:types", 9 | "test:e2e": "test-e2e", 10 | "test:units": "pnpm --recursive --sequential --filter {packages/*} run test", 11 | "test:types": "test-types", 12 | "========= Formatting": "", 13 | "format": "pnpm run format:biome", 14 | "format:prettier": "git ls-files | egrep '\\.(json|js|jsx|css|ts|tsx|vue|mjs|cjs)$' | grep --invert-match package.json | xargs pnpm exec prettier --write", 15 | "format:biome": "biome format --write .", 16 | "format:check": "biome format . || (echo 'Fix formatting by running `$ pnpm run -w format`.' && exit 1)", 17 | "========= Release": "", 18 | "release": "cd ./packages/vike-react/ && pnpm run release", 19 | "release:minor": "cd ./packages/vike-react/ && pnpm run release:minor", 20 | "release:commit": "cd ./packages/vike-react/ && pnpm run release:commit", 21 | "========= Reset": "", 22 | "reset": "git clean -Xdf && pnpm install && pnpm run build", 23 | "========= Only allow pnpm; forbid yarn & npm": "", 24 | "preinstall": "npx only-allow pnpm" 25 | }, 26 | "pnpm": { 27 | "overrides": { 28 | "vike-react": "link:./packages/vike-react/", 29 | "vike-react-query": "link:./packages/vike-react-query/", 30 | "vike-react-apollo": "link:./packages/vike-react-apollo/", 31 | "vike-react-chakra": "link:./packages/vike-react-chakra/", 32 | "vike-react-antd": "link:./packages/vike-react-antd/", 33 | "vike-react-styled-components": "link:./packages/vike-react-styled-components/", 34 | "vike-react-styled-jsx": "link:./packages/vike-react-styled-jsx/", 35 | "vike-react-redux": "link:./packages/vike-react-redux/", 36 | "vike-react-zustand": "link:./packages/vike-react-zustand" 37 | } 38 | }, 39 | "devDependencies": { 40 | "@biomejs/biome": "^1.8.3", 41 | "@brillout/test-e2e": "^0.6.10", 42 | "@brillout/test-types": "^0.1.15", 43 | "playwright": "^1.45.0", 44 | "prettier": "^3.2.5" 45 | }, 46 | "packageManager": "pnpm@9.4.0" 47 | } 48 | -------------------------------------------------------------------------------- /packages/vike-react-antd/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-antd/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2](https://github.com/vikejs/vike-react/compare/vike-react-antd@1.0.1...vike-react-antd@1.0.2) (2024-12-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * simplify condition checks ([d67da36](https://github.com/vikejs/vike-react/commit/d67da3646e13f0b4c7493ff8c193e50c6f5ca15e)) 7 | * stop overriding Wrapper meta ([ddcab7c](https://github.com/vikejs/vike-react/commit/ddcab7c21742b3701909b3698dca21b89c3dfebc)) ([f8452fc](https://github.com/vikejs/vike-react/commit/f8452fc1d3750693b5fd1556da64fceec930e21c)) 8 | 9 | 10 | 11 | ## [1.0.1](https://github.com/vikejs/vike-react/compare/vike-react-antd@1.0.0...vike-react-antd@1.0.1) (2024-12-23) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * move `pageContext.config.antd.cache` to `pageContext.antd.cache` ([a2a533d](https://github.com/vikejs/vike-react/commit/a2a533dab959897c213770d019942b46438a517c)) 17 | 18 | 19 | 20 | # 1.0.0 (2024-11-30) 21 | 22 | 23 | ### Features 24 | 25 | * new extension `vike-react-antd` ([636fb8a](https://github.com/vikejs/vike-react/commit/636fb8ad6abfadf485f647cb68ea7f77ede216cf)) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/vike-react-antd/README.md: -------------------------------------------------------------------------------- 1 | <!-- WARNING: keep links absolute in this file so they work on NPM too --> 2 | 3 | [![npm version](https://img.shields.io/npm/v/vike-react-antd)](https://www.npmjs.com/package/vike-react-antd) 4 | 5 | # `vike-react-antd` 6 | 7 | Integrates [Ant Design](https://ant.design) into your [`vike-react`](https://vike.dev/vike-react) app. 8 | 9 | [Installation](#installation) 10 | [Settings](#settings) 11 | [Version history](https://github.com/vikejs/vike-react/blob/main/packages/vike-react-antd/CHANGELOG.md) 12 | [What it does](#what-it-does) 13 | [See also](#see-also) 14 | 15 | <br/> 16 | 17 | ## Installation 18 | 19 | 1. `npm install vike-react-antd antd @ant-design/cssinjs` 20 | 2. Extend `+config.js`: 21 | ```js 22 | // pages/+config.js 23 | 24 | import vikeReact from "vike-react/config" 25 | import vikeReactAntd from "vike-react-antd/config" 26 | 27 | export default { 28 | // ... 29 | extends: [vikeReact, vikeReactAntd] 30 | } 31 | ``` 32 | 3. You can now use Ant Design in any of your components. 33 | ```jsx 34 | import { Button, Flex } from "antd"; 35 | 36 | function SomeComponent() { 37 | return ( 38 | <Flex gap="small" wrap> 39 | <Button type="primary">Primary Button</Button> 40 | <Button>Default Button</Button> 41 | </Flex> 42 | ) 43 | } 44 | ``` 45 | 46 | > [!NOTE] 47 | > The `vike-react-antd` extension requires [`vike-react`](https://vike.dev/vike-react). 48 | 49 | <br/> 50 | 51 | ## Settings 52 | 53 | `vike-react-antd` provides a configuration `+antd` for customizing Ant Design [Style Compatibility](https://ant.design/docs/react/compatible-style). 54 | 55 | ```ts 56 | // pages/+antd.ts 57 | export { antd } 58 | 59 | import { legacyLogicalPropertiesTransformer, px2remTransformer, type StyleProviderProps } from "@ant-design/cssinjs" 60 | 61 | const px2rem = px2remTransformer({ 62 | rootValue: 32, // 32px = 1rem; @default 16 63 | }) 64 | 65 | const antd: Omit<StyleProviderProps, "children" | "cache"> = { 66 | hashPriority: "high", 67 | layer: true, 68 | transformers: [legacyLogicalPropertiesTransformer, px2rem], 69 | } 70 | ``` 71 | 72 | You can remove the `vike-react-antd` integration from [some of your pages](https://vike.dev/config#inheritance): 73 | 74 | ```js 75 | // pages/about/+antd.js 76 | 77 | export const antd = null 78 | ``` 79 | 80 | For full customization consider [ejecting](https://vike.dev/eject). 81 | 82 | > [!NOTE] 83 | > Consider making a [Pull Request before ejecting](https://vike.dev/eject#when-to-eject). 84 | 85 | <br/> 86 | 87 | ## What it does 88 | 89 | The `vike-react-antd` extension allows you to use Ant Design without [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content). 90 | 91 | It collects the page's styles during SSR and injects them in the HTML, ensuring that styles are applied early (before even JavaScript starts loading). 92 | 93 | You can learn more at: 94 | - [Vike > CSS-in-JS > Collect styles](https://vike.dev/css-in-js#collect-styles) 95 | - [Antd Design > Server Side Rendering](https://ant.design/docs/react/server-side-rendering) 96 | 97 | For more details, have a look at the source code of `vike-react-styled-jsx` (which is small). 98 | 99 | <br/> 100 | 101 | ## See also 102 | 103 | - [Vike Docs > Ant Design](https://vike.dev/antd) 104 | - [Vike Docs > CSS-in-JS](https://vike.dev/css-in-js) 105 | - [Antd Design](https://ant.design) 106 | -------------------------------------------------------------------------------- /packages/vike-react-antd/Wrapper.server.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React, { type ReactNode } from 'react' 4 | import { StyleProvider } from '@ant-design/cssinjs' 5 | import { usePageContext } from 'vike-react/usePageContext' 6 | 7 | function Wrapper({ children }: { children: ReactNode }) { 8 | const pageContext = usePageContext() 9 | const { antd } = pageContext.config 10 | 11 | if (antd === null) { 12 | return <>{children}</> 13 | } 14 | 15 | return ( 16 | <StyleProvider cache={pageContext.antd!.cache} {...antd}> 17 | {children} 18 | </StyleProvider> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/vike-react-antd/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { StyleProviderProps } from '@ant-design/cssinjs' 4 | import type { Config } from 'vike/types' 5 | 6 | const config = { 7 | name: 'vike-react-antd', 8 | require: { 9 | vike: '>=0.4.211', 10 | 'vike-react': '>=0.4.13', 11 | }, 12 | onAfterRenderHtml: 'import:vike-react-antd/__internal/onAfterRenderHtml:onAfterRenderHtml', 13 | onBeforeRenderHtml: 'import:vike-react-antd/__internal/onBeforeRenderHtml:onBeforeRenderHtml', 14 | Wrapper: 'import:vike-react-antd/__internal/Wrapper:Wrapper', 15 | meta: { 16 | antd: { 17 | env: { 18 | server: true, 19 | client: false, 20 | }, 21 | }, 22 | }, 23 | } satisfies Config 24 | 25 | declare global { 26 | namespace Vike { 27 | interface PageContext { 28 | antd?: { 29 | cache?: StyleProviderProps['cache'] 30 | } 31 | } 32 | interface Config { 33 | antd?: null | Omit<StyleProviderProps, 'children' | 'cache'> 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/vike-react-antd/onAfterRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onAfterRenderHtml } 2 | 3 | import React from 'react' 4 | import { extractStyle } from '@ant-design/cssinjs' 5 | import { useConfig } from 'vike-react/useConfig' 6 | import type { PageContext } from 'vike/types' 7 | 8 | function onAfterRenderHtml(pageContext: PageContext) { 9 | const config = useConfig() 10 | const cache = pageContext.antd?.cache 11 | 12 | if (cache) { 13 | const styleTag = React.createElement('style', { 14 | id: 'antd-cssinjs', 15 | dangerouslySetInnerHTML: { 16 | __html: extractStyle(cache, true), 17 | }, 18 | }) 19 | config({ 20 | Head: styleTag, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/vike-react-antd/onBeforeRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onBeforeRenderHtml } 2 | 3 | import { createCache } from '@ant-design/cssinjs' 4 | import type { PageContext } from 'vike/types' 5 | 6 | function onBeforeRenderHtml(pageContext: PageContext) { 7 | if (pageContext.config.antd !== null) { 8 | pageContext.antd = { 9 | cache: createCache(), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/vike-react-antd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-antd", 3 | "version": "1.0.2", 4 | "type": "module", 5 | "exports": { 6 | "./config": "./dist/config.js", 7 | "./__internal/onAfterRenderHtml": "./dist/onAfterRenderHtml.js", 8 | "./__internal/onBeforeRenderHtml": "./dist/onBeforeRenderHtml.js", 9 | "./__internal/Wrapper": "./dist/Wrapper.server.js" 10 | }, 11 | "scripts": { 12 | "dev": "tsc --watch", 13 | "build": "rimraf dist/ && tsc", 14 | "release": "release-me patch", 15 | "release:minor": "release-me minor", 16 | "release:major": "release-me major", 17 | "release:commit": "release-me commit" 18 | }, 19 | "peerDependencies": { 20 | "@ant-design/cssinjs": ">=1.22", 21 | "antd": ">=5", 22 | "react": ">=18", 23 | "vike-react": ">=0.4.13" 24 | }, 25 | "devDependencies": { 26 | "@ant-design/cssinjs": "^1.22.1", 27 | "@brillout/release-me": "^0.4.2", 28 | "@types/react": "^19.0.10", 29 | "antd": "^5.22.5", 30 | "react": "^19.0.0", 31 | "rimraf": "^5.0.5", 32 | "typescript": "^5.8.3", 33 | "vike": "^0.4.230", 34 | "vike-react": "0.6.4", 35 | "vite": "^6.2.5" 36 | }, 37 | "typesVersions": { 38 | "*": { 39 | "config": [ 40 | "dist/config.d.ts" 41 | ], 42 | "__internal/onAfterRenderHtml": [ 43 | "dist/onAfterRenderHtml.d.ts" 44 | ], 45 | "__internal/onBeforeRenderHtml": [ 46 | "dist/onBeforeRenderHtml.d.ts" 47 | ], 48 | "__internal/Wrapper": [ 49 | "dist/Wrapper.server.d.ts" 50 | ] 51 | } 52 | }, 53 | "files": [ 54 | "dist" 55 | ], 56 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-antd", 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /packages/vike-react-antd/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "jsx": "react", 8 | "outDir": "./dist/", 9 | "skipLibCheck": true, 10 | "types": ["vike-react"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.1.2](https://github.com/vikejs/vike-react/compare/vike-react-apollo@0.1.1...vike-react-apollo@0.1.2) (2025-05-29) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * update +stream usage ([#175](https://github.com/vikejs/vike-react/issues/175)) ([7a3d1d6](https://github.com/vikejs/vike-react/commit/7a3d1d601f0ff2ff45409d92b3226f544eaf24c7)) 7 | 8 | 9 | 10 | ## [0.1.1](https://github.com/vikejs/vike-react/compare/vike-react-apollo@0.1.0...vike-react-apollo@0.1.1) (2024-08-05) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * withFallback bug ([aa51a93](https://github.com/vikejs/vike-react/commit/aa51a93d40cbd5fc04225a56d2be546b794c1fb2)) 16 | 17 | 18 | 19 | # 0.1.0 (2024-07-16) 20 | 21 | 22 | ### Features 23 | 24 | * add vike-react-apollo ([#134](https://github.com/vikejs/vike-react/issues/134)) ([95bc323](https://github.com/vikejs/vike-react/commit/95bc323c696091bae908c72e38a010f27eff22e0)) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-apollo", 3 | "version": "0.1.2", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.js", 7 | "exports": { 8 | ".": "./dist/index.js", 9 | "./config": "./dist/integration/+config.js", 10 | "./__internal/integration/Wrapper": "./dist/integration/Wrapper.js" 11 | }, 12 | "scripts": { 13 | "dev": "tsc --watch", 14 | "build": "rimraf dist/ && tsc", 15 | "release": "release-me patch", 16 | "release:minor": "release-me minor", 17 | "release:commit": "release-me commit" 18 | }, 19 | "peerDependencies": { 20 | "@apollo/client": ">=3.0.0", 21 | "graphql": ">=16.0.0", 22 | "@apollo/client-react-streaming": ">=0.11.0", 23 | "react": ">=18.0.0", 24 | "react-dom": ">=18.0.0", 25 | "react-streaming": ">=0.3.41", 26 | "vike-react": ">=0.6.4" 27 | }, 28 | "devDependencies": { 29 | "@brillout/release-me": "^0.4.2", 30 | "@apollo/client": "^3.10.8", 31 | "@apollo/client-react-streaming": "^0.11.2", 32 | "graphql": "^16.9.0", 33 | "@types/node": "^20.11.17", 34 | "@types/react": "^19.0.10", 35 | "react": "^19.0.0", 36 | "react-dom": "^19.0.0", 37 | "@types/react-dom": "^19.0.4", 38 | "react-streaming": "^0.4.2", 39 | "rimraf": "^5.0.5", 40 | "typescript": "^5.8.3", 41 | "vike": "^0.4.230", 42 | "vike-react": "0.6.4", 43 | "vite": "^6.2.5" 44 | }, 45 | "dependencies": { 46 | "react-error-boundary": "^4.0.12" 47 | }, 48 | "typesVersions": { 49 | "*": { 50 | "config": [ 51 | "dist/integration/+config.d.ts" 52 | ], 53 | "__internal/integration/Wrapper": [ 54 | "dist/integration/Wrapper.d.ts" 55 | ] 56 | } 57 | }, 58 | "files": [ 59 | "dist" 60 | ], 61 | "repository": "github:vikejs/vike-react", 62 | "license": "MIT" 63 | } 64 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/src/index.ts: -------------------------------------------------------------------------------- 1 | export { withFallback } from './withFallback.js' 2 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/src/integration/+config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { Config } from 'vike/types' 4 | import 'vike-react/config' // Needed for declaration merging of Config 5 | import type { ApolloClient } from '@apollo/client-react-streaming' 6 | 7 | const config = { 8 | name: 'vike-react-apollo', 9 | require: { 10 | 'vike-react': '>=0.6.4', 11 | }, 12 | Wrapper: 'import:vike-react-apollo/__internal/integration/Wrapper:Wrapper', 13 | stream: { require: true }, 14 | meta: { 15 | ApolloClient: { 16 | env: { 17 | server: true, 18 | client: true, 19 | }, 20 | }, 21 | }, 22 | } satisfies Config 23 | 24 | declare global { 25 | namespace Vike { 26 | interface Config { 27 | ApolloClient?: (pageContext: PageContext) => ApolloClient 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/src/integration/Transport.tsx: -------------------------------------------------------------------------------- 1 | import { WrapApolloProvider } from '@apollo/client-react-streaming' 2 | import { buildManualDataTransport } from '@apollo/client-react-streaming/manual-transport' 3 | import { useStream } from 'react-streaming' 4 | import { renderToString } from 'react-dom/server' 5 | import React from 'react' 6 | 7 | export const WrappedApolloProvider = WrapApolloProvider( 8 | buildManualDataTransport({ 9 | useInsertHtml() { 10 | const stream = useStream() 11 | if (!stream) { 12 | return () => {} 13 | } 14 | return (callback: () => React.ReactNode) => { 15 | stream.injectToStream( 16 | // https://github.com/apollographql/apollo-client-nextjs/issues/325 17 | (async () => renderToString(await callback()))(), 18 | ) 19 | } 20 | }, 21 | }), 22 | ) 23 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/src/integration/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React, { type ReactNode } from 'react' 4 | import { usePageContext } from 'vike-react/usePageContext' 5 | import { assertUsage } from '../utils/assert.js' 6 | import { WrappedApolloProvider } from './Transport.js' 7 | 8 | function Wrapper({ children }: { children: ReactNode }) { 9 | const pageContext = usePageContext() 10 | const { ApolloClient: getApolloClient } = pageContext.config 11 | assertUsage(getApolloClient, 'Setting +ApolloClient is required') 12 | return <WrappedApolloProvider makeClient={() => getApolloClient(pageContext)}>{children}</WrappedApolloProvider> 13 | } 14 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | export { assertUsage } 2 | 3 | function assertUsage(condition: unknown, message: string): asserts condition { 4 | if (condition) return 5 | throw new Error('Wrong usage: ' + message) 6 | } 7 | -------------------------------------------------------------------------------- /packages/vike-react-apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | // Resolution 5 | "target": "ES2020", 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | // Libs 9 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 10 | "types": ["vite/client"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true, 15 | // Output 16 | "declaration": true, 17 | "noEmitOnError": false, 18 | "rootDir": "./src/", 19 | // Misc 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "jsx": "react" 23 | }, 24 | "include": ["./src"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.1](https://github.com/vikejs/vike-react/compare/vike-react-chakra@1.0.0...vike-react-chakra@1.0.1) (2024-11-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * add eject.config.js ([0070495](https://github.com/vikejs/vike-react/commit/00704957fcf374ad0c7ebb0645a36b8d2035d2d2)) 7 | 8 | 9 | 10 | # 1.0.0 (2024-11-18) 11 | 12 | ### Features 13 | 14 | * new extension vike-react-chakra ([#151](https://github.com/vikejs/vike-react/issues/151)) ([0850332](https://github.com/vikejs/vike-react/pull/151/commits/0850332bcc570145b6d548fe1add73427788b0bd)) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/README.md: -------------------------------------------------------------------------------- 1 | <!-- WARNING: keep links absolute in this file so they work on NPM too --> 2 | 3 | [![npm version](https://img.shields.io/npm/v/vike-react-chakra)](https://www.npmjs.com/package/vike-react-chakra) 4 | 5 | # `vike-react-chakra` 6 | 7 | Integrates [Chakra UI](https://www.chakra-ui.com/) into your [`vike-react`](https://vike.dev/vike-react) app. 8 | 9 | [Installation](#installation) 10 | [Settings](#settings) 11 | [Version history](https://github.com/vikejs/vike-react/blob/main/packages/vike-react-chakra/CHANGELOG.md) 12 | [See also](#see-also) 13 | 14 | <br/> 15 | 16 | ## Installation 17 | 18 | 1. `npm install vike-react-chakra @chakra-ui/react @emotion/react` 19 | 2. Extend `+config.js`: 20 | ```js 21 | // pages/+config.js 22 | 23 | import vikeReact from 'vike-react/config' 24 | import vikeReactChakra from 'vike-react-chakra/config' 25 | 26 | export default { 27 | // ... 28 | extends: [vikeReact, vikeReactChakra] 29 | } 30 | ``` 31 | 3. You can now use Chakra in any of your components. 32 | ```jsx 33 | import { HStack, Button } from '@chakra-ui/react' 34 | 35 | function SomeComponent() { 36 | return ( 37 | <HStack> 38 | <Button>Click me</Button> 39 | <Button>Click me</Button> 40 | </HStack> 41 | ) 42 | } 43 | ``` 44 | 45 | > [!NOTE] 46 | > The `vike-react-chakra` extension requires [`vike-react`](https://vike.dev/vike-react). 47 | 48 | <br/> 49 | 50 | ## Settings 51 | 52 | `vike-react-chakra` provides a configuration `+chakra` for setting the theme system and locale. 53 | 54 | ```js 55 | // pages/+chakra.js 56 | 57 | export { chakra } 58 | 59 | import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react' 60 | 61 | const customConfig = defineConfig({ 62 | globalCss: { 63 | "html, body": { 64 | margin: 0, 65 | padding: 0 66 | } 67 | } 68 | }) 69 | 70 | const system = createSystem(defaultConfig, customConfig) 71 | 72 | const chakra = { 73 | system, 74 | locale: "fr-FR" 75 | } 76 | ``` 77 | 78 | You can remove Chakra from [some of your pages](https://vike.dev/config#inheritance): 79 | 80 | ```js 81 | // pages/about/+chakra.js 82 | 83 | export const chakra = null 84 | ``` 85 | 86 | For full customization consider [ejecting](https://vike.dev/eject). 87 | 88 | > [!NOTE] 89 | > Consider making a [Pull Request before ejecting](https://vike.dev/eject#when-to-eject). 90 | 91 | <br/> 92 | 93 | ## See also 94 | 95 | - [Vike Docs > Chakra UI](https://vike.dev/chakra) 96 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React, { type ReactNode } from 'react' 4 | import { ChakraProvider, LocaleProvider, defaultSystem } from '@chakra-ui/react' 5 | import { usePageContext } from 'vike-react/usePageContext' 6 | 7 | function Wrapper({ children }: { children: ReactNode }) { 8 | const pageContext = usePageContext() 9 | const { chakra } = pageContext.config 10 | 11 | if (chakra === null) { 12 | return <>{children}</> 13 | } 14 | 15 | return ( 16 | <ChakraProvider value={chakra?.system ?? defaultSystem}> 17 | <ChakraLocaleProvider locale={chakra?.locale}>{children}</ChakraLocaleProvider> 18 | </ChakraProvider> 19 | ) 20 | } 21 | 22 | function ChakraLocaleProvider({ locale, children }: { locale?: string; children: ReactNode }) { 23 | if (locale) { 24 | return <LocaleProvider locale={locale}>{children}</LocaleProvider> 25 | } 26 | return <>{children}</> 27 | } 28 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { LocaleProviderProps, SystemContext } from '@chakra-ui/react' 4 | import type { Config } from 'vike/types' 5 | 6 | const config = { 7 | name: 'vike-react-chakra', 8 | require: { 9 | vike: '>=0.4.203', 10 | 'vike-react': '>=0.4.13', 11 | }, 12 | Wrapper: 'import:vike-react-chakra/__internal/Wrapper:Wrapper', 13 | meta: { 14 | chakra: { 15 | env: { 16 | server: true, 17 | client: true, 18 | }, 19 | }, 20 | }, 21 | } satisfies Config 22 | 23 | declare global { 24 | namespace Vike { 25 | interface Config { 26 | chakra?: null | { 27 | system?: SystemContext 28 | locale?: LocaleProviderProps['locale'] 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/eject.config.js: -------------------------------------------------------------------------------- 1 | // This is work-in-progress, see: 2 | // - https://github.com/brillout/playground_eject-vike-react-chakra 3 | // - https://github.com/snake-py/eject/issues/4#issuecomment-2506217514 4 | export const config = { 5 | files: 'Wrapper.tsx', 6 | operations: ['mv Wrapper.tsx pages/+Wrapper.tsx'], 7 | } 8 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-chakra", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "exports": { 6 | "./config": "./dist/config.js", 7 | "./__internal/Wrapper": "./dist/Wrapper.js" 8 | }, 9 | "scripts": { 10 | "dev": "tsc --watch", 11 | "build": "rimraf dist/ && tsc", 12 | "release": "release-me patch", 13 | "release:minor": "release-me minor", 14 | "release:major": "release-me major", 15 | "release:commit": "release-me commit" 16 | }, 17 | "peerDependencies": { 18 | "@chakra-ui/react": ">=3", 19 | "@emotion/react": ">=11", 20 | "react": ">=18", 21 | "vike-react": ">=0.4.13" 22 | }, 23 | "devDependencies": { 24 | "@brillout/release-me": "^0.4.2", 25 | "@chakra-ui/react": "^3.0.2", 26 | "@emotion/react": "^11.13.3", 27 | "@types/react": "^19.0.10", 28 | "react": "^19.0.0", 29 | "rimraf": "^5.0.5", 30 | "typescript": "^5.8.3", 31 | "vike": "^0.4.230", 32 | "vike-react": "0.6.4", 33 | "vite": "^6.2.5" 34 | }, 35 | "typesVersions": { 36 | "*": { 37 | "config": [ 38 | "dist/config.d.ts" 39 | ], 40 | "__internal/Wrapper": [ 41 | "dist/Wrapper.d.ts" 42 | ] 43 | } 44 | }, 45 | "files": [ 46 | "dist" 47 | ], 48 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-chakra", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /packages/vike-react-chakra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "jsx": "react", 8 | "outDir": "./dist/", 9 | "skipLibCheck": true, 10 | "types": ["vike-react"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-query/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-query", 3 | "version": "0.1.4", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.js", 7 | "exports": { 8 | ".": "./dist/index.js", 9 | "./config": "./dist/integration/+config.js", 10 | "./__internal/integration/Wrapper": "./dist/integration/Wrapper.js", 11 | "./__internal/integration/FallbackErrorBoundary": "./dist/integration/FallbackErrorBoundary.js" 12 | }, 13 | "scripts": { 14 | "dev": "tsc --watch", 15 | "build": "rimraf dist/ && tsc", 16 | "release": "release-me patch", 17 | "release:minor": "release-me minor", 18 | "release:commit": "release-me commit", 19 | "test": "vitest run" 20 | }, 21 | "peerDependencies": { 22 | "@tanstack/react-query": ">=5.0.0", 23 | "react": ">=18.0.0", 24 | "react-streaming": ">=0.3.42", 25 | "vike-react": ">=0.6.4" 26 | }, 27 | "devDependencies": { 28 | "@brillout/release-me": "^0.4.2", 29 | "@tanstack/react-query": "^5.20.1", 30 | "@testing-library/react": "^14.2.1", 31 | "@types/node": "^20.11.17", 32 | "@types/react": "^19.0.10", 33 | "jsdom": "^24.0.0", 34 | "react": "^19.0.0", 35 | "react-streaming": "^0.4.2", 36 | "rimraf": "^5.0.5", 37 | "typescript": "^5.8.3", 38 | "vike": "^0.4.230", 39 | "vike-react": "0.6.4", 40 | "vite": "^6.2.5", 41 | "vitest": "^1.2.2" 42 | }, 43 | "dependencies": { 44 | "devalue": "^4.3.2", 45 | "react-error-boundary": "^4.0.12" 46 | }, 47 | "typesVersions": { 48 | "*": { 49 | "config": [ 50 | "dist/integration/+config.d.ts" 51 | ], 52 | "__internal/integration/Wrapper": [ 53 | "dist/integration/Wrapper.d.ts" 54 | ] 55 | } 56 | }, 57 | "files": [ 58 | "dist" 59 | ], 60 | "repository": "github:vikejs/vike-react", 61 | "license": "MIT" 62 | } 63 | -------------------------------------------------------------------------------- /packages/vike-react-query/src/index.ts: -------------------------------------------------------------------------------- 1 | export { withFallback } from './withFallback.js' 2 | -------------------------------------------------------------------------------- /packages/vike-react-query/src/integration/+config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { QueryClientConfig } from '@tanstack/react-query' 4 | import type { ReactNode } from 'react' 5 | import type { Config, ImportString } from 'vike/types' 6 | import 'vike-react/config' // Needed for declaration merging of Config 7 | 8 | const config = { 9 | name: 'vike-react-query', 10 | require: { 11 | 'vike-react': '>=0.6.4', 12 | }, 13 | queryClientConfig: undefined, 14 | Wrapper: 'import:vike-react-query/__internal/integration/Wrapper:Wrapper', 15 | FallbackErrorBoundary: 'import:vike-react-query/__internal/integration/FallbackErrorBoundary:FallbackErrorBoundary', 16 | stream: { require: true }, 17 | meta: { 18 | queryClientConfig: { 19 | env: { 20 | server: true, 21 | client: true, 22 | }, 23 | }, 24 | FallbackErrorBoundary: { 25 | env: { 26 | server: true, 27 | client: true, 28 | }, 29 | }, 30 | }, 31 | } satisfies Config 32 | 33 | declare global { 34 | namespace Vike { 35 | interface Config { 36 | queryClientConfig?: QueryClientConfig | ((pageContext: PageContext) => QueryClientConfig) 37 | FallbackErrorBoundary?: ((props: { children: ReactNode }) => ReactNode) | ImportString 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/vike-react-query/src/integration/FallbackErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | export { FallbackErrorBoundary } 2 | 3 | import { QueryErrorResetBoundary } from '@tanstack/react-query' 4 | import React, { CSSProperties, ReactElement } from 'react' 5 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary' 6 | 7 | function FallbackErrorBoundary({ children }: { children: ReactElement }) { 8 | /* TODO: either remove this or properly check whether env is DEV: 9 | * - Safe check against process.env.NODE_ENV for server-side 10 | * - Safe check against import.meta.env.DEV for client-side 11 | */ 12 | return (false as boolean) /*import.meta.env.DEV*/ ? ( 13 | <QueryErrorResetBoundary> 14 | {({ reset }) => ( 15 | <ErrorBoundary onReset={reset} FallbackComponent={Fallback}> 16 | {children} 17 | </ErrorBoundary> 18 | )} 19 | </QueryErrorResetBoundary> 20 | ) : ( 21 | children 22 | ) 23 | } 24 | 25 | function Fallback({ resetErrorBoundary, error }: FallbackProps) { 26 | return ( 27 | <div style={pageStyle}> 28 | <div style={textStyle}>There was an error.</div> 29 | <button style={buttonStyle} onClick={() => resetErrorBoundary()}> 30 | Try again 31 | </button> 32 | { 33 | /* TODO: either remove this or properly check whether env is DEV: 34 | * - Safe check against process.env.NODE_ENV for server-side 35 | * - Safe check against import.meta.env.DEV for client-side 36 | */ 37 | (false as boolean) /*import.meta.env.DEV*/ && <pre>{getErrorStack(error)}</pre> 38 | } 39 | </div> 40 | ) 41 | } 42 | 43 | function getErrorStack(error: unknown) { 44 | if (error && error instanceof Error) { 45 | return error.stack 46 | } 47 | 48 | return '' 49 | } 50 | 51 | const pageStyle: CSSProperties = { 52 | position: 'absolute', 53 | inset: 0, 54 | display: 'flex', 55 | flexDirection: 'column', 56 | alignItems: 'center', 57 | justifyContent: 'center', 58 | backgroundColor: '#ffe1e3', 59 | padding: 12, 60 | } 61 | 62 | const textStyle = { 63 | fontSize: 20, 64 | fontWeight: 500, 65 | color: '#f44250', 66 | } 67 | 68 | const buttonStyle: CSSProperties = { 69 | fontSize: 18, 70 | fontWeight: 500, 71 | marginTop: 16, 72 | outline: 0, 73 | border: 0, 74 | color: '#272727', 75 | boxShadow: '0px 1px 2px 0px #ffc5c5', 76 | backgroundColor: '#fff', 77 | padding: '8px 12px', 78 | borderRadius: 16, 79 | cursor: 'pointer', 80 | } 81 | -------------------------------------------------------------------------------- /packages/vike-react-query/src/integration/StreamedHydration.tsx: -------------------------------------------------------------------------------- 1 | export { StreamedHydration } 2 | 3 | import type { QueryClient } from '@tanstack/react-query' 4 | import { dehydrate, hydrate, DehydratedState } from '@tanstack/react-query' 5 | import { uneval } from 'devalue' 6 | import type { ReactNode } from 'react' 7 | import { useStream } from 'react-streaming' 8 | 9 | declare global { 10 | interface Window { 11 | _rqd_?: { push: (entry: DehydratedState) => void } | DehydratedState[] 12 | _rqc_?: () => void 13 | } 14 | } 15 | 16 | /** 17 | * This component is responsible for: 18 | * - dehydrating the query client on the server 19 | * - hydrating the query client on the client 20 | * - if react-streaming is not used, it doesn't do anything 21 | */ 22 | function StreamedHydration({ client, children }: { client: QueryClient; children: ReactNode }) { 23 | const stream = useStream() 24 | 25 | // stream is only avaiable in SSR 26 | const isSSR = !!stream 27 | 28 | if (isSSR) { 29 | stream.injectToStream( 30 | `<script class="_rqd_">_rqd_=[];_rqc_=()=>{Array.from( 31 | document.getElementsByClassName("_rqd_") 32 | ).forEach((e) => e.remove())};_rqc_()</script>`, 33 | ) 34 | client.getQueryCache().subscribe((event) => { 35 | if (['added', 'updated'].includes(event.type) && event.query.state.status === 'success') 36 | stream.injectToStream( 37 | `<script class="_rqd_">_rqd_.push(${uneval( 38 | dehydrate(client, { 39 | shouldDehydrateQuery: (query) => query.queryHash === event.query.queryHash, 40 | }), 41 | )});_rqc_()</script>`, 42 | ) 43 | }) 44 | } 45 | 46 | if (!isSSR && Array.isArray(window._rqd_)) { 47 | const onEntry = (entry: DehydratedState) => { 48 | hydrate(client, entry) 49 | } 50 | for (const entry of window._rqd_) { 51 | onEntry(entry) 52 | } 53 | window._rqd_ = { push: onEntry } 54 | } 55 | return children 56 | } 57 | -------------------------------------------------------------------------------- /packages/vike-react-query/src/integration/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import { QueryClient, QueryClientProvider, type QueryClientConfig } from '@tanstack/react-query' 4 | import React, { ReactNode, useState } from 'react' 5 | import { StreamedHydration } from './StreamedHydration.js' 6 | import { usePageContext } from 'vike-react/usePageContext' 7 | 8 | function Wrapper({ children }: { children: ReactNode }) { 9 | const pageContext = usePageContext() 10 | const { queryClientConfig, FallbackErrorBoundary = PassThrough } = pageContext.config 11 | const [queryClient] = useState(() => { 12 | const config = typeof queryClientConfig === 'function' ? queryClientConfig(pageContext) : queryClientConfig 13 | return getQueryClient(config) 14 | }) 15 | 16 | return ( 17 | <QueryClientProvider client={queryClient}> 18 | <FallbackErrorBoundary> 19 | <StreamedHydration client={queryClient}>{children}</StreamedHydration> 20 | </FallbackErrorBoundary> 21 | </QueryClientProvider> 22 | ) 23 | } 24 | 25 | function PassThrough({ children }: any) { 26 | return <>{children}</> 27 | } 28 | 29 | let clientQueryClient: QueryClient | undefined 30 | function getQueryClient(config: QueryClientConfig | undefined) { 31 | if (!isBrowser()) return new QueryClient(config) 32 | // React may throw away a partially rendered tree if it suspends, and then start again from scratch. 33 | // If it's no suspense boundary between the creation of queryClient and useSuspenseQuery, 34 | // then the entire tree is thrown away, including the creation of queryClient, which may produce infinity refetchs 35 | // https://github.com/TanStack/query/issues/6116#issuecomment-1904051005 36 | // https://github.com/vikejs/vike-react/pull/157 37 | if (!clientQueryClient) clientQueryClient = new QueryClient(config) 38 | return clientQueryClient 39 | } 40 | 41 | function isBrowser() { 42 | // Using `typeof window !== 'undefined'` alone is not enough because some users use https://www.npmjs.com/package/ssr-window 43 | return typeof window !== 'undefined' && typeof window.scrollY === 'number' 44 | // Alternatively, test whether environment is a *real* browser: https://github.com/brillout/picocolors/blob/d59a33a0fd52a8a33e4158884069192a89ce0113/picocolors.js#L87-L89 45 | } 46 | -------------------------------------------------------------------------------- /packages/vike-react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | // Resolution 5 | "target": "ES2020", 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | // Libs 9 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 10 | "types": ["vite/client"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true, 15 | // Output 16 | "declaration": true, 17 | "noEmitOnError": false, 18 | "rootDir": "./src/", 19 | // Misc 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "jsx": "react" 23 | }, 24 | "include": ["./src"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/vike-react-query/vitest.config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import { defineConfig } from 'vitest/config' 4 | 5 | const config = defineConfig({ 6 | test: { 7 | // test/**/*.test.ts => @brillout/test-e2e 8 | include: ['**/*.spec.*'], 9 | environment: 'jsdom', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/vike-react-redux/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-redux/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (2025-05-20) 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/vike-react-redux/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React from 'react' 4 | import { Provider } from 'react-redux' 5 | import { usePageContext } from 'vike-react/usePageContext' 6 | import type { Store } from '@reduxjs/toolkit' 7 | 8 | function Wrapper({ children }: { children: React.ReactNode }) { 9 | const pageContext = usePageContext() 10 | let store: undefined | Store 11 | if (pageContext.isClientSide) { 12 | store = pageContext.globalContext.store 13 | } else { 14 | store = pageContext.store 15 | } 16 | if (!store) return <>{children}</> 17 | return <Provider store={store}>{children}</Provider> 18 | } 19 | -------------------------------------------------------------------------------- /packages/vike-react-redux/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { Config } from 'vike/types' 4 | import type { Store } from '@reduxjs/toolkit' 5 | 6 | const config = { 7 | name: 'vike-react-redux', 8 | require: { 9 | vike: '>=0.4.230', 10 | 'vike-react': '>=0.6.3', 11 | }, 12 | 13 | passToClient: ['redux.ssrState'], 14 | 15 | meta: { 16 | redux: { 17 | env: { server: true, client: true }, 18 | global: true, 19 | }, 20 | }, 21 | 22 | onCreatePageContext: 'import:vike-react-redux/__internal/onCreatePageContext:onCreatePageContext', 23 | onAfterRenderHtml: 'import:vike-react-redux/__internal/onAfterRenderHtml:onAfterRenderHtml', 24 | onBeforeRenderClient: 'import:vike-react-redux/__internal/onBeforeRenderClient:onBeforeRenderClient', 25 | Wrapper: 'import:vike-react-redux/__internal/Wrapper:Wrapper', 26 | } satisfies Config 27 | 28 | declare global { 29 | namespace Vike { 30 | interface Config { 31 | redux?: { 32 | createStore: (pageContext: PageContext | GlobalContextClient) => Store 33 | } 34 | } 35 | interface PageContext { 36 | // vike-react-redux only defines pageContext.redux.store on the server-side, but thanks to https://github.com/vikejs/vike/pull/2459 the store is also avaiable at pageContext.redux.store on the client-side: on the client-side pageContext.redux.store falls back to globalContext.store 37 | store: Store 38 | redux?: { 39 | ssrState?: Record<string, unknown> 40 | } 41 | } 42 | interface GlobalContextClient { 43 | store: Store 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/vike-react-redux/onAfterRenderHtml.server.ts: -------------------------------------------------------------------------------- 1 | export { onAfterRenderHtml } 2 | 3 | import type { PageContextServer } from 'vike/types' 4 | 5 | function onAfterRenderHtml(pageContext: PageContextServer) { 6 | const configRedux = pageContext.config.redux 7 | if (!configRedux) return 8 | pageContext.redux ??= {} 9 | pageContext.redux.ssrState = pageContext.store.getState() 10 | } 11 | -------------------------------------------------------------------------------- /packages/vike-react-redux/onBeforeRenderClient.client.ts: -------------------------------------------------------------------------------- 1 | export { onBeforeRenderClient } 2 | 3 | import type { PageContextClient } from 'vike/types' 4 | 5 | function onBeforeRenderClient(pageContext: PageContextClient) { 6 | const configRedux = pageContext.config.redux 7 | if (!configRedux) return 8 | pageContext.globalContext.store ??= configRedux.createStore(pageContext) 9 | } 10 | -------------------------------------------------------------------------------- /packages/vike-react-redux/onCreatePageContext.server.ts: -------------------------------------------------------------------------------- 1 | export { onCreatePageContext } 2 | 3 | import type { PageContextServer } from 'vike/types' 4 | 5 | function onCreatePageContext(pageContext: PageContextServer) { 6 | const configRedux = pageContext.config.redux 7 | if (!configRedux) return 8 | pageContext.store = configRedux.createStore(pageContext) 9 | } 10 | -------------------------------------------------------------------------------- /packages/vike-react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-redux", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "exports": { 6 | "./config": "./dist/config.js", 7 | "./__internal/onCreatePageContext": "./dist/onCreatePageContext.server.js", 8 | "./__internal/onAfterRenderHtml": "./dist/onAfterRenderHtml.server.js", 9 | "./__internal/onBeforeRenderClient": "./dist/onBeforeRenderClient.client.js", 10 | "./__internal/Wrapper": "./dist/Wrapper.js" 11 | }, 12 | "scripts": { 13 | "dev": "tsc --watch", 14 | "build": "rimraf dist/ && tsc", 15 | "release": "release-me patch", 16 | "release:minor": "release-me minor", 17 | "release:major": "release-me major", 18 | "release:commit": "release-me commit" 19 | }, 20 | "peerDependencies": { 21 | "react-redux": ">=9", 22 | "react": ">=18", 23 | "vike": ">=0.4.230", 24 | "vike-react": ">=0.6.3" 25 | }, 26 | "devDependencies": { 27 | "@brillout/release-me": "^0.4.2", 28 | "@reduxjs/toolkit": "^2.6.1", 29 | "@types/react": "^19.0.10", 30 | "react": "^19.0.0", 31 | "rimraf": "^5.0.5", 32 | "typescript": "^5.8.3", 33 | "vike": "^0.4.230", 34 | "vike-react": "0.6.4" 35 | }, 36 | "typesVersions": { 37 | "*": { 38 | "config": [ 39 | "dist/config.d.ts" 40 | ], 41 | "__internal/onAfterRenderHtml": [ 42 | "dist/onAfterRenderHtml.d.ts" 43 | ], 44 | "__internal/onCreatePageContext": [ 45 | "dist/onCreatePageContext.server.d.ts" 46 | ], 47 | "__internal/onBeforeRenderClient": [ 48 | "dist/onBeforeRenderClient.d.ts" 49 | ], 50 | "__internal/Wrapper": [ 51 | "dist/Wrapper.d.ts" 52 | ] 53 | } 54 | }, 55 | "files": [ 56 | "dist" 57 | ], 58 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-redux", 59 | "license": "MIT" 60 | } 61 | -------------------------------------------------------------------------------- /packages/vike-react-redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "jsx": "react", 8 | "outDir": "./dist/", 9 | "skipLibCheck": true, 10 | "types": ["vike-react"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2](https://github.com/vikejs/vike-react/compare/vike-react-styled-components@1.0.1...vike-react-styled-components@1.0.2) (2024-12-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * simplify condition checks ([d67da36](https://github.com/vikejs/vike-react/commit/d67da3646e13f0b4c7493ff8c193e50c6f5ca15e)) 7 | * stop overriding Wrapper meta ([ddcab7c](https://github.com/vikejs/vike-react/commit/ddcab7c21742b3701909b3698dca21b89c3dfebc)) 8 | 9 | 10 | 11 | ## [1.0.1](https://github.com/vikejs/vike-react/compare/vike-react-styled-components@1.0.0...vike-react-styled-components@1.0.1) (2024-12-23) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * move `pageContext.styledComponentsSheet` to `pageContext.styledComponents.sheet` ([6fed80d](https://github.com/vikejs/vike-react/commit/6fed80dad3111f92d739a734c2a9e216747e1fa2)) 17 | 18 | 19 | 20 | # 1.0.0 (2024-12-03) 21 | 22 | 23 | ### Features 24 | 25 | * new extension `vike-react-styled-components` ([#156](https://github.com/vikejs/vike-react/issues/156)) ([d28cd8e](https://github.com/vikejs/vike-react/commit/d28cd8e366a6aa0b5bc861de8c818b9ff5f5cf66)) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/Wrapper.server.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React, { type ReactNode } from 'react' 4 | import { StyleSheetManager } from 'styled-components' 5 | import { usePageContext } from 'vike-react/usePageContext' 6 | 7 | function Wrapper({ children }: { children: ReactNode }) { 8 | const pageContext = usePageContext() 9 | const { styledComponents } = pageContext.config 10 | 11 | if (styledComponents === null) { 12 | return <>{children}</> 13 | } 14 | 15 | return ( 16 | <StyleSheetManager sheet={pageContext.styledComponents!.sheet?.instance} {...styledComponents?.styleSheetManager}> 17 | {children} 18 | </StyleSheetManager> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { IStyleSheetManager, ServerStyleSheet } from 'styled-components' 4 | import type { Config } from 'vike/types' 5 | 6 | const config = { 7 | name: 'vike-react-styled-components', 8 | require: { 9 | vike: '>=0.4.211', 10 | 'vike-react': '>=0.4.13', 11 | }, 12 | onAfterRenderHtml: 'import:vike-react-styled-components/__internal/onAfterRenderHtml:onAfterRenderHtml', 13 | onBeforeRenderHtml: 'import:vike-react-styled-components/__internal/onBeforeRenderHtml:onBeforeRenderHtml', 14 | Wrapper: 'import:vike-react-styled-components/__internal/Wrapper:Wrapper', 15 | meta: { 16 | styledComponents: { 17 | env: { 18 | server: true, 19 | client: false, 20 | }, 21 | }, 22 | }, 23 | } satisfies Config 24 | 25 | declare global { 26 | namespace Vike { 27 | interface PageContext { 28 | styledComponents?: { 29 | sheet?: ServerStyleSheet 30 | } 31 | } 32 | interface Config { 33 | styledComponents?: null | { 34 | styleSheetManager?: Omit<IStyleSheetManager, 'sheet' | 'children'> 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/onAfterRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onAfterRenderHtml } 2 | 3 | import React from 'react' 4 | import { useConfig } from 'vike-react/useConfig' 5 | import type { PageContext } from 'vike/types' 6 | 7 | function onAfterRenderHtml(pageContext: PageContext) { 8 | const config = useConfig() 9 | const sheet = pageContext.styledComponents?.sheet 10 | 11 | if (sheet) { 12 | try { 13 | const styles = sheet.getStyleElement() 14 | config({ 15 | Head: styles, 16 | }) 17 | } catch (error) { 18 | throw error 19 | } finally { 20 | sheet.seal() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/onBeforeRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onBeforeRenderHtml } 2 | 3 | import { ServerStyleSheet } from 'styled-components' 4 | import type { PageContext } from 'vike/types' 5 | 6 | function onBeforeRenderHtml(pageContext: PageContext) { 7 | if (pageContext.config.styledComponents !== null) { 8 | pageContext.styledComponents = { 9 | sheet: new ServerStyleSheet(), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-styled-components", 3 | "version": "1.0.2", 4 | "type": "module", 5 | "exports": { 6 | "./config": "./dist/config.js", 7 | "./__internal/onAfterRenderHtml": "./dist/onAfterRenderHtml.js", 8 | "./__internal/onBeforeRenderHtml": "./dist/onBeforeRenderHtml.js", 9 | "./__internal/Wrapper": "./dist/Wrapper.server.js" 10 | }, 11 | "scripts": { 12 | "dev": "tsc --watch", 13 | "build": "rimraf dist/ && tsc", 14 | "release": "release-me patch", 15 | "release:minor": "release-me minor", 16 | "release:major": "release-me major", 17 | "release:commit": "release-me commit" 18 | }, 19 | "peerDependencies": { 20 | "styled-components": ">=6", 21 | "react": ">=18", 22 | "vike-react": ">=0.4.13" 23 | }, 24 | "devDependencies": { 25 | "@brillout/release-me": "^0.4.2", 26 | "@types/react": "^19.0.10", 27 | "react": "^19.0.0", 28 | "rimraf": "^5.0.5", 29 | "styled-components": "^6.1.13", 30 | "typescript": "^5.8.3", 31 | "vike": "^0.4.230", 32 | "vike-react": "0.6.4", 33 | "vite": "^6.2.5" 34 | }, 35 | "typesVersions": { 36 | "*": { 37 | "config": [ 38 | "dist/config.d.ts" 39 | ], 40 | "__internal/onAfterRenderHtml": [ 41 | "dist/onAfterRenderHtml.d.ts" 42 | ], 43 | "__internal/onBeforeRenderHtml": [ 44 | "dist/onBeforeRenderHtml.d.ts" 45 | ], 46 | "__internal/Wrapper": [ 47 | "dist/Wrapper.server.d.ts" 48 | ] 49 | } 50 | }, 51 | "files": [ 52 | "dist" 53 | ], 54 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-styled-components", 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /packages/vike-react-styled-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "jsx": "react", 8 | "outDir": "./dist/", 9 | "skipLibCheck": true, 10 | "types": ["vike-react"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2](https://github.com/vikejs/vike-react/compare/vike-react-styled-jsx@1.0.1...vike-react-styled-jsx@1.0.2) (2024-12-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * simplify condition checks ([d67da36](https://github.com/vikejs/vike-react/commit/d67da3646e13f0b4c7493ff8c193e50c6f5ca15e)) 7 | * stop overriding Wrapper meta ([ddcab7c](https://github.com/vikejs/vike-react/commit/ddcab7c21742b3701909b3698dca21b89c3dfebc)) 8 | 9 | 10 | 11 | ## [1.0.1](https://github.com/vikejs/vike-react/compare/vike-react-styled-jsx@1.0.0...vike-react-styled-jsx@1.0.1) (2024-12-23) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * refactor multiple conditions into a single evaluation ([c4d5bb0](https://github.com/vikejs/vike-react/commit/c4d5bb02cecf068bc8ebe7499f1c4ebe852a6a58)) 17 | 18 | 19 | 20 | # 1.0.0 (2024-12-17) 21 | 22 | 23 | ### Features 24 | 25 | * new extension `vike-react-styled-jsx` ([#158](https://github.com/vikejs/vike-react/issues/158)) ([2f3dab2](https://github.com/vikejs/vike-react/commit/2f3dab29096577da468fe01b046ef0b2b4a1f8c7)) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/README.md: -------------------------------------------------------------------------------- 1 | <!-- WARNING: keep links absolute in this file so they work on NPM too --> 2 | 3 | [![npm version](https://img.shields.io/npm/v/vike-react-styled-jsx)](https://www.npmjs.com/package/vike-react-styled-jsx) 4 | 5 | # `vike-react-styled-jsx` 6 | 7 | Integrates [styled-jsx](https://github.com/vercel/styled-jsx) into your [`vike-react`](https://vike.dev/vike-react) app. 8 | 9 | [Installation](#installation) 10 | [Settings](#settings) 11 | [Version history](https://github.com/vikejs/vike-react/blob/main/packages/vike-react-styled-jsx/CHANGELOG.md) 12 | [What it does](#what-it-does) 13 | [See also](#see-also) 14 | 15 | <br/> 16 | 17 | ## Installation 18 | 19 | 1. `npm install vike-react-styled-jsx styled-jsx` 20 | 2. Extend `+config.js`: 21 | ```js 22 | // pages/+config.js 23 | 24 | import vikeReact from "vike-react/config" 25 | import vikeReactStyledJsx from "vike-react-styled-jsx/config" 26 | 27 | export default { 28 | // ... 29 | extends: [vikeReact, vikeReactStyledJsx] 30 | } 31 | ``` 32 | 33 | 3. Add `styled-jsx`'s Babel plugin: 34 | ```js 35 | // vite.config.js 36 | import react from "@vitejs/plugin-react" 37 | import vike from "vike/plugin" 38 | 39 | export default { 40 | plugins: [ 41 | vike(), 42 | react({ 43 | babel: { 44 | plugins: [["styled-jsx/babel"]] 45 | } 46 | }) 47 | ] 48 | } 49 | ``` 50 | 51 | 4. You can now use `styled-jsx` in any of your components. 52 | ```jsx 53 | function SomeComponent() { 54 | return ( 55 | <div> 56 | <p>Only this paragraph will get the style.</p> 57 | 58 | <style jsx>{` 59 | p { 60 | color: red; 61 | } 62 | `}</style> 63 | </div> 64 | ) 65 | } 66 | ``` 67 | 68 | > [!NOTE] 69 | > The `vike-react-styled-jsx` extension requires [`vike-react`](https://vike.dev/vike-react). 70 | 71 | <br/> 72 | 73 | ## Settings 74 | 75 | `vike-react-styled-jsx` provides a configuration `+styledJsx` to set the [CSP nonce for `styled-jsx`](https://github.com/vercel/styled-jsx#content-security-policy). 76 | 77 | > [!NOTE] 78 | > You need to set a `<meta property="csp-nonce" content={nonce} />` tag with the same nonce. 79 | 80 | ```ts 81 | // pages/+styledJsx.js 82 | export { styledJsx } 83 | 84 | import nanoid from 'nanoid' 85 | 86 | const styledJsx = { 87 | nonce: Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 88 | } 89 | ``` 90 | 91 | You can remove the `vike-react-styled-jsx` integration from [some of your pages](https://vike.dev/config#inheritance): 92 | 93 | ```js 94 | // pages/about/+styledJsx.js 95 | 96 | export const styledJsx = null 97 | ``` 98 | 99 | For full customization consider [ejecting](https://vike.dev/eject). 100 | 101 | > [!NOTE] 102 | > Consider making a [Pull Request before ejecting](https://vike.dev/eject#when-to-eject). 103 | 104 | <br/> 105 | 106 | ## What it does 107 | 108 | The `vike-react-styled-jsx` extension allows you to use `styled-jsx` without [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content). 109 | 110 | It collects the page's styles during SSR and injects them in the HTML, ensuring that styles are applied early (before even JavaScript starts loading). 111 | 112 | You can learn more at: 113 | - [Vike > CSS-in-JS > Collect styles](https://vike.dev/css-in-js#collect-styles) 114 | - [styled-jsx README > Server Side Rendering](https://github.com/vercel/styled-jsx#server-side-rendering) 115 | 116 | For more details, have a look at the source code of `vike-react-styled-jsx` (which is small). 117 | 118 | <br/> 119 | 120 | ## See also 121 | 122 | - [Vike Docs > styled-jsx](https://vike.dev/styled-jsx) 123 | - [Vike Docs > CSS-in-JS](https://vike.dev/css-in-js) 124 | - [styled-jsx README](https://github.com/vercel/styled-jsx#readme) 125 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/Wrapper.server.tsx: -------------------------------------------------------------------------------- 1 | export { Wrapper } 2 | 3 | import React, { type ReactNode } from 'react' 4 | import { StyleRegistry } from 'styled-jsx' 5 | import { usePageContext } from 'vike-react/usePageContext' 6 | 7 | function Wrapper({ children }: { children: ReactNode }) { 8 | const pageContext = usePageContext() 9 | const { styledJsx } = pageContext.config 10 | 11 | if (styledJsx === null) { 12 | return <>{children}</> 13 | } 14 | 15 | return <StyleRegistry registry={pageContext.styledJsx!.registry}>{children}</StyleRegistry> 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { Config } from 'vike/types' 4 | import type { StyledJsxStyleRegistry } from 'styled-jsx' 5 | 6 | const config = { 7 | name: 'vike-react-styled-jsx', 8 | require: { 9 | vike: '>=0.4.211', 10 | 'vike-react': '>=0.4.13', 11 | }, 12 | onBeforeRenderHtml: 'import:vike-react-styled-jsx/__internal/onBeforeRenderHtml:onBeforeRenderHtml', 13 | onAfterRenderHtml: 'import:vike-react-styled-jsx/__internal/onAfterRenderHtml:onAfterRenderHtml', 14 | Wrapper: 'import:vike-react-styled-jsx/__internal/Wrapper:Wrapper', 15 | meta: { 16 | styledJsx: { 17 | env: { server: true }, 18 | }, 19 | }, 20 | } satisfies Config 21 | 22 | declare global { 23 | namespace Vike { 24 | interface PageContext { 25 | styledJsx?: { 26 | registry?: StyledJsxStyleRegistry 27 | } 28 | } 29 | interface Config { 30 | styledJsx?: null | { 31 | nonce?: string 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/onAfterRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onAfterRenderHtml } 2 | 3 | import { useConfig } from 'vike-react/useConfig' 4 | import type { PageContext } from 'vike/types' 5 | 6 | function onAfterRenderHtml(pageContext: PageContext) { 7 | const config = useConfig() 8 | const registry = pageContext.styledJsx?.registry 9 | 10 | if (registry) { 11 | const nonce = pageContext.config.styledJsx?.nonce 12 | const styles = registry.styles({ nonce }) 13 | 14 | config({ 15 | Head: styles, 16 | }) 17 | 18 | registry.flush() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/onBeforeRenderHtml.ts: -------------------------------------------------------------------------------- 1 | export { onBeforeRenderHtml } 2 | 3 | import { createStyleRegistry } from 'styled-jsx' 4 | import type { PageContext } from 'vike/types' 5 | 6 | function onBeforeRenderHtml(pageContext: PageContext) { 7 | if (pageContext.config.styledJsx !== null) { 8 | pageContext.styledJsx = { 9 | registry: createStyleRegistry(), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-styled-jsx", 3 | "version": "1.0.2", 4 | "type": "module", 5 | "exports": { 6 | "./config": "./dist/config.js", 7 | "./__internal/onAfterRenderHtml": "./dist/onAfterRenderHtml.js", 8 | "./__internal/onBeforeRenderHtml": "./dist/onBeforeRenderHtml.js", 9 | "./__internal/Wrapper": "./dist/Wrapper.server.js" 10 | }, 11 | "scripts": { 12 | "dev": "tsc --watch", 13 | "build": "rimraf dist/ && tsc", 14 | "release": "release-me patch", 15 | "release:minor": "release-me minor", 16 | "release:major": "release-me major", 17 | "release:commit": "release-me commit" 18 | }, 19 | "peerDependencies": { 20 | "styled-jsx": ">=5", 21 | "react": ">=18", 22 | "vike-react": ">=0.4.13" 23 | }, 24 | "devDependencies": { 25 | "@brillout/release-me": "^0.4.2", 26 | "@types/react": "^19.0.10", 27 | "react": "^19.0.0", 28 | "rimraf": "^5.0.5", 29 | "styled-jsx": "^5.1.6", 30 | "typescript": "^5.8.3", 31 | "vike": "^0.4.230", 32 | "vike-react": "0.6.4", 33 | "vite": "^6.2.5" 34 | }, 35 | "typesVersions": { 36 | "*": { 37 | "config": [ 38 | "dist/config.d.ts" 39 | ], 40 | "__internal/onAfterRenderHtml": [ 41 | "dist/onAfterRenderHtml.d.ts" 42 | ], 43 | "__internal/onBeforeRenderHtml": [ 44 | "dist/onBeforeRenderHtml.d.ts" 45 | ], 46 | "__internal/Wrapper": [ 47 | "dist/Wrapper.server.d.ts" 48 | ] 49 | } 50 | }, 51 | "files": [ 52 | "dist" 53 | ], 54 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-styled-jsx", 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /packages/vike-react-styled-jsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "jsx": "react", 8 | "outDir": "./dist/", 9 | "skipLibCheck": true, 10 | "types": ["vike-react"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (2025-05-20) 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react-zustand", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "exports": { 8 | ".": "./dist/index.js", 9 | "./config": "./dist/integration/config.js" 10 | }, 11 | "scripts": { 12 | "dev": "tsc --watch", 13 | "build": "rimraf dist/ && tsc", 14 | "release": "release-me patch", 15 | "release:minor": "release-me minor", 16 | "release:commit": "release-me commit" 17 | }, 18 | "peerDependencies": { 19 | "react": ">=18.0.0", 20 | "react-dom": ">=18.0.0", 21 | "react-streaming": ">=0.3.42", 22 | "vike-react": ">=0.4.13", 23 | "zustand": ">=5.0.0" 24 | }, 25 | "devDependencies": { 26 | "@brillout/release-me": "^0.3.4", 27 | "@types/babel__core": "^7.20.5", 28 | "@types/node": "^20.11.17", 29 | "@types/react": "^19.0.10", 30 | "@types/react-dom": "^19.0.4", 31 | "react": "^19.0.0", 32 | "react-dom": "^19.0.0", 33 | "rimraf": "^5.0.5", 34 | "typescript": "^5.8.3", 35 | "vike": "^0.4.223", 36 | "vike-react": "0.6.4", 37 | "react-streaming": "^0.4.2", 38 | "vite": "^6.2.5", 39 | "zustand": "^5.0.3" 40 | }, 41 | "dependencies": { 42 | "@babel/core": "^7.24.0", 43 | "@babel/types": "^7.24.0", 44 | "@brillout/json-serializer": "^0.5.15" 45 | }, 46 | "typesVersions": { 47 | "*": { 48 | "config": [ 49 | "dist/integration/config.d.ts" 50 | ] 51 | } 52 | }, 53 | "files": [ 54 | "dist" 55 | ], 56 | "repository": "https://github.com/vikejs/vike-react/tree/main/packages/vike-react-zustand", 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/context.ts: -------------------------------------------------------------------------------- 1 | export { setPageContext } 2 | export { getPageContext } 3 | 4 | import type { PageContext } from 'vike/types' 5 | import { getGlobalObject } from './utils/getGlobalObject.js' 6 | 7 | const globalObject = getGlobalObject('context.ts', { 8 | pageContextCurrent: null as PageContext | null, 9 | }) 10 | 11 | function setPageContext(pageContext: PageContext | null) { 12 | globalObject.pageContextCurrent = pageContext 13 | } 14 | 15 | function getPageContext() { 16 | return globalObject.pageContextCurrent 17 | } 18 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/getOrCreateStore.ts: -------------------------------------------------------------------------------- 1 | export { getOrCreateStore } 2 | export type { CreateStoreReturn } 3 | 4 | import { parse } from '@brillout/json-serializer/parse' 5 | import { stringify } from '@brillout/json-serializer/stringify' 6 | import type { PageContext } from 'vike/types' 7 | import { create as createZustand, StateCreator } from 'zustand' 8 | import { setPageContext } from './context.js' 9 | import { assert } from './utils/assert.js' 10 | import { getGlobalObject } from './utils/getGlobalObject.js' 11 | import { sanitizeForSerialization } from './utils/sanitizeForSerialization.js' 12 | import { assignDeep } from './utils/assignDeep.js' 13 | 14 | // Client-side cache (not used in SSR) 15 | const clientCache = import.meta.env.SSR 16 | ? null 17 | : getGlobalObject('getOrCreateStore.ts', { 18 | initializers: {} as Record<string, StateCreator<any, [], []>>, 19 | stores: {} as Record<string, CreateStoreReturn<any>>, 20 | }) 21 | 22 | function getOrCreateStore<T>({ 23 | key, 24 | initializerFn, 25 | pageContext, 26 | stream, 27 | }: { 28 | key: string 29 | initializerFn: StateCreator<T, [], []> 30 | pageContext: PageContext 31 | stream: ReturnType<typeof import('react-streaming').useStreamOptional> 32 | }): CreateStoreReturn<T> { 33 | try { 34 | setPageContext(pageContext) 35 | if (import.meta.env.SSR) { 36 | pageContext._vikeReactZustandStoresServer ??= {} 37 | let store = pageContext._vikeReactZustandStoresServer[key] as CreateStoreReturn<T> 38 | if (store) return store 39 | store = createStore_(initializerFn) 40 | const serverState = store.getInitialState() 41 | const transferableState = sanitizeForSerialization(serverState) 42 | assert(stream) 43 | stream.injectToStream( 44 | `<script>if(!globalThis._vikeReactZustandState)globalThis._vikeReactZustandState={};globalThis._vikeReactZustandState['${key}']='${stringify(transferableState)}'</script>`, 45 | ) 46 | pageContext._vikeReactZustandStoresServer[key] = store 47 | return store 48 | } else { 49 | assert(clientCache) 50 | const storeNeedsRecreate = clientCache.initializers[key] !== initializerFn 51 | if (storeNeedsRecreate) { 52 | const store = createStore_(initializerFn) 53 | clientCache.stores[key] = store 54 | clientCache.initializers[key] = initializerFn 55 | assignServerStateOptional({ key, store }) 56 | return store 57 | } else { 58 | const store = clientCache.stores[key] 59 | assert(store) 60 | return store 61 | } 62 | } 63 | } finally { 64 | setPageContext(null) 65 | } 66 | } 67 | 68 | type CreateStoreReturn<T> = ReturnType<typeof createStore_<T>> 69 | function createStore_<T>(initializer: StateCreator<T, [], []>) { 70 | return createZustand<T>()(initializer) 71 | } 72 | 73 | declare global { 74 | var _vikeReactZustandState: undefined | Record<string, string> 75 | } 76 | function assignServerStateOptional<T>({ key, store }: { key: string; store: CreateStoreReturn<T> }) { 77 | if (globalThis._vikeReactZustandState && globalThis._vikeReactZustandState[key]) { 78 | const clientState = store.getInitialState() 79 | const serverState = parse(globalThis._vikeReactZustandState[key]) 80 | assert(clientState && typeof clientState === 'object') 81 | assert(serverState && typeof serverState === 'object') 82 | assignDeep(clientState, serverState) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/integration/config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'vike/types' 2 | import { vikeReactZustand } from '../plugin/index.js' 3 | 4 | export default { 5 | name: 'vike-react-zustand', 6 | require: { 7 | 'vike-react': '>=0.4.13', 8 | }, 9 | vite: { 10 | plugins: [vikeReactZustand()], 11 | }, 12 | } satisfies Config 13 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/integration/types.d.ts: -------------------------------------------------------------------------------- 1 | export type {} 2 | 3 | // The types we add here aren't visible to the user (because this file only matches the TypeScript rootDir of packages/vike-react-zustand/tsconfig.json) 4 | 5 | declare global { 6 | namespace Vike { 7 | interface PageContext { 8 | _vikeReactZustandStoresServer: { [key: string]: import('../getOrCreateStore.ts').CreateStoreReturn<any> } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export { vikeReactZustand } 2 | 3 | import type { Plugin } from 'vite' 4 | import { transformCode } from './babelTransformer.js' 5 | 6 | function vikeReactZustand(): Plugin[] { 7 | return [ 8 | { 9 | name: 'vike-react-zustand:config', 10 | configEnvironment() { 11 | return { 12 | resolve: { 13 | noExternal: ['vike-react-zustand'], 14 | }, 15 | } 16 | }, 17 | }, 18 | { 19 | name: 'vike-react-zustand:transform', 20 | enforce: 'post', 21 | transform(code, id) { 22 | if (id.includes('node_modules') || !/[jt]sx?$/.test(id)) { 23 | return 24 | } 25 | return transformCode(code, id) 26 | }, 27 | }, 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/types.ts: -------------------------------------------------------------------------------- 1 | export type { StoreVanillaAndHook, StoreVanilla, StoreHookOnly, Create } 2 | 3 | import type { StateCreator, StoreApi, StoreMutatorIdentifier } from 'zustand' 4 | 5 | /** 6 | * The store hook function that is returned by createWrapped 7 | */ 8 | type StoreHookOnly<T> = { 9 | (): T 10 | <U>(selector: (state: T) => U): U 11 | } 12 | 13 | /** 14 | * Just the store API without the hook functionality 15 | */ 16 | type StoreVanilla<T> = StoreApi<T> 17 | 18 | /** 19 | * Combined type used in the React context 20 | */ 21 | type StoreVanillaAndHook<T = any> = StoreVanilla<T> & { 22 | (): any 23 | <U>(selector: (state: any) => U): U 24 | } 25 | 26 | /** 27 | * The create function type with support for the key parameter 28 | */ 29 | type Create = { 30 | // Direct call with initializer 31 | <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, [], Mos>): StoreHookOnly<T> 32 | 33 | // Direct call with key and initializer 34 | <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>( 35 | key: string, 36 | initializer: StateCreator<T, [], Mos>, 37 | ): StoreHookOnly<T> 38 | 39 | // Curried call with no arguments 40 | <T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>( 41 | initializer: StateCreator<T, [], Mos>, 42 | ) => StoreHookOnly<T> 43 | 44 | // Curried call with key 45 | <T>( 46 | key: string, 47 | ): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, [], Mos>) => StoreHookOnly<T> 48 | } 49 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | export { assert, assertUsage } 2 | 3 | function assert(condition: unknown): asserts condition { 4 | if (condition) return 5 | throw new Error( 6 | "You stumbled upon a bug in vike-react-zustand's source code. Reach out on GitHub and we will fix the bug.", 7 | ) 8 | } 9 | 10 | function assertUsage(condition: unknown, message: string): asserts condition { 11 | if (condition) return 12 | throw new Error('Wrong usage: ' + message) 13 | } 14 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/utils/assignDeep.ts: -------------------------------------------------------------------------------- 1 | // Credits: https://github.com/radashi-org/radashi/blob/main/src/object/assign.ts 2 | 3 | export { assignDeep } 4 | 5 | function assignDeep(initial: Record<keyof any, any>, override: Record<keyof any, any>) { 6 | if (!initial || !override) { 7 | return initial ?? override ?? {} 8 | } 9 | for (const key of Object.keys(override)) { 10 | initial[key] = 11 | isPlainObject(initial[key]) && isPlainObject(override[key]) 12 | ? assignDeep(initial[key], override[key]) 13 | : override[key] 14 | } 15 | return initial 16 | } 17 | 18 | function isPlainObject(value: any): value is object { 19 | if (typeof value !== 'object' || value === null) { 20 | return false 21 | } 22 | 23 | const prototype = Object.getPrototypeOf(value) 24 | return ( 25 | // Fast path for most common objects. 26 | prototype === Object.prototype || 27 | // Support objects created without a prototype. 28 | prototype === null || 29 | // Support plain objects from other realms. 30 | Object.getPrototypeOf(prototype) === null 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/utils/getGlobalObject.ts: -------------------------------------------------------------------------------- 1 | export function getGlobalObject<T extends Record<string, unknown> = never>( 2 | // We use the filename as key; each `getGlobalObject()` call should live in a unique filename. 3 | key: `${string}.ts`, 4 | defaultValue: T, 5 | ): T { 6 | const allGlobalObjects = (globalThis.__vike_react_zustand = globalThis.__vike_react_zustand || {}) 7 | const globalObject = (allGlobalObjects[key] = (allGlobalObjects[key] as T) || defaultValue) 8 | return globalObject 9 | } 10 | declare global { 11 | var __vike_react_zustand: undefined | Record<string, Record<string, unknown>> 12 | } 13 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/utils/sanitizeForSerialization.ts: -------------------------------------------------------------------------------- 1 | export { sanitizeForSerialization } 2 | 3 | /** 4 | * Sanitizes data for serialization by removing functions, promises, and undefined values. 5 | * Creates a deep copy of the input with all non-serializable values removed. 6 | */ 7 | function sanitizeForSerialization<T>(input: T, visited = new WeakSet<object>()): T | undefined { 8 | if (typeof input !== 'object' || input === null) { 9 | return input 10 | } 11 | if (visited.has(input)) { 12 | return input 13 | } 14 | visited.add(input) 15 | if (Array.isArray(input)) { 16 | const output = [] 17 | for (const value of input) { 18 | if (include(value)) { 19 | const ret = sanitizeForSerialization(value, visited) 20 | if (include(ret)) { 21 | output.push(ret) 22 | } else { 23 | // Skip the whole array, we can't skip one in the middle of an ordered array 24 | return undefined 25 | } 26 | } else { 27 | return undefined 28 | } 29 | } 30 | return output as T 31 | } 32 | if (input instanceof Map) { 33 | const output = new Map() 34 | for (const [key, value] of input.entries()) { 35 | if (include(value)) { 36 | const ret = sanitizeForSerialization(value, visited) 37 | if (include(ret)) { 38 | output.set(key, ret) 39 | } 40 | } 41 | } 42 | return output as T 43 | } 44 | if (input instanceof Set) { 45 | const output = new Set() 46 | for (const value of input.values()) { 47 | if (include(value)) { 48 | const ret = sanitizeForSerialization(value, visited) 49 | if (include(ret)) { 50 | output.add(ret) 51 | } 52 | } 53 | } 54 | return output as T 55 | } 56 | const output: { [key: string]: any } = {} 57 | for (const key in input) { 58 | if (Object.prototype.hasOwnProperty.call(input, key)) { 59 | const value = input[key] 60 | if (include(value)) { 61 | const ret = sanitizeForSerialization(value, visited) 62 | if (include(ret)) { 63 | output[key] = ret 64 | } 65 | } 66 | } 67 | } 68 | return output as T 69 | } 70 | /** 71 | * Determines if a value should be included in the sanitized output. 72 | * Excludes functions, promises, undefined, and empty objects/collections. 73 | */ 74 | function include(value: unknown): boolean { 75 | if (isPromiseLike(value) || typeof value === 'function' || value === undefined) { 76 | return false 77 | } 78 | 79 | if (value instanceof Map || value instanceof Set) { 80 | return value.size > 0 81 | } 82 | 83 | if (typeof value === 'object' && value !== null) { 84 | return Object.keys(value).length > 0 85 | } 86 | 87 | return true 88 | } 89 | 90 | /** 91 | * Checks if a value is promise-like (has then and catch methods). 92 | */ 93 | function isPromiseLike(value: unknown): boolean { 94 | return Boolean( 95 | value && 96 | typeof value === 'object' && 97 | typeof (value as any).then === 'function' && 98 | typeof (value as any).catch === 'function', 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/src/withPageContext.ts: -------------------------------------------------------------------------------- 1 | export { withPageContext } 2 | 3 | import type { PageContext } from 'vike/types' 4 | import type { StateCreator, StoreMutatorIdentifier } from 'zustand' 5 | import { getPageContext } from './context.js' 6 | import { assert } from './utils/assert.js' 7 | 8 | type WithPageContext = < 9 | T, 10 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 11 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 12 | >( 13 | f: (pageContext: PageContext) => StateCreator<T, Mps, Mcs>, 14 | ) => StateCreator<T, Mps, Mcs> 15 | 16 | /** 17 | * Middleware to make `pageContext` available to the store during initialization. 18 | * 19 | * Example usage: 20 | * 21 | * ```ts 22 | * interface Store { 23 | * user: { 24 | * id: number 25 | * firstName: string 26 | * } 27 | * } 28 | * 29 | * const useStore = create<Store>()( 30 | * withPageContext((pageContext) => (set, get, store) => ({ 31 | * user: pageContext.user 32 | * })) 33 | * ) 34 | * ``` 35 | * 36 | * https://github.com/vikejs/vike-react/tree/main/packages/vike-react-zustand 37 | */ 38 | const withPageContext: WithPageContext = (fn) => (set, get, store) => { 39 | const pageContext = getPageContext() 40 | assert(pageContext) 41 | return fn(pageContext)(set, get, store) 42 | } 43 | -------------------------------------------------------------------------------- /packages/vike-react-zustand/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "rootDir": "./src/", 5 | // Resolution 6 | "target": "ES2020", 7 | "module": "Node16", 8 | "moduleResolution": "Node16", 9 | // Libs 10 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 11 | "types": ["vite/client"], 12 | // Strictness 13 | "strict": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noImplicitAny": true, 16 | // Output 17 | "declaration": true, 18 | "noEmitOnError": false, 19 | // Misc 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "jsx": "react" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vike-react/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /packages/vike-react/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Building 4 | 5 | Build `vike-react`: 6 | 7 | ```bash 8 | git clone git@github.com:vikejs/vike-react 9 | cd vike-react/ 10 | pnpm install 11 | pnpm build 12 | ``` 13 | 14 | > Note that you'll need [pnpm](https://pnpm.io/) for development, which you can install with `$ npm install -g pnpm`. 15 | > 16 | > However pnpm isn't needed for simply making use of the npm package `vike-react`: any package manager will do. 17 | 18 | ## Validating 19 | 20 | ### Running the examples 21 | 22 | You can then test your modifications against an example, e.g. `examples/full/`: 23 | 24 | ```bash 25 | cd examples/full/ 26 | pnpm dev 27 | cd ../../ 28 | ``` 29 | 30 | ## Releasing 31 | 32 | In order to release the next patch version (`MAJOR.MINOR.PATCH`, see [Semantic Versioning](https://semver.org/)), run: 33 | 34 | ```bash 35 | cd packages/vike-react/ 36 | pnpm release 37 | cd ../../ 38 | ``` 39 | 40 | This will: 41 | 42 | - update the version number and dependencies in `package.json`, 43 | - extend [`CHANGELOG.md`](CHANGELOG.md), 44 | - update the `pnpm-lock.yaml` file, 45 | - create a `release: vike-react@1.2.3` git commit and push it, 46 | - create a `vike-react@1.2.3` git tag and push it, 47 | - build the package and publish it to npm. 48 | 49 | Extend [`CHANGELOG.md`](CHANGELOG.md) if anything is missing, as the release script: 50 | 51 | - only picks up commits 52 | [that match the pattern of "Feature", "Fix", "Performance Improvement" or "Breaking Changes"](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-cli), 53 | - mistakenly picks up commits touching all packages in this monorepo, not only `vike-react`. 54 | -------------------------------------------------------------------------------- /packages/vike-react/README.md: -------------------------------------------------------------------------------- 1 | <!-- WARNING: keep links absolute in this file so they work on NPM too --> 2 | 3 | [![npm version](https://img.shields.io/npm/v/vike-react)](https://www.npmjs.com/package/vike-react) 4 | 5 | See [Vike Docs > vike-react](https://vike.dev/vike-react). 6 | -------------------------------------------------------------------------------- /packages/vike-react/eject.config.js: -------------------------------------------------------------------------------- 1 | // This is work-in-progress, see: 2 | // - https://github.com/brillout/playground_eject-vike-react 3 | // - https://github.com/snake-py/eject/issues/4#issuecomment-2506217514 4 | export const config = { 5 | files: 'src/*', 6 | operations: [ 7 | 'mv src/* .', 8 | 'mv integration renderer', 9 | 'mv config.ts renderer/+config.ts', 10 | 'mv renderer/onRenderHtml.tsx renderer/+onRenderHtml.tsx', 11 | 'mv renderer/onRenderClient.tsx renderer/+onRenderClient.tsx', 12 | 'mv renderer/Loading.tsx renderer/+Loading.tsx', 13 | 'rm index.ts', 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /packages/vike-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vike-react", 3 | "version": "0.6.4", 4 | "repository": "https://github.com/vikejs/vike-react", 5 | "type": "module", 6 | "exports": { 7 | "./usePageContext": "./dist/hooks/usePageContext.js", 8 | "./useData": "./dist/hooks/useData.js", 9 | "./useConfig": { 10 | "browser": "./dist/hooks/useConfig/useConfig-client.js", 11 | "default": "./dist/hooks/useConfig/useConfig-server.js" 12 | }, 13 | "./Config": { 14 | "browser": "./dist/components/Config/Config-client.js", 15 | "default": "./dist/components/Config/Config-server.js" 16 | }, 17 | "./Head": { 18 | "browser": "./dist/components/Head/Head-client.js", 19 | "default": "./dist/components/Head/Head-server.js" 20 | }, 21 | "./clientOnly": "./dist/helpers/clientOnly.js", 22 | "./ClientOnly": "./dist/components/ClientOnly.js", 23 | ".": "./dist/index.js", 24 | "./config": "./dist/config.js", 25 | "./__internal/integration/onRenderHtml": "./dist/integration/onRenderHtml.js", 26 | "./__internal/integration/onRenderClient": "./dist/integration/onRenderClient.js", 27 | "./__internal/integration/Loading": "./dist/integration/Loading.js" 28 | }, 29 | "dependencies": { 30 | "react-streaming": "^0.4.2" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=19", 34 | "react-dom": ">=19", 35 | "vike": ">=0.4.182" 36 | }, 37 | "scripts": { 38 | "dev": "tsc --watch", 39 | "build": "rimraf dist/ && tsc && pnpm run build:css", 40 | "build:css": "cp src/integration/Loading.css dist/integration/Loading.css", 41 | "release": "release-me patch", 42 | "release:minor": "release-me minor", 43 | "release:commit": "release-me commit" 44 | }, 45 | "devDependencies": { 46 | "@biomejs/biome": "^1.6.4", 47 | "@brillout/release-me": "^0.4.2", 48 | "@types/node": "^20.11.17", 49 | "@types/react": "^19.0.10", 50 | "@types/react-dom": "^19.0.4", 51 | "react": "^19.0.0", 52 | "react-dom": "^19.0.0", 53 | "rimraf": "^5.0.5", 54 | "typescript": "^5.8.3", 55 | "vike": "^0.4.230", 56 | "vite": "^6.2.5" 57 | }, 58 | "typesVersions": { 59 | "*": { 60 | "useData": [ 61 | "./dist/hooks/useData.d.ts" 62 | ], 63 | "usePageContext": [ 64 | "./dist/hooks/usePageContext.d.ts" 65 | ], 66 | "useConfig": [ 67 | "./dist/hooks/useConfig/useConfig-server.d.ts" 68 | ], 69 | "Config": [ 70 | "./dist/components/Config/Config-server.d.ts" 71 | ], 72 | "Head": [ 73 | "./dist/components/Head/Head-server.d.ts" 74 | ], 75 | "ClientOnly": [ 76 | "./dist/components/ClientOnly.d.ts" 77 | ], 78 | "clientOnly": [ 79 | "./dist/helpers/clientOnly.d.ts" 80 | ], 81 | ".": [ 82 | "./dist/index.d.ts" 83 | ], 84 | "config": [ 85 | "./dist/config.d.ts" 86 | ], 87 | "__/internal/integration/onRenderHtml": [ 88 | "./dist/integration/onRenderHtml.d.ts" 89 | ], 90 | "__/internal/integration/onRenderClient": [ 91 | "./dist/integration/onRenderClient.d.ts" 92 | ] 93 | } 94 | }, 95 | "main": "./dist/index.js", 96 | "types": "./dist/index.d.ts", 97 | "files": [ 98 | "dist/" 99 | ], 100 | "license": "MIT", 101 | "keywords": [ 102 | "react" 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /packages/vike-react/src/components/ClientOnly.tsx: -------------------------------------------------------------------------------- 1 | export { ClientOnly } 2 | 3 | import React, { lazy, useEffect, useState, startTransition } from 'react' 4 | import type { ComponentType, ReactNode } from 'react' 5 | 6 | function ClientOnly<T>({ 7 | load, 8 | children, 9 | fallback, 10 | deps = [], 11 | }: { 12 | load: () => Promise<{ default: React.ComponentType<T> } | React.ComponentType<T>> 13 | children: (Component: React.ComponentType<T>) => ReactNode 14 | fallback: ReactNode 15 | deps?: Parameters<typeof useEffect>[1] 16 | }) { 17 | // TODO/next-major: remove this file/export 18 | console.warn('[vike-react][warning] <ClientOnly> is deprecated: use clientOnly() instead https://vike.dev/clientOnly') 19 | 20 | const [Component, setComponent] = useState<ComponentType<unknown> | null>(null) 21 | 22 | useEffect(() => { 23 | const loadComponent = () => { 24 | const Component = lazy(() => 25 | load() 26 | .then((LoadedComponent) => { 27 | return { 28 | default: () => children('default' in LoadedComponent ? LoadedComponent.default : LoadedComponent), 29 | } 30 | }) 31 | .catch((error) => { 32 | console.error('Component loading failed:', error) 33 | return { default: () => <p>Error loading component.</p> } 34 | }), 35 | ) 36 | setComponent(Component) 37 | } 38 | 39 | startTransition(() => { 40 | loadComponent() 41 | }) 42 | }, deps) 43 | 44 | return Component ? <Component /> : fallback 45 | } 46 | -------------------------------------------------------------------------------- /packages/vike-react/src/components/Config/Config-client.ts: -------------------------------------------------------------------------------- 1 | export { Config } 2 | 3 | import { useConfig } from '../../hooks/useConfig/useConfig-client.js' 4 | import type { ConfigFromHook } from '../../types/Config.js' 5 | 6 | function Config(props: ConfigFromHook): null { 7 | const config = useConfig() 8 | config(props) 9 | return null 10 | } 11 | -------------------------------------------------------------------------------- /packages/vike-react/src/components/Config/Config-server.ts: -------------------------------------------------------------------------------- 1 | export { Config } 2 | 3 | import { useConfig } from '../../hooks/useConfig/useConfig-server.js' 4 | import type { ConfigFromHook } from '../../types/Config.js' 5 | 6 | /** 7 | * Set configurations inside React components. 8 | * 9 | * https://vike.dev/useConfig 10 | */ 11 | function Config(props: ConfigFromHook): null { 12 | const config = useConfig() 13 | config(props) 14 | return null 15 | } 16 | -------------------------------------------------------------------------------- /packages/vike-react/src/components/Head/Head-client.ts: -------------------------------------------------------------------------------- 1 | // https://vike.dev/Head#only-html 2 | export function Head(): null { 3 | return null 4 | } 5 | -------------------------------------------------------------------------------- /packages/vike-react/src/components/Head/Head-server.ts: -------------------------------------------------------------------------------- 1 | export { Head } 2 | 3 | import { useConfig } from '../../hooks/useConfig/useConfig-server.js' 4 | import type React from 'react' 5 | 6 | /** 7 | * Add arbitrary `<head>` tags. 8 | * 9 | * (The children are teleported to `<head>`.) 10 | * 11 | * https://vike.dev/Head 12 | */ 13 | function Head({ children }: { children: React.ReactNode }): null { 14 | const config = useConfig() 15 | config({ Head: children }) 16 | return null 17 | } 18 | -------------------------------------------------------------------------------- /packages/vike-react/src/config.ts: -------------------------------------------------------------------------------- 1 | export { config as default } 2 | 3 | import type { Config } from 'vike/types' 4 | import { ssrEffect } from './integration/ssrEffect.js' 5 | import { isNotFalse } from './utils/isNotFalse.js' 6 | 7 | const config = { 8 | // @eject-remove start 9 | name: 'vike-react', 10 | require: { 11 | vike: '>=0.4.182', 12 | }, 13 | 14 | Loading: 'import:vike-react/__internal/integration/Loading:default', 15 | 16 | // https://vike.dev/onRenderHtml 17 | onRenderHtml: 'import:vike-react/__internal/integration/onRenderHtml:onRenderHtml', 18 | // https://vike.dev/onRenderClient 19 | onRenderClient: 'import:vike-react/__internal/integration/onRenderClient:onRenderClient', 20 | 21 | // @eject-remove end 22 | passToClient: [ 23 | '_configFromHook', 24 | // https://github.com/vikejs/vike-react/issues/25 25 | process.env.NODE_ENV !== 'production' && '$$typeof', 26 | ].filter(isNotFalse), 27 | 28 | // https://vike.dev/clientRouting 29 | clientRouting: true, 30 | hydrationCanBeAborted: true, 31 | 32 | // https://vike.dev/meta 33 | meta: { 34 | Head: { 35 | env: { server: true }, 36 | cumulative: true, 37 | }, 38 | Layout: { 39 | env: { server: true, client: true }, 40 | cumulative: true, 41 | }, 42 | title: { 43 | env: { server: true, client: true }, 44 | }, 45 | description: { 46 | env: { server: true }, 47 | }, 48 | image: { 49 | env: { server: true }, 50 | }, 51 | viewport: { 52 | env: { server: true }, 53 | }, 54 | favicon: { 55 | env: { server: true }, 56 | global: true, 57 | }, 58 | lang: { 59 | env: { server: true, client: true }, 60 | }, 61 | bodyHtmlBegin: { 62 | env: { server: true }, 63 | cumulative: true, 64 | global: true, 65 | }, 66 | bodyHtmlEnd: { 67 | env: { server: true }, 68 | cumulative: true, 69 | global: true, 70 | }, 71 | htmlAttributes: { 72 | env: { server: true }, 73 | global: true, 74 | cumulative: true, // for Vike extensions 75 | }, 76 | bodyAttributes: { 77 | env: { server: true }, 78 | global: true, 79 | cumulative: true, // for Vike extensions 80 | }, 81 | ssr: { 82 | env: { config: true }, 83 | effect: ssrEffect, 84 | }, 85 | stream: { 86 | env: { server: true }, 87 | cumulative: true, 88 | }, 89 | streamIsRequired: { 90 | env: { server: true }, 91 | }, 92 | onBeforeRenderHtml: { 93 | env: { server: true }, 94 | cumulative: true, 95 | }, 96 | onAfterRenderHtml: { 97 | env: { server: true }, 98 | cumulative: true, 99 | }, 100 | onBeforeRenderClient: { 101 | env: { client: true }, 102 | cumulative: true, 103 | }, 104 | onAfterRenderClient: { 105 | env: { client: true }, 106 | cumulative: true, 107 | }, 108 | Wrapper: { 109 | cumulative: true, 110 | env: { client: true, server: true }, 111 | }, 112 | // TODO/next-major: move to +react.js > strictMode ? 113 | reactStrictMode: { 114 | env: { client: true, server: true }, 115 | }, 116 | Loading: { 117 | env: { server: true, client: true }, 118 | }, 119 | react: { 120 | cumulative: true, 121 | env: {}, 122 | }, 123 | }, 124 | } satisfies Config 125 | // @eject-remove start 126 | 127 | // This is required to make TypeScript load the global interfaces Vike.Config and Vike.PageContext so that they're always loaded: we can assume that the user always imports this file over `import vikeReact from 'vike-react/config'` 128 | import './types/Config.js' 129 | import './types/PageContext.js' 130 | // @eject-remove end 131 | -------------------------------------------------------------------------------- /packages/vike-react/src/helpers/clientOnly.tsx: -------------------------------------------------------------------------------- 1 | export { clientOnly } 2 | 3 | import React, { 4 | Suspense, 5 | forwardRef, 6 | lazy, 7 | useEffect, 8 | useState, 9 | type ComponentProps, 10 | type ComponentType, 11 | type ReactNode, 12 | } from 'react' 13 | 14 | function clientOnly<T extends ComponentType<any>>( 15 | load: () => Promise<{ default: T } | T>, 16 | ): ComponentType<ComponentProps<T> & { fallback?: ReactNode }> { 17 | // Client side: always bundled by Vite, import.meta.env.SSR === false 18 | // Server side: may or may not be bundled by Vite, import.meta.env.SSR === true || import.meta.env === undefined 19 | //@ts-expect-error 20 | import.meta.env ??= { SSR: true } 21 | if (import.meta.env.SSR) { 22 | return (props) => <>{props.fallback}</> 23 | } else { 24 | const Component = lazy(() => 25 | load() 26 | .then((LoadedComponent) => ('default' in LoadedComponent ? LoadedComponent : { default: LoadedComponent })) 27 | .catch((error) => { 28 | console.error('Component loading failed:', error) 29 | return { default: (() => <p>Error loading component.</p>) as any } 30 | }), 31 | ) 32 | 33 | return forwardRef((props, ref) => { 34 | const [mounted, setMounted] = useState(false) 35 | useEffect(() => { 36 | setMounted(true) 37 | }, []) 38 | if (!mounted) { 39 | return <>{props.fallback}</> 40 | } 41 | const { fallback, ...rest } = props 42 | return ( 43 | <Suspense fallback={<>{props.fallback}</>}> 44 | {/* @ts-ignore */} 45 | <Component {...rest} ref={ref} /> 46 | </Suspense> 47 | ) 48 | }) as ComponentType<ComponentProps<T> & { fallback?: ReactNode }> 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/vike-react/src/hooks/useConfig/configsCumulative.ts: -------------------------------------------------------------------------------- 1 | export const configsCumulative = ['Head', 'bodyAttributes', 'htmlAttributes'] as const 2 | export type ConfigsCumulative = (typeof configsCumulative)[number] 3 | -------------------------------------------------------------------------------- /packages/vike-react/src/hooks/useConfig/useConfig-client.ts: -------------------------------------------------------------------------------- 1 | export { useConfig } 2 | 3 | import type { PageContext } from 'vike/types' 4 | import type { PageContextInternal } from '../../types/PageContext.js' 5 | import type { ConfigFromHook } from '../../types/Config.js' 6 | import { usePageContext } from '../usePageContext.js' 7 | import { getPageContext } from 'vike/getPageContext' 8 | import { applyHeadSettings } from '../../integration/applyHeadSettings.js' 9 | 10 | function useConfig(): (config: ConfigFromHook) => void { 11 | // Vike hook 12 | let pageContext = getPageContext() as PageContext & PageContextInternal 13 | if (pageContext) return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext) 14 | 15 | // Component 16 | pageContext = usePageContext() 17 | return (config: ConfigFromHook) => { 18 | if (!('_headAlreadySet' in pageContext)) { 19 | setPageContextConfigFromHook(config, pageContext) 20 | } else { 21 | applyHead(config) 22 | } 23 | } 24 | } 25 | 26 | function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageContextInternal) { 27 | pageContext._configFromHook ??= {} 28 | Object.assign(pageContext._configFromHook, config) 29 | } 30 | 31 | function applyHead(config: ConfigFromHook) { 32 | const { title, lang } = config 33 | applyHeadSettings(title, lang) 34 | } 35 | -------------------------------------------------------------------------------- /packages/vike-react/src/hooks/useConfig/useConfig-server.ts: -------------------------------------------------------------------------------- 1 | export { useConfig } 2 | import type { PageContext } from 'vike/types' 3 | import type { PageContextInternal } from '../../types/PageContext.js' 4 | import type { ConfigFromHook } from '../../types/Config.js' 5 | import { usePageContext } from '../usePageContext.js' 6 | import { getPageContext } from 'vike/getPageContext' 7 | import { useStreamOptional } from 'react-streaming' 8 | import { objectKeys } from '../../utils/objectKeys.js' 9 | import { includes } from '../../utils/includes.js' 10 | import { assert } from '../../utils/assert.js' 11 | import { configsCumulative } from './configsCumulative.js' 12 | 13 | /** 14 | * Set configurations inside components and Vike hooks. 15 | * 16 | * https://vike.dev/useConfig 17 | */ 18 | function useConfig(): (config: ConfigFromHook) => void { 19 | // Vike hook 20 | let pageContext = getPageContext() as PageContext & PageContextInternal 21 | if (pageContext) return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext) 22 | 23 | // Component 24 | pageContext = usePageContext() 25 | const stream = useStreamOptional() 26 | return (config: ConfigFromHook) => { 27 | if (!pageContext._headAlreadySet) { 28 | setPageContextConfigFromHook(config, pageContext) 29 | } else { 30 | assert(stream) 31 | // <head> already sent to the browser => send DOM-manipulating scripts during HTML streaming 32 | apply(config, stream) 33 | } 34 | } 35 | } 36 | 37 | const configsClientSide = ['title'] 38 | function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageContext & PageContextInternal) { 39 | pageContext._configFromHook ??= {} 40 | objectKeys(config).forEach((configName) => { 41 | // Skip HTML only configs which the client-side doesn't need, saving KBs sent to the client as well as avoiding serialization errors. 42 | if (pageContext.isClientSideNavigation && !configsClientSide.includes(configName)) return 43 | 44 | if (!includes(configsCumulative, configName)) { 45 | // Overridable config 46 | const configValue = config[configName] 47 | if (configValue === undefined) return 48 | pageContext._configFromHook![configName] = configValue as any 49 | } else { 50 | // Cumulative config 51 | const configValue = config[configName] 52 | if (!configValue) return 53 | pageContext._configFromHook![configName] ??= [] 54 | pageContext._configFromHook![configName].push(configValue as any) 55 | } 56 | }) 57 | } 58 | 59 | type Stream = NonNullable<ReturnType<typeof useStreamOptional>> 60 | function apply(config: ConfigFromHook, stream: Stream) { 61 | const { title } = config 62 | if (title) { 63 | const htmlSnippet = `<script>document.title = ${JSON.stringify(title)}</script>` 64 | stream.injectToStream(htmlSnippet) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/vike-react/src/hooks/useData.tsx: -------------------------------------------------------------------------------- 1 | export { useData } 2 | 3 | import { usePageContext } from './usePageContext.js' 4 | 5 | function useData<Data>(): Data { 6 | const data = usePageContext()?.data 7 | return data as any 8 | } 9 | -------------------------------------------------------------------------------- /packages/vike-react/src/hooks/usePageContext.tsx: -------------------------------------------------------------------------------- 1 | export { usePageContext } 2 | export { PageContextProvider } 3 | 4 | import React, { useContext } from 'react' 5 | import { getGlobalObject } from '../utils/getGlobalObject.js' 6 | import type { PageContext } from 'vike/types' 7 | 8 | const globalObject = getGlobalObject('PageContextProvider.ts', { 9 | reactContext: React.createContext<PageContext>(undefined as never), 10 | }) 11 | 12 | function PageContextProvider({ pageContext, children }: { pageContext: PageContext; children: React.ReactNode }) { 13 | const { reactContext } = globalObject 14 | return <reactContext.Provider value={pageContext}>{children}</reactContext.Provider> 15 | } 16 | 17 | /** 18 | * Access `pageContext` from any React component. 19 | * 20 | * https://vike.dev/usePageContext 21 | */ 22 | function usePageContext(): PageContext { 23 | const { reactContext } = globalObject 24 | const pageContext = useContext(reactContext) 25 | return pageContext 26 | } 27 | -------------------------------------------------------------------------------- /packages/vike-react/src/index.ts: -------------------------------------------------------------------------------- 1 | // TODO/next-major: remove this file/export 2 | console.warn( 3 | "[vike-react][warning][deprecation] Replace `import vikeReact from 'vike-react'` with `import vikeReact from 'vike-react/config'` (typically in your /pages/+config.js)", 4 | ) 5 | export { default } from './config.js' 6 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/Loading.css: -------------------------------------------------------------------------------- 1 | /* 2 | This CSS is loaded for all vike-react users, even if they don't use the <Loading> component because it's imported not directly but over depednency injection, see: 3 | https://github.com/vikejs/vike/discussions/2340 4 | */ 5 | 6 | @keyframes vike-react-shine { 7 | to { 8 | background-position-x: -200%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/Loading.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | component: LoadingComponent, 3 | } 4 | 5 | import React from 'react' 6 | /* We can't import it here: https://github.com/vikejs/vike/issues/2460 7 | * - We import it inside onRenderClient.js instead. 8 | * - We'll be able to do it if Vite + Rolldown always transpiles the server-side. 9 | import './Loading.css' 10 | */ 11 | 12 | function LoadingComponent() { 13 | return ( 14 | <div 15 | style={{ 16 | width: '100%', 17 | height: '100%', 18 | maxHeight: '100%', 19 | background: 'linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%)', 20 | borderRadius: '5px', 21 | backgroundSize: '200% 100%', 22 | animation: '1.3s vike-react-shine linear infinite', 23 | aspectRatio: '2.5/1', 24 | }} 25 | /> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/applyHeadSettings.tsx: -------------------------------------------------------------------------------- 1 | export { applyHeadSettings } 2 | 3 | type Value = string | null | undefined 4 | 5 | // - We skip if `undefined` as we shouldn't remove values set by the Head setting. 6 | // - Setting a default prevents the previous value to be leaked: upon client-side navigation, the value set by the previous page won't be removed if the next page doesn't override it. 7 | // - Most of the time, the user sets a default himself (i.e. a value defined at /pages/+config.js) 8 | // - If he doesn't have a default then he can use `null` to opt into Vike's defaults 9 | 10 | function applyHeadSettings(title: Value, lang: Value) { 11 | if (title !== undefined) document.title = title || '' 12 | if (lang !== undefined) document.documentElement.lang = lang || 'en' 13 | } 14 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/getHeadSetting.tsx: -------------------------------------------------------------------------------- 1 | export { getHeadSetting } 2 | 3 | import { isCallable } from '../utils/isCallable.js' 4 | import type { PageContext } from 'vike/types' 5 | import type { PageContextInternal } from '../types/PageContext.js' 6 | import type { ConfigFromHookResolved } from '../types/Config.js' 7 | import { configsCumulative } from '../hooks/useConfig/configsCumulative.js' 8 | import { includes } from '../utils/includes.js' 9 | 10 | // We use `any` instead of doing proper validation in order to save KBs sent to the client-side. 11 | 12 | function getHeadSetting<T>( 13 | configName: keyof ConfigFromHookResolved, 14 | pageContext: PageContext & PageContextInternal, 15 | ): undefined | T { 16 | // Set by useConfig() 17 | const valFromUseConfig = pageContext._configFromHook?.[configName] 18 | // Set by +configName.js 19 | const valFromConfig = pageContext.config[configName] 20 | 21 | const getCallable = (val: unknown) => (isCallable(val) ? val(pageContext) : val) 22 | if (!includes(configsCumulative, configName)) { 23 | if (valFromUseConfig !== undefined) return valFromUseConfig as any 24 | return getCallable(valFromConfig) as any 25 | } else { 26 | return [ 27 | // 28 | ...((valFromConfig as any) ?? []).map(getCallable), 29 | ...((valFromUseConfig as any) ?? []), 30 | ] as any 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/getPageElement.tsx: -------------------------------------------------------------------------------- 1 | export { getPageElement } 2 | 3 | import React, { Suspense, useEffect } from 'react' 4 | import type { PageContext } from 'vike/types' 5 | import { PageContextProvider } from '../hooks/usePageContext.js' 6 | 7 | function getPageElement(pageContext: PageContext): { page: React.JSX.Element; renderPromise: Promise<void> } { 8 | const { 9 | Page, 10 | config: { Loading }, 11 | } = pageContext 12 | let page = Page ? <Page /> : null 13 | 14 | // Wrapping 15 | const addSuspense = (el: React.JSX.Element | null) => { 16 | if (!Loading?.layout) return el 17 | return <Suspense fallback={<Loading.layout />}>{page}</Suspense> 18 | } 19 | page = addSuspense(page) 20 | ;[ 21 | // Inner wrapping 22 | ...(pageContext.config.Layout || []), 23 | // Outer wrapping 24 | ...(pageContext.config.Wrapper || []), 25 | ].forEach((Wrap) => { 26 | page = <Wrap>{page}</Wrap> 27 | page = addSuspense(page) 28 | }) 29 | 30 | page = <PageContextProvider pageContext={pageContext}>{page}</PageContextProvider> 31 | 32 | let renderPromiseResolve!: () => void 33 | let renderPromise = new Promise<void>((r) => (renderPromiseResolve = r)) 34 | page = <RenderPromiseProvider renderPromiseResolve={renderPromiseResolve}>{page}</RenderPromiseProvider> 35 | 36 | if (pageContext.config.reactStrictMode !== false) { 37 | page = <React.StrictMode>{page}</React.StrictMode> 38 | } 39 | 40 | return { page, renderPromise } 41 | } 42 | 43 | function RenderPromiseProvider({ 44 | children, 45 | renderPromiseResolve, 46 | }: { children: React.ReactNode; renderPromiseResolve: () => void }) { 47 | useEffect(renderPromiseResolve) 48 | return children 49 | } 50 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/onRenderClient.tsx: -------------------------------------------------------------------------------- 1 | // https://vike.dev/onRenderClient 2 | export { onRenderClient } 3 | 4 | import ReactDOM from 'react-dom/client' 5 | import { getHeadSetting } from './getHeadSetting.js' 6 | import type { OnRenderClientAsync, PageContextClient } from 'vike/types' 7 | import { getPageElement } from './getPageElement.js' 8 | import type { PageContextInternal } from '../types/PageContext.js' 9 | import { callCumulativeHooks } from '../utils/callCumulativeHooks.js' 10 | import { applyHeadSettings } from './applyHeadSettings.js' 11 | import { resolveReactOptions } from './resolveReactOptions.js' 12 | import './Loading.css' // See comment inside Loading.tsx 13 | 14 | let root: ReactDOM.Root 15 | const onRenderClient: OnRenderClientAsync = async ( 16 | pageContext: PageContextClient & PageContextInternal, 17 | ): ReturnType<OnRenderClientAsync> => { 18 | pageContext._headAlreadySet = pageContext.isHydration 19 | 20 | // Use case: 21 | // - Store hydration https://github.com/vikejs/vike-react/issues/110 22 | await callCumulativeHooks(pageContext.config.onBeforeRenderClient, pageContext) 23 | 24 | const { page, renderPromise } = getPageElement(pageContext) 25 | pageContext.page = page 26 | 27 | // TODO: implement this? So that, upon errors, onRenderClient() throws an error and Vike can render the error page. As of April 2024 it isn't released yet. 28 | // - https://react-dev-git-fork-rickhanlonii-rh-root-options-fbopensource.vercel.app/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors 29 | // - https://react-dev-git-fork-rickhanlonii-rh-root-options-fbopensource.vercel.app/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors 30 | const onUncaughtError = (_error: any, _errorInfo: any) => {} 31 | 32 | const container = document.getElementById('root')! 33 | const { hydrateRootOptions, createRootOptions } = resolveReactOptions(pageContext) 34 | if ( 35 | pageContext.isHydration && 36 | // Whether the page was [Server-Side Rendered](https://vike.dev/ssr). 37 | container.innerHTML !== '' 38 | ) { 39 | // First render while using SSR, i.e. [hydration](https://vike.dev/hydration) 40 | root = ReactDOM.hydrateRoot(container, page, hydrateRootOptions) 41 | } else { 42 | if (!root) { 43 | // First render without SSR 44 | root = ReactDOM.createRoot(container, createRootOptions) 45 | } 46 | root.render(page) 47 | } 48 | pageContext.root = root 49 | 50 | await renderPromise 51 | 52 | if (!pageContext.isHydration) { 53 | pageContext._headAlreadySet = true 54 | applyHead(pageContext) 55 | } 56 | 57 | // Use cases: 58 | // - Custom user settings: https://vike.dev/head-tags#custom-settings 59 | // - Testing tools: https://github.com/vikejs/vike-react/issues/95 60 | await callCumulativeHooks(pageContext.config.onAfterRenderClient, pageContext) 61 | } 62 | 63 | function applyHead(pageContext: PageContextClient) { 64 | const title = getHeadSetting<string | null>('title', pageContext) 65 | const lang = getHeadSetting<string | null>('lang', pageContext) 66 | applyHeadSettings(title, lang) 67 | } 68 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/resolveReactOptions.ts: -------------------------------------------------------------------------------- 1 | export { resolveReactOptions } 2 | 3 | import type { PageContext } from 'vike/types' 4 | import type { ReactOptions } from '../types/Config.js' 5 | import { isCallable } from '../utils/isCallable.js' 6 | import { objectEntries } from '../utils/objectEntries.js' 7 | 8 | function resolveReactOptions(pageContext: PageContext) { 9 | const optionsAcc: ReactOptions = {} 10 | ;(pageContext.config.react ?? []).forEach((valUnresolved) => { 11 | const optionList = isCallable(valUnresolved) ? valUnresolved(pageContext) : valUnresolved 12 | if (!optionList) return 13 | objectEntries(optionList).forEach(([fnName, options]) => { 14 | if (!options) return 15 | optionsAcc[fnName] ??= {} 16 | objectEntries(options).forEach(([key, val]) => { 17 | if (!isCallable(val)) { 18 | // @ts-ignore 19 | optionsAcc[fnName][key] ??= val 20 | } else { 21 | const valPrevious = optionsAcc[fnName]![key] as any as Function | undefined 22 | // @ts-ignore 23 | optionsAcc[fnName][key] = (...args: unknown[]) => { 24 | valPrevious?.(...args) 25 | val(...args) 26 | } 27 | } 28 | }) 29 | }) 30 | }) 31 | return optionsAcc 32 | } 33 | -------------------------------------------------------------------------------- /packages/vike-react/src/integration/ssrEffect.ts: -------------------------------------------------------------------------------- 1 | export { ssrEffect } 2 | 3 | import type { ConfigEffect } from 'vike/types' 4 | 5 | function ssrEffect({ configDefinedAt, configValue }: Parameters<ConfigEffect>[0]): ReturnType<ConfigEffect> { 6 | if (typeof configValue !== 'boolean') throw new Error(`${configDefinedAt} should be a boolean`) 7 | const env = { 8 | // Always load on the client-side. 9 | client: true, 10 | // When the SSR flag is false, we want to render the page only on the client-side. 11 | // We achieve this by loading `Page` only on the client-side: when onRenderHtml() 12 | // gets a `Page` value that is undefined it skip server-side rendering. 13 | server: configValue !== false, 14 | } 15 | return { 16 | meta: { 17 | Page: { env }, 18 | /* We don't do this to enable wrapping <Head> with <Wrapper> 19 | Wrapper: { env }, */ 20 | Layout: { env }, 21 | Loading: { env }, 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vike-react/src/types/PageContext.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import type ReactDOM from 'react-dom/client' 3 | import type { ConfigFromHookResolved } from './Config.js' 4 | import type { PageHtmlStream } from '../integration/onRenderHtml.js' 5 | 6 | // https://vike.dev/pageContext#typescript 7 | declare global { 8 | namespace Vike { 9 | interface PageContext { 10 | /** The root React component of the page */ 11 | Page?: () => React.ReactNode 12 | /** The root React element of the page */ 13 | page?: React.JSX.Element 14 | /** The React root DOM container */ 15 | root?: ReactDOM.Root 16 | 17 | /** The +Page.jsx component rendered to the HTML string. */ 18 | pageHtmlString?: string 19 | /** The +Pagejsx component rendered to an HTML stream. */ 20 | pageHtmlStream?: PageHtmlStream 21 | } 22 | } 23 | } 24 | 25 | // Internal usage 26 | export type PageContextInternal = { 27 | _configFromHook?: ConfigFromHookResolved 28 | _headAlreadySet?: boolean 29 | } 30 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | export function assert(condition: unknown): asserts condition { 2 | if (condition) return 3 | throw new Error('You stumbled upon a vike-react bug, reach out on GitHub.') 4 | } 5 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/callCumulativeHooks.ts: -------------------------------------------------------------------------------- 1 | export { callCumulativeHooks } 2 | 3 | import { providePageContext } from 'vike/getPageContext' 4 | import { isCallable } from './isCallable.js' 5 | import type { PageContext } from 'vike/types' 6 | 7 | async function callCumulativeHooks<T>( 8 | values: undefined | T[], 9 | pageContext: PageContext, 10 | ): Promise<(undefined | null | Exclude<T, Function>)[]> { 11 | if (!values) return [] 12 | const valuesPromises = values.map((val) => { 13 | if (isCallable(val)) { 14 | providePageContext(pageContext) 15 | // Hook 16 | return val(pageContext) 17 | } else { 18 | // Plain value 19 | return val 20 | } 21 | }) 22 | const valuesResolved = await Promise.all(valuesPromises) 23 | return valuesResolved 24 | } 25 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/getGlobalObject.ts: -------------------------------------------------------------------------------- 1 | export function getGlobalObject<T extends Record<string, unknown> = never>( 2 | // We use the filename as key; each `getGlobalObject()` call should live inside a file with a unique filename. 3 | key: `${string}.ts`, 4 | defaultValue: T, 5 | ): T { 6 | // @ts-ignore 7 | const globalObjectsAll = (globalThis[projectKey] = globalThis[projectKey] || {}) 8 | const globalObject = (globalObjectsAll[key] = globalObjectsAll[key] || defaultValue) 9 | return globalObject 10 | } 11 | const projectKey = '_vike_react' 12 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/getTagAttributesString.ts: -------------------------------------------------------------------------------- 1 | export { getTagAttributesString } 2 | export type { TagAttributes } 3 | 4 | type TagAttributes = Record<string, string | number | boolean | undefined | null> 5 | 6 | function getTagAttributesString(tagAttributes: TagAttributes): string { 7 | const tagAttributesString = Object.entries(tagAttributes) 8 | .filter(([_key, value]) => value !== false && value !== null && value !== undefined) 9 | .map(([key, value]) => `${ensureIsValidAttributeName(key)}=${JSON.stringify(String(value))}`) 10 | .join(' ') 11 | if (tagAttributesString.length === 0) return '' 12 | return ` ${tagAttributesString}` 13 | } 14 | 15 | function ensureIsValidAttributeName(str: string): string { 16 | if (/^[a-z][a-z0-9\-]*$/i.test(str) && !str.endsWith('-')) return str 17 | throw new Error(`Invalid HTML tag attribute name ${JSON.stringify(str)}`) 18 | } 19 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/includes.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/56565528/typescript-const-assertions-how-to-use-array-prototype-includes/74213179#74213179 2 | /** Same as Array.prototype.includes() but with type inference */ 3 | export function includes<T>(values: readonly T[], x: unknown): x is T { 4 | return values.includes(x as any) 5 | } 6 | /* 7 | export function includes<Arr extends any[] | readonly any[]>(arr: Arr, el: unknown): el is Arr[number] { 8 | return arr.includes(el as any) 9 | } 10 | */ 11 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isCallable.ts: -------------------------------------------------------------------------------- 1 | export function isCallable<T extends (...args: any[]) => any>(thing: T | unknown): thing is T { 2 | return thing instanceof Function || typeof thing === 'function' 3 | } 4 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isNotFalse.ts: -------------------------------------------------------------------------------- 1 | export function isNotFalse<T>(val: T | false): val is T { 2 | return val !== false 3 | } 4 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isNotNullish.ts: -------------------------------------------------------------------------------- 1 | export function isNullish(val: unknown): val is null | undefined { 2 | return val === null || val === undefined 3 | } 4 | // someArray.filter(isNotNullish) 5 | export function isNotNullish<T>(p: T | null | undefined): p is T { 6 | return !isNullish(p) 7 | } 8 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isObject.ts: -------------------------------------------------------------------------------- 1 | export function isObject(value: unknown): value is Record<string, unknown> { 2 | return typeof value === 'object' && value !== null 3 | } 4 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isReactElement.ts: -------------------------------------------------------------------------------- 1 | export { isReactElement } 2 | 3 | import React, { isValidElement } from 'react' 4 | 5 | function isReactElement(value: ReactElement | ReactComponent): value is ReactElement { 6 | return isValidElement(value) || Array.isArray(value) 7 | } 8 | type ReactElement = React.ReactNode 9 | type ReactComponent = () => ReactElement 10 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/isType.ts: -------------------------------------------------------------------------------- 1 | export function isType<Type>(_: Type) {} 2 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/objectEntries.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type/75337277#75337277 2 | /** Same as Object.entries() but with type inference */ 3 | export function objectEntries<T extends object>(obj: T): [keyof T, T[keyof T]][] { 4 | return Object.entries(obj) as any 5 | } 6 | -------------------------------------------------------------------------------- /packages/vike-react/src/utils/objectKeys.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/52856496/typescript-object-keys-return-string 2 | // https://github.com/sindresorhus/ts-extras/blob/main/source/object-keys.ts 3 | /** Same as Object.keys() but with type inference */ 4 | export function objectKeys<T extends object>(obj: T): (keyof T)[] { 5 | return Object.keys(obj) as any 6 | } 7 | -------------------------------------------------------------------------------- /packages/vike-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | // Resolution 5 | "target": "ES2020", 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | // Libs 9 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 10 | "types": ["vite/client"], 11 | // Strictness 12 | "strict": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noImplicitAny": true, 15 | // Output 16 | "declaration": true, 17 | "noEmitOnError": false, 18 | "rootDir": "./src", 19 | // Misc 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "jsx": "react" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | --------------------------------------------------------------------------------