Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Next.js 13 RSC integration #139

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.DS_Store
node_modules
dist
.vscode
8 changes: 6 additions & 2 deletions packages/example/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
require('eslint-config-molindo/setupPlugins');

module.exports = {
extends: ['molindo/typescript', 'molindo/react', 'plugin:@next/next/recommended'],
extends: [
'molindo/typescript',
'molindo/react',
'plugin:@next/next/recommended'
],
rules: {
'react/react-in-jsx-scope': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'react/display-name': 'off'
}
}
};
1 change: 1 addition & 0 deletions packages/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/.next/
.DS_Store
tsconfig.tsbuildinfo
.vscode
7 changes: 2 additions & 5 deletions packages/example/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
module.exports = {
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en'
}
}
experimental: {appDir: true}
};
10 changes: 6 additions & 4 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
"start": "next start"
},
"dependencies": {
"accept-language-parser": "1.5.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"next": "^12.1.4",
"next": "^13.0.1",
"next-intl": "^2.8.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"server-only": "0.0.1",
"typescript": "^4.6.3"
},
"devDependencies": {
Expand All @@ -24,6 +26,6 @@
"@types/react": "18.0.0",
"eslint": "8.12.0",
"eslint-config-molindo": "6.0.0",
"eslint-config-next": "^12.0.0"
"eslint-config-next": "^13.0.0"
}
}
15 changes: 15 additions & 0 deletions packages/example/src/app/Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';
import {NextIntlProvider} from 'next-intl';
import {ReactNode} from 'react';

type Props = {
children: ReactNode;
locale: string;
};

export default function Provider({children, locale}: Props) {
console.log('Provider');

// return children;
return <NextIntlProvider locale={locale}>{children}</NextIntlProvider>;
}
8 changes: 8 additions & 0 deletions packages/example/src/app/ServerOnlyContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'server-only';
import {createServerContext} from 'react';

// This is always passed to the client, regardless of if it's read from a client component!
// Interestingly the module code is not there, but the context value.
const ServerOnlyContext = createServerContext('serverOnly', 'initialValue');

export default ServerOnlyContext;
48 changes: 48 additions & 0 deletions packages/example/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Storage from '../../utils/Storage';

export function generateStaticParams() {
return ['de', 'en'].map((locale) => ({locale}));
}

type Props = {
params: {
locale: string;
};
};

export default function Index({params: {locale}}: Props) {
const value = Storage.get();

return (
<p>
Hello {locale} ({value.now})
</p>
);
}

// import {useTranslations} from 'next-intl';
// import LocaleSwitcher from 'components/LocaleSwitcher';
// import PageLayout from 'components/PageLayout';

// import {useContext} from 'react';
// import ServerOnlyContext from './ServerOnlyContext';

// export default function Index() {
// // TODO: Use middleware to redirect to a specific locale

// const serverOnly = useContext(ServerOnlyContext);
// // const t = useTranslations('Index');

// console.log('Index');

// // return <p>{t('title')}</p>;

// return <p>Hello {serverOnly.only.for.server + 10}</p>;

// // return (
// // <PageLayout title={t('title')}>
// // <p>{t('description')}</p>
// // <LocaleSwitcher />
// // </PageLayout>
// // );
// }
31 changes: 31 additions & 0 deletions packages/example/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {ReactNode} from 'react';
import Storage from '../utils/Storage';
import ServerOnlyContext from './ServerOnlyContext';

type Props = {
children: ReactNode;
};

export default function RootLayout({children}: Props) {
const now = new Date().toISOString();
Storage.set({now});

// How to get this from the URL?
Copy link

Choose a reason for hiding this comment

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

Maybe wrap all pages under optional [[locale]] then get using params

https://beta.nextjs.org/docs/routing/defining-routes#example

Copy link
Owner Author

Choose a reason for hiding this comment

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

Hey and thank you for your interest! Yep, that seems to work, I've experimented with it here:

export default function Index({params: {locale}}: Props) {
// TODO: Validate locale or redirect to default locale
return <p>Hello {locale}</p>;
}

I think that part is doable, the part where I currently don't see an ergonomic solution is how global configuration like messages can be passed to the library (see the topic about context in the PR description).

Copy link

@fdarian fdarian Nov 11, 2022

Choose a reason for hiding this comment

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

I've tried some workarounds,
1, Using singleton — Failed because for RSC, initializing on root layout requires 2 render to assign the messages object.
2. Using third-party state manager like zustand — Failed because of the same reason as number 1.

Probably the solution is gonna be around client component. To use in a server component we could provide utility component like <T key='dashboard.hi' />. Performance-wise, next will also server-render client components so the downside is on developer experience. But won't be much I think.

ps: I can't find any documentation for createServerContext, but I just try to use it regardless the type-error and it seems like it works without sacrificing DX. However, we need to refactor functions out of the context.

// TODO: Validate locale or redirect to default locale
const locale = 'en';

return (
<html lang={locale}>
<head>
<title>next-intl example</title>
</head>
<body>
<ServerOnlyContext.Provider value={{only: {for: {server: 42}}}}>
{/* <Provider locale={locale}> */}
{children}
{/* </Provider> */}
</ServerOnlyContext.Provider>
</body>
</html>
);
}
4 changes: 4 additions & 0 deletions packages/example/src/i18n.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
locales: ['en', 'de'],
defaultLocale: 'en'
};
26 changes: 26 additions & 0 deletions packages/example/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import acceptLanguageParser from 'accept-language-parser';
import {headers} from 'next/headers';
import {NextResponse} from 'next/server';
import type {NextRequest} from 'next/server';
import i18n from './i18n';

// Somehow not invoked?

Choose a reason for hiding this comment

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

From a quick glance at the example WIP, the middleware doesn't seem to fire unless there is a "pages" folder as well. After that, it fires on both "pages" and "app" folders.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Oh cool, thanks for figuring this out! Seems like a bug on the Next.js side.


function resolveLocale(requestHeaders: Headers) {
const locale =
acceptLanguageParser.pick(
i18n.locales,
requestHeaders.get('accept-language') || i18n.defaultLocale
) || i18n.defaultLocale;

return locale;
}

export function middleware(request: NextRequest) {
const locale = resolveLocale(headers());
return NextResponse.redirect(new URL('/' + locale, request.url));
}

export const config = {
matcher: '/test'
};
10 changes: 0 additions & 10 deletions packages/example/src/pages/_app.tsx

This file was deleted.

23 changes: 0 additions & 23 deletions packages/example/src/pages/index.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions packages/example/src/utils/Storage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {requestAsyncStorage} from 'next/dist/client/components/request-async-storage';

const key = '__next-intl';

/**
* Returns the request-level storage of Next.js where typically headers
* and cookies are stored. This is recreated for every request.
*
* This uses internal APIs of Next.js and may break in the future.
*/
function getStorage() {
const requestStore =
requestAsyncStorage && 'getStore' in requestAsyncStorage
? requestAsyncStorage.getStore()!
: requestAsyncStorage;

return requestStore;
}

export default {
set(value: any) {
const storage = getStorage();
(storage as any)[key] = value;
},
get() {
const storage = getStorage();
return (storage as any)[key];
}
};
6 changes: 4 additions & 2 deletions packages/example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [{"name": "next"}]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
Expand Down
6 changes: 3 additions & 3 deletions packages/use-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"intl-messageformat": "^9.3.18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^18.2.0"
},
"devDependencies": {
"@testing-library/react": "^13.0.0",
Expand All @@ -52,8 +52,8 @@
"dts-cli": "1.4.0",
"eslint": "8.12.0",
"eslint-config-molindo": "6.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tslib": "^2.3.1",
"typescript": "^4.4.4"
},
Expand Down
Loading