Skip to content

Commit

Permalink
Merge branch 'canary' into update/telemetry-flushing
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk authored Jan 6, 2023
2 parents 6dc7155 + 02f97ab commit c28bd32
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'

export function clientHookInServerComponentError(
hookName: string
): void | never {
if (process.env.NODE_ENV !== 'production') {
// If useState is undefined we're in a server component
if (!React.useState) {
throw new Error(
`${hookName} only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component`
)
}
}
}
12 changes: 7 additions & 5 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
// LayoutSegmentsContext,
} from '../../shared/lib/hooks-client-context'
import { bailoutToClientRendering } from './bailout-to-client-rendering'
import { clientHookInServerComponentError } from './client-hook-in-server-component-error'

const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol(
'internal for urlsearchparams readonly'
Expand Down Expand Up @@ -70,6 +71,7 @@ class ReadonlyURLSearchParams {
* Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
*/
export function useSearchParams() {
clientHookInServerComponentError('useSearchParams')
const searchParams = useContext(SearchParamsContext)

const readonlySearchParams = useMemo(() => {
Expand All @@ -92,19 +94,16 @@ export function useSearchParams() {
* Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard"
*/
export function usePathname(): string | null {
clientHookInServerComponentError('usePathname')
return useContext(PathnameContext)
}

// TODO-APP: getting all params when client-side navigating is non-trivial as it does not have route matchers so this might have to be a server context instead.
// export function useParams() {
// clientHookInServerComponentError('useParams')
// return useContext(ParamsContext)
// }

// TODO-APP: define what should be provided through context.
// export function useLayoutSegments() {
// return useContext(LayoutSegmentsContext)
// }

export {
ServerInsertedHTMLContext,
useServerInsertedHTML,
Expand All @@ -114,6 +113,7 @@ export {
* Get the router methods. For example router.push('/dashboard')
*/
export function useRouter(): import('../../shared/lib/app-router-context').AppRouterInstance {
clientHookInServerComponentError('useRouter')
const router = useContext(AppRouterContext)
if (router === null) {
throw new Error('invariant expected app router to be mounted')
Expand Down Expand Up @@ -161,6 +161,7 @@ function getSelectedLayoutSegmentPath(
export function useSelectedLayoutSegments(
parallelRouteKey: string = 'children'
): string[] {
clientHookInServerComponentError('useSelectedLayoutSegments')
const { tree } = useContext(LayoutRouterContext)
return getSelectedLayoutSegmentPath(tree, parallelRouteKey)
}
Expand All @@ -172,6 +173,7 @@ export function useSelectedLayoutSegments(
export function useSelectedLayoutSegment(
parallelRouteKey: string = 'children'
): string | null {
clientHookInServerComponentError('useSelectedLayoutSegment')
const selectedLayoutSegments = useSelectedLayoutSegments(parallelRouteKey)
if (selectedLayoutSegments.length === 0) {
return null
Expand Down
2 changes: 1 addition & 1 deletion scripts/run-for-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function main() {
const typeIndex = process.argv.indexOf('--type')
const type = typeIndex > -1 && process.argv[typeIndex + 1]
const isNegated = process.argv.indexOf('--not') > -1
const alwaysCanary = process.argv.includes('--always-canary') > -1
const alwaysCanary = process.argv.indexOf('--always-canary') > -1

if (!type) {
throw new Error(
Expand Down
42 changes: 42 additions & 0 deletions test/development/acceptance-app/server-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,5 +291,47 @@ createNextDescribe(
await cleanup()
})
})

describe('Next.js component hooks called in Server Component', () => {
it.each([
// TODO-APP: add test for useParams
// ["useParams"],
['useRouter'],
['useSearchParams'],
['useSelectedLayoutSegment'],
['useSelectedLayoutSegments'],
['usePathname'],
])('should show error when %s is called', async (hook: string) => {
const { session, cleanup } = await sandbox(
next,
new Map([
[
'app/page.js',
`
import { ${hook} } from 'next/navigation'
export default function Page() {
${hook}()
return "Hello world"
}`,
],
])
)

expect(await session.hasRedbox(true)).toBe(true)

await check(async () => {
expect(await session.getRedboxSource(true)).toContain(
`Error: ${hook} only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component`
)
return 'success'
}, 'success')

expect(next.cliOutput).toContain(
`${hook} only works in Client Components`
)

await cleanup()
})
})
}
)
4 changes: 3 additions & 1 deletion test/unit/google-font-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ describe('@next/font/google loader', () => {
variableName: 'myFont',
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Cannot read properties of undefined (reading 'subsets')"`
process.version.startsWith('v14')
? `"Cannot read property 'subsets' of undefined"`
: `"Cannot read properties of undefined (reading 'subsets')"`
)
})

Expand Down

0 comments on commit c28bd32

Please sign in to comment.