Skip to content

Commit

Permalink
Treat all navigation as a side effect
Browse files Browse the repository at this point in the history
- Removes <Redirect>
- Removes support for { redirectTo } in useRoutes()
- Warns if navigate() is used during the initial render
- Warns when <Navigate> is used on the initial render in <StaticRouter>

This began as a discussion on GitHub (see #7203). Essentially we need to
treat all navigation as a side effect, which means it needs to happen in
a useEffect. If people need v5's <Redirect> functionality (when it was
used in the route config inside a <Switch>), they either need to a) do
the redirect on the server (the best solution) or b) render a <Navigate>
on the initial render. However, (b) will only show the new route on the
subsequent render.

This should also eliminate warnings in the dev-experimental builds.

Closes #7203
Fixes #7199
  • Loading branch information
mjackson committed Apr 3, 2020
1 parent f59ee54 commit cbcd398
Show file tree
Hide file tree
Showing 15 changed files with 137 additions and 408 deletions.
2 changes: 0 additions & 2 deletions docs/advanced-guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ build a certain capability or feature of your app with React Router.
- [Using Layouts](#TODO)
- ["Not Found" Routes (404)](#TODO)
- [Using Route Config Objects](#TODO)
- [Using Redirects](#TODO)

## Common User Flows

Expand All @@ -31,7 +30,6 @@ build a certain capability or feature of your app with React Router.

- [Creating Routes from Files](#TODO)
- [Using `StaticRouter` Directly](#TODO)
- [Handling Redirects](#TODO)

## Testing

Expand Down
14 changes: 7 additions & 7 deletions docs/advanced-guides/migrating-5-to-6.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ In general, the process looks like this:
- Consolidate your `<Route>`s into a nested config (optional)
- [Use `navigate` instead of `history`](#use-navigate-instead-of-history)
- Use `useNavigate` hook instead of `useHistory`
- Use `<Navigate>` instead of `<Redirect>` (outside of route configs)
- No need to change `<Redirect>` directly inside `<Routes>`
- Use `<Navigate>` instead of `<Redirect>`
- [Use `useRoutes` instead of `react-router-config`](#use-useroutes-instead-of-react-router-config)
- [Rename `<Link component>` to `<Link as>`](#rename-link-component-to-link-as)
- [Get `StaticRouter` from `react-router-dom/server`](#get-staticrouter-from-react-router-domserver)
Expand Down Expand Up @@ -521,8 +520,6 @@ function App() {
{ path: 'sent', element: <SentInvoices /> }
]
},
// Redirects use a redirectTo property to
{ path: 'home', redirectTo: '/' },
// Not found routes work as you'd expect
{ path: '*', element: <NotFound /> }
]);
Expand Down Expand Up @@ -657,9 +654,12 @@ The `navigate` API is aware of the internal pending transition state and will
do a REPLACE instead of a PUSH onto the history stack, so the user doesn't end
up with pages in their history that never actually loaded.

*Note: You should still use a `<Redirect>` as part of your route config
(inside a `<Routes>`). This change is only necessary for `<Redirect>`s that are
used to navigate in response to user interaction.*
*Note: The `<Redirect>` element from v5 is no longer supported as part of your
route config (inside a `<Routes>`). This is due to upcoming changes in React
that make it unsafe to alter the state of the router during the initial render.
If you need to redirect immediately, you can either a) do it on your server
(probably the best solution) or b) render a `<Navigate>` element in your route
component. However, recognize that the navigation will happen in a `useEffect`.*

Aside from suspense compatibility, `navigate`, like `Link`, supports relative
navigation. For example:
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced-guides/testing/testing-with-mocha.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

TODO

## Testing Routes and Redirects
## Testing Routes

TODO

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

TODO

## Testing Routes and Redirects
## Testing Routes

TODO

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

TODO

## Testing Routes and Redirects
## Testing Routes

TODO

Expand Down
5 changes: 0 additions & 5 deletions docs/main-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ We will cover the following topics:
- [Params and Search (Query) Strings](#params-and-search-query-strings)
- [Routing and Navigation](#routing-and-navigation)
- [Layouts and Nesting](#layouts-and-nesting)
- [Redirects](#redirects)
- [Transitions](#transitions)
- [Prompts](#prompts)

Expand All @@ -39,10 +38,6 @@ TODO

TODO

### Redirects

TODO

### Transitions

TODO
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ function ProductLayout() {
```

- Links
- Redirects (main uses are for preserving old URLs and doing auth)
- Redirects (after auth)
- Descendant `<Routes>` (rendered somewhere further down the component tree)
12 changes: 10 additions & 2 deletions packages/react-router-dom/__tests__/navigate-encode-params-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ describe('navigate with params', () => {
it('correctly encodes the param in the URL and decodes the param when it is used', () => {
function Start() {
let navigate = useNavigate();
navigate('/blog/react router');

React.useEffect(() => {
navigate('/blog/react router');
});

return null;
}

Expand Down Expand Up @@ -55,7 +59,11 @@ describe('navigate with params', () => {
it('does not alter the param encoding in the URL and decodes the param when it is used', () => {
function Start() {
let navigate = useNavigate();
navigate('/blog/react+router');

React.useEffect(() => {
navigate('/blog/react+router');
});

return null;
}

Expand Down
192 changes: 22 additions & 170 deletions packages/react-router-dom/__tests__/static-navigate-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,178 +4,30 @@ import { Navigate, Routes, Route } from 'react-router-dom';
import { StaticRouter as Router } from 'react-router-dom/server';

describe('A <Navigate> in a <StaticRouter>', () => {
describe('a replace navigation', () => {
it('mutates the context object', () => {
let context = {};

function Home() {
return <Navigate replace to="/somewhere-else?the=query" />;
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(context).toMatchObject({
url: '/somewhere-else?the=query',
state: undefined
});
});

describe('with an object to prop', () => {
it('mutates the context object', () => {
let context = {};

function Home() {
return (
<Navigate
replace
to={{ pathname: '/somewhere-else', search: '?the=query' }}
/>
);
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(context).toMatchObject({
url: '/somewhere-else?the=query',
state: undefined
});
});

it('reuses pieces of the current location that are missing from the to value', () => {
let context = {};

function Home() {
// only use search here, so pathname should be reused
return <Navigate replace to={{ search: '?the=query' }} />;
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(context).toMatchObject({
url: '/home?the=query',
state: undefined
});
});
});
let consoleWarn;
beforeEach(() => {
consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
});

describe('a push navigation', () => {
let consoleWarn;
beforeEach(() => {
consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
consoleWarn.mockRestore();
});

it('issues a warning', () => {
let context = {};

function Home() {
return <Navigate push to="/somewhere-else?the=query" />;
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining('cannot perform a PUSH with a <StaticRouter>')
);
});

it('mutates the context object', () => {
let context = {};

function Home() {
return <Navigate push to="/somewhere-else?the=query" />;
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(context).toMatchObject({
url: '/somewhere-else?the=query',
state: undefined
});
});

describe('with an object to prop', () => {
it('mutates the context object', () => {
let context = {};

function Home() {
return (
<Navigate
push
to={{ pathname: '/somewhere-else', search: '?the=query' }}
/>
);
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(context).toMatchObject({
url: '/somewhere-else?the=query',
state: undefined
});
});

it('reuses pieces of the current location that are missing from the to value', () => {
let context = {};

function Home() {
// only use search here, so pathname should be reused
return <Navigate push to={{ search: '?the=query' }} />;
}

ReactDOMServer.renderToStaticMarkup(
<Router context={context} location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);
afterEach(() => {
consoleWarn.mockRestore();
});

expect(context).toMatchObject({
url: '/home?the=query',
state: undefined
});
});
});
it('warns about using on the initial render', () => {
function Home() {
return <Navigate to="/somewhere-else?the=query" />;
}

ReactDOMServer.renderToStaticMarkup(
<Router location="/home">
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Router>
);

expect(consoleWarn).toHaveBeenCalledWith(
expect.stringMatching('not be rendered on the initial render')
);
});
});
44 changes: 0 additions & 44 deletions packages/react-router-dom/__tests__/static-redirect-test.js

This file was deleted.

2 changes: 0 additions & 2 deletions packages/react-router-dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
MemoryRouter,
Navigate,
Outlet,
Redirect,
Route,
Router,
Routes,
Expand Down Expand Up @@ -49,7 +48,6 @@ export {
MemoryRouter,
Navigate,
Outlet,
Redirect,
Route,
Router,
Routes,
Expand Down

0 comments on commit cbcd398

Please sign in to comment.