Skip to content

Commit

Permalink
Introduce useParams and useSearchParams hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
santino committed Nov 20, 2022
1 parent 1badc3d commit 364fb83
Show file tree
Hide file tree
Showing 12 changed files with 1,237 additions and 748 deletions.
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Performant routing embracing React [Concurrent UI patterns](https://it.reactjs.o
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Overview](#overview)
- [Accessibility](#accessibility)
- [More info on performance](#more-info-on-performance)
Expand All @@ -24,6 +23,8 @@ Performant routing embracing React [Concurrent UI patterns](https://it.reactjs.o
- [useRouter](#userouter)
- [useNavigation](#usenavigation)
- [useHistory](#usehistory)
- [useParams](#useparams)
- [useSearchParams](#usesearchparams)
- [useBeforeRouteLeave](#usebeforerouteleave)
- [Redirect rules](#redirect-rules)
- [Group routes](#group-routes)
Expand Down Expand Up @@ -435,7 +436,7 @@ export default router
Hopefully, this illustrates the power of the router when it comes to data prefetching, as well as the full customisation opportunity, should you need it.

## Hooks
React Concurrent Router currently provides four different hooks.
React Concurrent Router provides the following hooks.

### useRouter
```js
Expand Down Expand Up @@ -505,6 +506,61 @@ Returns an object with the following properties that provides information about
- `index`: only provided by Memory Router; current index in the history stack
- `entries`: only provided by Memory Router; all entries available in history instance

### useParams
```js
import useParams from 'react-concurrent-router/useParams'

const MyComponent = () => {
const { foo, bar, baz } = useParams()

return (
<>
<div>the value for the "foo" param is: ${foo}</div>
<div>the value for the "bar" param is: ${bar}</div>
<div>the value for the "baz" param is: ${baz}</div>
<>
)
}
```
Returns an object with the key/value pairs for all params of the current URL; including named, query and hash params.
For instance assuming the URL rendering the component above is `/home/fooValue?bar=barValue#baz=bazValue`, where the route for the URL is `/home/:foo` (hence `foo` being a named parameter); the component would return the following content:
```txt
the value for the "foo" param is: fooValue
the value for the "bar" param is: barValue
the value for the "baz" param is: bazValue
```

### useSearchParams
```js
import useSearchParams from 'react-concurrent-router/useSearchParams'

const MyComponent = () => {
const [searchParams, setSearchParams] = useSearchParams()

return (
<>
<div>current search params object is: ${JSON.stringify(searchParams)}</div>
<button onClick={() => setSearchParams({ quux: 'corge' })}>
replaceSearchParams
</button>
<button
onClick={() =>
setSearchParams(currentParams => ({
...currentParams,
quux: 'corge'
}))
}
>
mergeSearchParms
</button>
<>
)
}
```
Like the popular `useState` hook from React, this hook returns an array with the following two items:
- `searchParams`: an object containing the key/value pairs of the query params available in the URL for the current location
- `setSearchParams`: a function to set new query parameters in the URL for the current location. Like React's `useState` this function can take an object which sets new query parameters (overriding the existing ones); or a function receiving the current query parameters, as the only argument, and returning an object which ultimately sets new query parameters (useful when you want to compute new parameters from current parameters, or merge current and new parameters)

### useBeforeRouteLeave
I consider this a bonus hook which hopefully will remove any effort and overhead when you want to have some degree of control to prevent your users from accidentally leaving the page they are in; for example in cases that would cause loss of data entered on the page without submitting.
```js
Expand Down
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-concurrent-router",
"version": "1.2.1",
"version": "1.3.0",
"description": "Performant routing embracing React concurrent UI patterns",
"author": "Santino Puleio",
"license": "MIT",
Expand Down Expand Up @@ -62,7 +62,7 @@
},
{
"path": "./dist/umd/react-concurrent-router.production.min.js",
"maxSize": "14.5 Kb",
"maxSize": "15 Kb",
"compression": "none"
}
]
Expand Down Expand Up @@ -95,43 +95,43 @@
},
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.27.0",
"@babel/core": "^7.18.10",
"@babel/eslint-parser": "^7.18.9",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/core": "^7.20.2",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-proposal-object-rest-spread": "^7.20.2",
"@babel/plugin-transform-object-assign": "^7.18.6",
"@babel/plugin-transform-react-constant-elements": "^7.18.12",
"@babel/plugin-transform-runtime": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/plugin-transform-react-constant-elements": "^7.20.2",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/runtime": "^7.18.9",
"@babel/runtime": "^7.20.1",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-node-resolve": "^13.3.0",
"@testing-library/dom": "^8.17.1",
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@types/react": "^18.0.17",
"babel-jest": "^28.1.3",
"@testing-library/react": "^13.4.0",
"@types/react": "^18.0.25",
"babel-jest": "^29.3.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bundlewatch": "^0.3.3",
"cherry-pick": "^0.5.0",
"coveralls": "^3.1.1",
"cross-env": "^7.0.3",
"doctoc": "^2.2.0",
"doctoc": "^2.2.1",
"dtslint": "^4.2.1",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"lint-staged": "^13.0.3",
"open-cli": "^7.0.1",
"open-cli": "^7.1.0",
"prettier-standard": "^16.4.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"rollup": "^2.78.0",
"rollup": "^2.79.1",
"rollup-plugin-terser": "^7.0.2",
"standard": "^17.0.0",
"typescript": "^4.7.4"
"typescript": "^4.9.3"
},
"peerDependencies": {
"@babel/runtime": "^7.11.0",
Expand Down
8 changes: 6 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const configBase = {
useBeforeRouteLeave: 'src/useBeforeRouteLeave',
useHistory: 'src/useHistory',
useNavigation: 'src/useNavigation',
useParams: 'src/useParams',
useRouter: 'src/useRouter',
useSearchParams: 'src/useSearchParams',
RouterProvider: 'src/RouterProvider',
RouteRenderer: 'src/RouteRenderer',
Link: 'src/Link'
Expand Down Expand Up @@ -48,7 +50,8 @@ const bundles = {
external: configBase.external,
plugins: [babel(configBase.babelConfig)]
},
{ // minified build only used to monitor bundlesize; will not be published
{
// minified build only used to monitor bundlesize; will not be published
input: 'dist/cjs/index.js',
output: {
file: 'dist/cjs/index.min.js'
Expand All @@ -75,7 +78,8 @@ const bundles = {
external: configBase.external,
plugins: [babel(configBase.babelConfig)]
},
{ // minified build only used to monitor bundlesize; will not be published
{
// minified build only used to monitor bundlesize; will not be published
input: 'dist/esm/index.js',
output: {
file: 'dist/esm/index.min.js'
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import SuspendableResource from '../SuspendableResource.js'
import useBeforeRouteLeave from '../useBeforeRouteLeave.js'
import useHistory from '../useHistory.js'
import useNavigation from '../useNavigation.js'
import useParams from '../useParams.js'
import useRouter from '../useRouter.js'
import useSearchParams from '../useSearchParams.js'
import RouterProvider from '../RouterProvider.js'
import RouteRenderer from '../RouteRenderer.js'
import Link from '../Link.js'
Expand All @@ -21,8 +23,10 @@ describe('index', () => {
SuspendableResource: SuspendableResource,
useBeforeRouteLeave: useBeforeRouteLeave,
useHistory: useHistory,
useParams: useParams,
useNavigation: useNavigation,
useRouter: useRouter,
useSearchParams: useSearchParams,
RouterProvider: RouterProvider,
RouteRenderer: RouteRenderer,
Link: Link
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/useParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useContext } from 'react'
import RouterContext from '../RouterContext'
import useParams from '../useParams'

jest.mock('react')
useContext.mockImplementation(() => ({
get: jest.fn().mockReturnValue({ params: { foo: 'bar', baz: 'qux' } })
}))

describe('useParams', () => {
it('exports an object with expected route params from RouterContext', () => {
const params = useParams()

expect(useContext).toHaveBeenCalledTimes(1)
expect(useContext).toHaveBeenCalledWith(RouterContext)
expect(params).toEqual(expect.any(Object))
expect(params).toEqual({
foo: 'bar',
baz: 'qux'
})
})
})
108 changes: 108 additions & 0 deletions src/__tests__/useSearchParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom/extend-expect'
import React, { useContext } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'

import RouterContext from '../RouterContext'
import useSearchParams from '../useSearchParams'

jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn()
}))
const mockHistoryPush = jest.fn()
useContext.mockImplementation(() => ({
history: {
location: {
pathname: '/home',
search: '?foo=bar&baz=qux',
hash: '',
state: null,
key: 'siezotjq'
},
push: mockHistoryPush
}
}))

const ExampleComponent = () => {
const [searchParams, setSearchParams] = useSearchParams()

return (
<>
<div>searchParams value: "{JSON.stringify(searchParams)}"</div>
<button onClick={() => setSearchParams({ quux: 'corge' })}>
replaceSearchParams
</button>
<button
onClick={() =>
setSearchParams(currentParams => ({
...currentParams,
quux: 'corge'
}))
}
>
mergeSearchParams
</button>
</>
)
}

describe('useSearchParams', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('returns current searchParams correctly', () => {
render(<ExampleComponent />)

expect(screen.queryByText(/searchParams value:/)).toHaveTextContent(
'searchParams value: "{"foo":"bar","baz":"qux"}"'
)

expect(mockHistoryPush).not.toHaveBeenCalled()
})

it('overrides searchParams correctly', () => {
render(<ExampleComponent />)

fireEvent.click(screen.getByRole('button', { name: 'replaceSearchParams' }))

waitFor(() =>
expect(screen.queryByText(/searchParams value:/)).toHaveTextContent(
'searchParams value: "{"quux":"corge"}"'
)
)

expect(mockHistoryPush).toHaveBeenCalledTimes(1)
expect(mockHistoryPush).toHaveBeenCalledWith(
{
pathname: '/home',
search: '?quux=corge'
},
null
)
})

it('merges searchParams correctly', () => {
render(<ExampleComponent />)

fireEvent.click(screen.getByRole('button', { name: 'mergeSearchParams' }))

waitFor(() =>
expect(screen.queryByText(/searchParams value:/)).toHaveTextContent(
'searchParams value: "{"foo":"bar","baz":"qux","quux":"corge"}"'
)
)

expect(mockHistoryPush).toHaveBeenCalledTimes(1)
expect(mockHistoryPush).toHaveBeenCalledWith(
{
pathname: '/home',
search: '?baz=qux&foo=bar&quux=corge'
},
null
)
})
})
2 changes: 1 addition & 1 deletion src/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('utils', () => {
qux: ['quux', 'quuz'],
baz: 'qux'
})
).toBe('?baz=qux&corge=grault&foo=bar&qux=quux,quuz')
).toBe('?baz=qux&corge=grault&foo=bar&qux=quux&qux=quuz')
})
})

Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export { default as SuspendableResource } from './SuspendableResource'
export { default as useBeforeRouteLeave } from './useBeforeRouteLeave'
export { default as useHistory } from './useHistory'
export { default as useNavigation } from './useNavigation'
export { default as useParams } from './useParams'
export { default as useRouter } from './useRouter'
export { default as useSearchParams } from './useSearchParams'

export { default as RouterProvider } from './RouterProvider'
export { default as RouteRenderer } from './RouteRenderer'
Expand Down
11 changes: 11 additions & 0 deletions src/useParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext } from 'react'
import RouterContext from './RouterContext'

const useParams = () => {
const { get } = useContext(RouterContext)
const routeEntry = get()

return routeEntry.params
}

export default useParams
Loading

0 comments on commit 364fb83

Please sign in to comment.