Update handling of ref in next/link (#8254)
This commit is contained in:
parent
3e8b36e879
commit
a4889f964e
8 changed files with 182 additions and 2 deletions
|
@ -578,6 +578,8 @@ function About() {
|
|||
export default About
|
||||
```
|
||||
|
||||
Note: if passing a functional component as a child of `<Link>` you will need to wrap it in [`React.forwardRef`](https://reactjs.org/docs/react-api.html#reactforwardref)
|
||||
|
||||
**Custom routes (using props from URL)**
|
||||
|
||||
If you find that your use case is not covered by [Dynamic Routing](#dynamic-routing) then you can create a custom server and manually add dynamic routes.
|
||||
|
|
|
@ -193,7 +193,7 @@ class Link extends Component<LinkProps> {
|
|||
render() {
|
||||
let { children } = this.props
|
||||
const { href, as } = this.formatUrls(this.props.href, this.props.as)
|
||||
// Deprecated. Warning shown by propType check. If the childen provided is a string (<Link>example</Link>) we wrap it in an <a> tag
|
||||
// Deprecated. Warning shown by propType check. If the children provided is a string (<Link>example</Link>) we wrap it in an <a> tag
|
||||
if (typeof children === 'string') {
|
||||
children = <a>{children}</a>
|
||||
}
|
||||
|
@ -206,7 +206,16 @@ class Link extends Component<LinkProps> {
|
|||
href?: string
|
||||
ref?: any
|
||||
} = {
|
||||
ref: (el: any) => this.handleRef(el),
|
||||
ref: (el: any) => {
|
||||
this.handleRef(el)
|
||||
|
||||
if (child && typeof child === 'object' && child.ref) {
|
||||
if (typeof child.ref === 'function') child.ref(el)
|
||||
else if (typeof child.ref === 'object') {
|
||||
child.ref.current = el
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseEnter: (e: React.MouseEvent) => {
|
||||
if (child.props && typeof child.props.onMouseEnter === 'function') {
|
||||
child.props.onMouseEnter(e)
|
||||
|
|
24
test/integration/link-ref/pages/child-ref-func.js
Normal file
24
test/integration/link-ref/pages/child-ref-func.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default () => {
|
||||
const myRef = React.createRef(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!myRef.current) {
|
||||
console.error(`ref wasn't updated`)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Link href='/'>
|
||||
<a
|
||||
ref={el => {
|
||||
myRef.current = el
|
||||
}}
|
||||
>
|
||||
Click me
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
18
test/integration/link-ref/pages/child-ref.js
Normal file
18
test/integration/link-ref/pages/child-ref.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default () => {
|
||||
const myRef = React.createRef(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!myRef.current) {
|
||||
console.error(`ref wasn't updated`)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Link href='/'>
|
||||
<a ref={myRef}>Click me</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
14
test/integration/link-ref/pages/class.js
Normal file
14
test/integration/link-ref/pages/class.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
class MyLink extends React.Component {
|
||||
render () {
|
||||
return <a {...this.props}>Click me</a>
|
||||
}
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<Link href='/' passHref>
|
||||
<MyLink />
|
||||
</Link>
|
||||
)
|
14
test/integration/link-ref/pages/functional.js
Normal file
14
test/integration/link-ref/pages/functional.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
const MyLink = React.forwardRef((props, ref) => (
|
||||
<a {...props} ref={ref}>
|
||||
Click me
|
||||
</a>
|
||||
))
|
||||
|
||||
export default () => (
|
||||
<Link href='/' passHref>
|
||||
<MyLink />
|
||||
</Link>
|
||||
)
|
1
test/integration/link-ref/pages/index.js
Normal file
1
test/integration/link-ref/pages/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => 'hi'
|
98
test/integration/link-ref/test/index.test.js
Normal file
98
test/integration/link-ref/test/index.test.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import {
|
||||
findPort,
|
||||
launchApp,
|
||||
killApp,
|
||||
nextStart,
|
||||
nextBuild,
|
||||
waitFor
|
||||
} from 'next-test-utils'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
|
||||
let app
|
||||
let appPort
|
||||
const appDir = join(__dirname, '..')
|
||||
|
||||
const noError = async pathname => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
await browser.eval(`(function() {
|
||||
window.caughtErrors = []
|
||||
const origError = window.console.error
|
||||
window.console.error = function () {
|
||||
window.caughtErrors.push(1)
|
||||
origError(arguments)
|
||||
}
|
||||
window.next.router.replace('${pathname}')
|
||||
})()`)
|
||||
await waitFor(1000)
|
||||
const numErrors = await browser.eval(`window.caughtErrors.length`)
|
||||
expect(numErrors).toBe(0)
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
const didPreload = async pathname => {
|
||||
const browser = await webdriver(appPort, pathname)
|
||||
await waitFor(500)
|
||||
const links = await browser.elementsByCss('link[rel=preload]')
|
||||
let found = false
|
||||
|
||||
for (const link of links) {
|
||||
const href = await link.getAttribute('href')
|
||||
if (href.includes('index')) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(found).toBe(true)
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
describe('Invalid hrefs', () => {
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it('should not show error for functional component with forwardRef', async () => {
|
||||
await noError('/functional')
|
||||
})
|
||||
|
||||
it('should not show error for class component as child of next/link', async () => {
|
||||
await noError('/class')
|
||||
})
|
||||
|
||||
it('should handle child ref with React.createRef', async () => {
|
||||
await noError('/child-ref')
|
||||
})
|
||||
|
||||
it('should handle child ref that is a function', async () => {
|
||||
await noError('/child-ref-func')
|
||||
})
|
||||
})
|
||||
|
||||
describe('production mode', () => {
|
||||
beforeAll(async () => {
|
||||
await nextBuild(appDir)
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it('should preload with forwardRef', async () => {
|
||||
await didPreload('/functional')
|
||||
})
|
||||
|
||||
it('should preload with child ref with React.createRef', async () => {
|
||||
await didPreload('/child-ref')
|
||||
})
|
||||
|
||||
it('should preload with child ref with function', async () => {
|
||||
await didPreload('/child-ref-func')
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue