Программная навигация с использованием реагирующего маршрутизатора V4

336

Я только что заменил react-routerс v3 на v4.
Но я не уверен, как программно перемещаться в функции-члене a Component. т.е. в handleClick()функции, к которой я хочу перейти /path/some/whereпосле обработки некоторых данных. Я использовал это для:

import { browserHistory } from 'react-router'
browserHistory.push('/path/some/where')

Но я не могу найти такие интерфейсы в v4.
Как я могу ориентироваться с помощью v4?

Колин Виткамп
источник
3
Вы можете получить доступ к объекту истории в v4 через props: this.props.history.push ('/')
colemerrick
6
Что делать, если вы хотите получить к нему доступ из другого места, чем Component? Например, внутри редукционных действий.
Maximus S
73
иногда я задаюсь вопросом, почему так сложно просто перейти с одной ссылки на другую =))
Thai Tran
12
Казалось бы, в наше время перенаправление не было бы таким сложным.
Джон Аллен
Я нашел этот пост полезным: medium.com/@anneeb/redirecting-in-react-4de5e517354a
BioData41

Ответы:

414

Если вы ориентируетесь на среду браузера, вам нужно использовать react-router-domпакет, а не react-router. Они следуют тому же подходу, что и React, чтобы разделить ядро ​​( react) и код, специфичный для платформы, ( react-dom, react-native) с тонким отличием в том, что вам не нужно устанавливать два отдельных пакета, поэтому пакеты среды содержат все тебе нужно. Вы можете добавить его в свой проект как:

yarn add react-router-dom

или

npm i react-router-dom

Первое, что вам нужно сделать, это предоставить <BrowserRouter>самый верхний родительский компонент в вашем приложении. <BrowserRouter>использует HTML5 historyAPI и управляет им за вас, поэтому вам не нужно беспокоиться о том, чтобы создать его экземпляр самостоятельно и передать его <BrowserRouter>компоненту в качестве опоры (как это требовалось в предыдущих версиях).

В V4 для программной навигации вам необходим доступ к historyобъекту, доступному через React context, при условии, что у вас есть компонент <BrowserRouter> провайдера , который является самым верхним родителем в вашем приложении. Библиотека раскрывает через контекст routerобъект, который сам содержит historyсвойство. historyИнтерфейс предлагает несколько методов навигации, такие как push, replaceи goBack, среди прочих. Вы можете проверить весь список свойств и методов здесь .

Важное замечание для пользователей Redux / Mobx

Если вы используете redux или mobx в качестве библиотеки управления состоянием в своем приложении, вы можете столкнуться с проблемами с компонентами, которые должны учитывать местоположение, но не будут повторно отображаться после запуска обновления URL-адреса.

Это происходит потому, что react-routerпереходит locationк компонентам с использованием контекстной модели.

И connect, и наблюдатель создают компоненты, чьи методы shouldComponentUpdate проводят поверхностное сравнение своих текущих и следующих компонентов. Эти компоненты будут перерисовываться только тогда, когда хотя бы одна опора была изменена. Это означает, что для того, чтобы они обновлялись при изменении местоположения, им необходимо предоставить реквизит, который изменяется при изменении местоположения.

2 подхода к решению этой проблемы:

  • Оберните ваш подключенный компонент в без пути <Route />. Текущий locationобъект - это один из реквизитов, <Route>передаваемых компоненту, который он отображает.
  • Оберните подключенный компонент компонентом withRouterболее высокого порядка, который на самом деле имеет тот же эффект и внедряет, locationчто и проп

Помимо этого, существует четыре способа программной навигации, упорядоченные по рекомендации:

1.- Использование <Route>компонента

Это продвигает декларативный стиль. До версии v4 <Route />компоненты размещались в верхней части иерархии компонентов, поэтому необходимо заранее продумать структуру маршрутов. Тем не менее, теперь вы можете иметь <Route>компоненты в любом месте вашего дерева, что позволяет вам лучше контролировать условный рендеринг в зависимости от URL. Routeвнедряет match, locationи historyкак подпирает в ваш компонент. Методы навигации (такие как push, replace, goBack...) доступны как свойства historyобъекта.

Есть 3 способа рендеринга чего-либо с помощью Route, или component, renderили childrenреквизита, но не используйте более одного в одном Route. Выбор зависит от варианта использования, но в основном первые два параметра будут отображать ваш компонент, только если он pathсоответствует расположению URL, тогда как с childrenкомпонентом будет отображаться, соответствует ли путь местоположению или нет (полезно для настройки интерфейса на основе URL соответствия).

Если вы хотите , чтобы настроить выход компонентного рендеринга , вам нужно обернуть компонент в функции и использовать renderопцию для того, чтобы перейти к компоненте любые другие реквизита вы желаете, кроме match, locationи history. Пример для иллюстрации:

import { BrowserRouter as Router } from 'react-router-dom'

const ButtonToNavigate = ({ title, history }) => (
  <button
    type="button"
    onClick={() => history.push('/my-new-location')}
  >
    {title}
  </button>
);

const SomeComponent = () => (
  <Route path="/" render={(props) => <ButtonToNavigate {...props} title="Navigate elsewhere" />} />
)    

const App = () => (
  <Router>
    <SomeComponent /> // Notice how in v4 we can have any other component interleaved
    <AnotherComponent />
  </Router>
);

2.- Использование withRouterHoC

Этот компонент более высокого порядка будет вводить те же реквизиты, что и Route. Тем не менее, это ограничивает то, что вы можете иметь только 1 HoC на файл.

import { withRouter } from 'react-router-dom'

const ButtonToNavigate = ({ history }) => (
  <button
    type="button"
    onClick={() => history.push('/my-new-location')}
  >
    Navigate
  </button>
);


ButtonToNavigate.propTypes = {
  history: React.PropTypes.shape({
    push: React.PropTypes.func.isRequired,
  }),
};

export default withRouter(ButtonToNavigate);

3.- Использование Redirectкомпонента

Рендеринг <Redirect>переместится на новое место. Но имейте в виду, что по умолчанию текущее местоположение заменяется новым, как перенаправления на стороне сервера (HTTP 3xx). Новое местоположение обеспечивается toprop, который может быть строкой (URL-адрес для перенаправления) или locationобъектом. Если вы хотите добавить новую запись в историю , передайте также pushопору и установите ее вtrue

<Redirect to="/your-new-location" push />

4.- Доступ routerвручную через контекст

Немного обескуражен, потому что контекст все еще является экспериментальным API, и он может сломаться / измениться в будущих версиях React

const ButtonToNavigate = (props, context) => (
  <button
    type="button"
    onClick={() => context.router.history.push('/my-new-location')}
  >
    Navigate to a new location
  </button>
);

ButtonToNavigate.contextTypes = {
  router: React.PropTypes.shape({
    history: React.PropTypes.object.isRequired,
  }),
};

Само собой разумеется, есть и другие компоненты Router, предназначенные для не браузерных экосистем, например, <NativeRouter>которые реплицируют стек памяти в памяти и предназначены для платформы React Native, доступной через react-router-nativeпакет.

Для любой дальнейшей ссылки, не стесняйтесь взглянуть на официальные документы . Также есть видео, сделанное одним из соавторов библиотеки, которое представляет довольно интересное введение в Reaction-router v4, выделяя некоторые из основных изменений.

rgommezz
источник
2
Я использую V4 и выше работает отлично. Я потратил немало времени, разбираясь с маршрутизатором V4, так как некоторые странные решения кажутся довольно сложными, но вышеприведенное определенно работает. Я предполагаю, что вы импортируете withRouterизreact-router-dom
Сэм Парментер
3
Я импортирую withRouterиз react-router-dom. History.push действительно изменяет URL, но, похоже, не загружает, <Route>пока я не
заставлю
1
@rauliyohmc Я был неправ по этому поводу. Проблема в том, что у меня <Router> внутри компонента React, который украшен @observer, который вызывает эту проблему . @withRouterОбходной путь должен иметь каждый такой компонент React.
BreakDS
3
Для тех, кто сталкивается с этим великим ответом, withRouterпросто HOC, который использует Routeпод капотом. Это означает, что он использует только 3 реквизита, историю, совпадение и местоположение . В приведенном выше примере кажется, что pushэто опора, withRouterкоторая добавит ButtonToNavigate, но это не так. props.history.pushбудет использоваться вместо этого. Надеюсь, что это помогает другим, которые были немного смущены.
vinnymac
39
Вот это да. browserHistory.push('/path/some/where')просто кажется намного проще. Авторы пытаются препятствовать императивному программированию, но иногда оно просто работает лучше!
люкс
149

Самый простой способ сделать это:

this.props.history.push("/new/url")

Примечание:

  • Возможно, вы захотите передать history propродительский компонент компоненту, для которого вы хотите вызвать действие, если оно недоступно.
Джикку Хосе
источник
10
Я использую 4.0 роутер, но на реквизите нет ключа истории. Как я могу это исправить?
Nicolas S.Xu
41
Если у вас нет this.props.historyдоступных в вашем компоненте, то вы можете, import {withRouter} from 'react-router-dom'а затем export default withRouter(MyComponent)(или const MyComponent = withRouter(...)), и он вставит historyэлемент в реквизиты вашего компонента.
Malvineous
@ Malvineous это интересно, не знал об этом! Попробую!
Джикку Хосе
2
Я, должно быть, упускаю что-то фундаментальное, потому что это прекрасно работает для меня, поэтому я понятия не имею, почему все ответы требуют таких длинных объяснений.
Джошуа Пинтер
как предотвратить перемонтирование компонента history.pushи просто запустить обновление, как когда мы нажимаем<Link>
Rahul Yadav
55

У меня была похожая проблема при переходе на React-Router v4, поэтому я попытаюсь объяснить свое решение ниже.

Пожалуйста, не рассматривайте этот ответ как верный способ решения проблемы, я думаю, есть хороший шанс, что что-то лучшее возникнет, когда React Router v4 станет более зрелым и выйдет из бета-версии (возможно, он уже существует, и я его просто не обнаружил) ,

Для контекста у меня была эта проблема, потому что я иногда использую, Redux-Sagaчтобы программно изменить объект истории (скажем, когда пользователь успешно аутентифицируется).

В документации по React Router посмотрите на <Router> компонент, и вы увидите, что у вас есть возможность передавать свой собственный объект истории через опору. Это суть решения - мы поставляем объект истории , чтобы React-Routerиз глобального модуля.

шаги:

  1. Установите модуль истории npm - yarn add history или npm install history --save
  2. создать файл с именем history.jsв App.jsпапке вашего уровня (это было моим предпочтением)

    // src/history.js
    
    import createHistory from 'history/createBrowserHistory';
    export default createHistory();`
  3. Добавьте этот объект истории в ваш компонент Router следующим образом

    // src/App.js
    
    import history from '../your/path/to/history.js;'
    <Router history={history}>
    // Route tags here
    </Router>
  4. Настройте URL как и прежде, импортируя свой глобальный объект истории:

    import history from '../your/path/to/history.js;'
    history.push('new/path/here/');

Теперь все должно синхронизироваться, и у вас также есть доступ к способу установки объекта истории программно, а не через компонент / контейнер.

Алекс Манн
источник
5
Это изменение сработало для меня, но только потому, что я перемещаюсь за пределы компонента. Если бы я перемещался внутри компонента, такого как OP, я бы использовал подход, предложенный @rauliyohmc, используя реквизиты, передаваемые Routeкомпонентом.
Дэн Эллис
2
это рекомендуемый подход с 17.08.
Спец
2
@Spets В моем случае, если я использую такой подход, ссылка будет обновляться должным образом после отправки, но компоненты не будут отображаться должным образом (например, после обновления ссылки компонент не обновляется, пока вы не принудительно обновите страницу). Где вы обнаружили, что это рекомендуемый подход? Любые ссылки / источники?
классно
2
@ScottCoates Я разобрался, используя приведенный выше пример, на самом деле, предоставив историю в качестве параметра, но после того, как я самостоятельно отладил модули узлов. Существует распространенная ошибка, совершаемая по всему Интернету с использованием импорта «BrowserHistory as Router», когда вместо этого в самой последней версииact-router-dom есть другой объект, называемый Router. Использование этого в сочетании с историей, созданной в примере выше, прекрасно работает.
круто
2
URL обновляется, но страница не отображается на основе нового корня. Любое решение? Почему вызвать маршрут так сложно? Люди, которые проектируют реакцию, не в своем уме?
Nicolas S.Xu
43

TL; DR:

if (navigate) {
  return <Redirect to="/" push={true} />
}

Простой и декларативный ответ заключается в том, что вам нужно использовать <Redirect to={URL} push={boolean} />в сочетании сsetState()

push: boolean - при значении true перенаправление будет помещать новую запись в историю вместо замены текущей.


import { Redirect } from 'react-router'

class FooBar extends React.Component {
  state = {
    navigate: false
  }

  render() {
    const { navigate } = this.state

    // here is the important part
    if (navigate) {
      return <Redirect to="/" push={true} />
    }
   // ^^^^^^^^^^^^^^^^^^^^^^^

    return (
      <div>
        <button onClick={() => this.setState({ navigate: true })}>
          Home
        </button>
      </div>
    )
  }
}

Полный пример здесь . Узнайте больше здесь .

PS. В примере используются инициализаторы свойств ES7 + для инициализации состояния. Посмотри здесь , если тебе интересно.

Любомир
источник
5
Это должен быть принят ответ. Самое простое элегантное решение! +1 для @lustoykov
Али Аббас Джаффри
1
Я также установил навигацию обратно в false в componentDidUpdate, так как моя кнопка находится в заголовке и в противном случае будет перемещаться только один раз.
peterorum
Это работает, только если вы знаете о перенаправлении, когда страница загружается. Если вы ожидаете асинхронного вызова для возврата (т.е. аутентификации с помощью Google или чего-то еще), вам придется использовать один из history.push()методов.
Бриттохаллоран
Не совсем, вы все равно можете использовать декларативный характер реакции в сочетании с компонентом <Redirect />. Если страница не загружается, вы можете перейти на другой <Redirect />
Любомир
@brittohalloran Идея этого метода, использующего правильный реагирующий маршрутизатор, состоит в том, чтобы убедиться, что вы используете setState для принудительного повторного рендеринга.
Кто-то
16

Используйте useHistoryхук, если вы используете функциональные компоненты

Вы можете использовать useHistoryкрюк, чтобы получить historyэкземпляр.

import { useHistory } from "react-router-dom";

const MyComponent = () => {
  var history = useHistory();

  return (
    <button onClick={() => history.push("/about")}>
      Click me
    </button>
  );
}

useHistoryКрюк дает вам доступ к экземпляру истории , которые вы можете использовать для навигации.

Использовать historyсвойство внутри компонентов страницы

React Router добавляет некоторые свойства, в том числе historyк компонентам страницы.

class HomePage extends React.Component {
  render() {
    const { history } = this.props;

    return (
      <div>
        <button onClick={() => history.push("/projects")}>
          Projects
        </button>
      </div>
    );
  }
}

Оберните дочерние компоненты, withRouterчтобы добавить свойства маршрутизатора

withRouterОболочка вводит свойства маршрутизатора в компоненты. Например, вы можете использовать эту оболочку, чтобы внедрить маршрутизатор в компонент кнопки выхода из пользовательского меню.

import { withRouter } from "react-router";

const LogoutButton = withRouter(({ history }) => {
  return (
    <button onClick={() => history.push("/login")}>
      Logout
    </button>
  );
});

export default LogoutButton;
TheMisir
источник
11

Вы также можете просто использовать реквизит для доступа к объекту истории: this.props.history.push('new_url')

user1445685
источник
5
Полезно только в том случае, если в компоненте непосредственно спущен маршрутизатор. Чтобы вы не передавали реквизиты истории каждому компоненту, в котором вам нужна эта функциональность.
mwieczorek
3
Если у вас нет this.props.historyдоступных в вашем компоненте, то вы можете, import {withRouter} from 'react-router-dom'а затем export default withRouter(MyComponent)(или const MyComponent = withRouter(...)), и он вставит historyэлемент в реквизиты вашего компонента.
Malvineous
9

Шаг 1: сверху можно импортировать только одну вещь:

import {Route} from 'react-router-dom';

Шаг 2: В вашем маршруте передайте историю:

<Route
  exact
  path='/posts/add'
  render={({history}) => (
    <PostAdd history={history} />
  )}
/>

Шаг 3: история будет принята как часть реквизита в следующем Компоненте, так что вы можете просто:

this.props.history.push('/');

Это было легко и действительно мощно.

КДИ-MeO
источник
7

Это работает:

import { withRouter } from 'react-router-dom';

const SomeComponent = withRouter(({ history }) => (
    <div onClick={() => history.push('/path/some/where')}>
        some clickable element
    </div>); 
);

export default SomeComponent;
Тост кая
источник
5

Мой ответ похож на ответ Алекса . Я не уверен, почему React-Router сделал это настолько излишне сложным. Почему я должен обернуть свой компонент с помощью HoC, чтобы получить доступ к тому, что по сути является глобальным?

В любом случае, если вы посмотрите, как они реализованы <BrowserRouter>, это всего лишь крошечная оболочка истории .

Мы можем немного извлечь эту историю, чтобы импортировать ее из любого места. Однако хитрость заключается в том, что если вы выполняете рендеринг на стороне сервера и пытаетесь importиспользовать модуль истории, он не будет работать, поскольку он использует API-интерфейсы только для браузера. Но это нормально, потому что мы обычно перенаправляем только в ответ на щелчок или какое-либо другое клиентское событие. Таким образом, это нормально, чтобы подделать это:

// history.js
if(__SERVER__) {
    module.exports = {};
} else {
    module.exports = require('history').createBrowserHistory();
}

С помощью веб-пакета мы можем определить некоторые переменные, чтобы мы знали, в какой среде мы находимся:

plugins: [
    new DefinePlugin({
        '__SERVER__': 'false',
        '__BROWSER__': 'true', // you really only need one of these, but I like to have both
    }),

И теперь вы можете

import history from './history';

Откуда угодно. Он просто вернет пустой модуль на сервер.

Если вы не хотите использовать эти магические переменные, вам просто нужно requireв глобальном объекте, где он необходим (внутри вашего обработчика событий). importне будет работать, потому что он работает только на верхнем уровне.

mpen
источник
6
Черт, они сделали это так сложно.
Абхишек
4
Я полностью с тобой согласен. это так сложно просто для навигации
Thai Tran
5

Я думаю, что @rgommezz охватывает большинство случаев, за исключением одного, который, я думаю, очень важен.

// history is already a dependency or React Router, but if don't have it then try npm install save-dev history

import createHistory from "history/createBrowserHistory"

// in your function then call add the below 
const history = createHistory();
// Use push, replace, and go to navigate around.
history.push("/home");

Это позволяет мне написать простой сервис с действиями / вызовами, которые я могу вызвать для навигации по любому компоненту, который я хочу, не делая много HoC для моих компонентов ...

Непонятно, почему никто не предоставил это решение раньше. Я надеюсь, что это поможет, и если вы обнаружите какие-либо проблемы с этим, пожалуйста, дайте мне знать.

user3053247
источник
4

Я уже несколько дней тестирую v4 и .. пока что мне это нравится! Это просто имеет смысл через некоторое время.

У меня также был тот же вопрос, и я обнаружил, что обработка его такова, что следующее сработало лучше (и, возможно, даже так, как задумано) Он использует состояние, троичный оператор и <Redirect>.

В конструкторе ()

this.state = {
    redirectTo: null
} 
this.clickhandler = this.clickhandler.bind(this);

В рендере ()

render(){
    return (
        <div>
        { this.state.redirectTo ?
            <Redirect to={{ pathname: this.state.redirectTo }} /> : 
            (
             <div>
               ..
             <button onClick={ this.clickhandler } />
              ..
             </div>
             )
         }

В обработчике кликов ()

 this.setState({ redirectTo: '/path/some/where' });

Надеюсь, поможет. Дай мне знать.

jar0m1r
источник
Есть ли подводные камни с этим методом? В моем проекте с работает только когда я устанавливаю состояние в конструкторе, оттуда я могу перенаправить на любую страницу, которую я хочу. Но когда я устанавливаю состояние для события (например, изменился реквизит), я вижу метод рендеринга, вызванный с новым состоянием, но перенаправление не происходит, я вижу ту же страницу
Дмитрий Малугин
Подводный камень в том, что он заменит историю, поэтому вы не сможете нанести ответный удар - поэтому, по сути, это не очень хорошее решение.
dannytenaglias
4

Некоторое время я боролся с этим - чем-то таким простым, но таким сложным, потому что ReactJS - это совершенно другой способ написания веб-приложений, он очень чужд для нас, пожилых людей!

Я создал отдельный компонент, чтобы абстрагироваться от беспорядка:

// LinkButton.js

import React from "react";
import PropTypes from "prop-types";
import {Route} from 'react-router-dom';

export default class LinkButton extends React.Component {

    render() {
        return (
            <Route render={({history}) => (
                <button {...this.props}
                       onClick={() => {
                           history.push(this.props.to)
                       }}>
                    {this.props.children}
                </button>
            )}/>
        );
    }
}

LinkButton.propTypes = {
    to: PropTypes.string.isRequired
};

Затем добавьте его в ваш render()метод:

<LinkButton className="btn btn-primary" to="/location">
    Button Text
</LinkButton>
mwieczorek
источник
Я считаю это решение весьма полезным. Я копирую код. Дайте мне знать, что вы положили его на github - я вам напрямую приписываю.
Абхинав Манчанда
3

Поскольку нет другого способа справиться с этим ужасным дизайном, я написал общий компонент, который использует подход withRouter HOC . В приведенном ниже примере оборачивается buttonэлемент, но вы можете изменить его на любой элемент, который можно щелкнуть:

import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';

const NavButton = (props) => (
  <Button onClick={() => props.history.push(props.to)}>
    {props.children}
  </Button>
);

NavButton.propTypes = {
  history: PropTypes.shape({
    push: PropTypes.func.isRequired
  }),
  to: PropTypes.string.isRequired
};

export default withRouter(NavButton);

Использование:

<NavButton to="/somewhere">Click me</NavButton>
Родриго
источник
3
this.props.history.push("/url")

Если вы не нашли this.props.history в вашем компоненте, попробуйте это

import {withRouter} from 'react-router-dom'
export default withRouter(MyComponent)  
Шашват Гупта
источник
Спасибо!
QauseenMZ
1

Поскольку иногда я предпочитаю переключать маршруты по Приложению, а не по кнопкам, это минимальный рабочий пример, который работает для меня:

import { Component } from 'react'
import { BrowserRouter as Router, Link } from 'react-router-dom'

class App extends Component {
  constructor(props) {
    super(props)

    /** @type BrowserRouter */
    this.router = undefined
  }

  async handleSignFormSubmit() {
    await magic()
    this.router.history.push('/')
  }

  render() {
    return (
      <Router ref={ el => this.router = el }>
        <Link to="/signin">Sign in</Link>
        <Route path="/signin" exact={true} render={() => (
          <SignPage onFormSubmit={ this.handleSignFormSubmit } />
        )} />
      </Router>
    )
  }
}
piotr_cz
источник
0

Для тех из вас, кто требует перенаправления перед полной инициализацией маршрутизатора, используя React Routerили, React Router DomВы можете предоставить перенаправление, просто получив доступ к объекту истории и выдвинув новое состояние в него в своей конструкции app.js. Учтите следующее:

function getSubdomain(hostname) {
    let regexParse = new RegExp('[a-z\-0-9]{2,63}\.[a-z\.]{2,5}$');
    let urlParts = regexParse.exec(hostname);
    return hostname.replace(urlParts[0], '').slice(0, -1);
}

class App extends Component {

    constructor(props) {
        super(props);


        this.state = {
            hostState: true
        };

        if (getSubdomain(window.location.hostname).length > 0) {
            this.state.hostState = false;
            window.history.pushState('', '', './login');
        } else {
            console.log(getSubdomain(window.location.hostname));
        }

    }


    render() {
        return (

            <BrowserRouter>
                {this.state.hostState ? (
                    <div>
                        <Route path="/login" component={LoginContainer}/>
                        <Route path="/" component={PublicContainer}/>
                    </div>
                ) : (
                    <div>
                        <Route path="/login" component={LoginContainer}/>
                    </div>
                )

                }
            </BrowserRouter>)
    }


}

Здесь мы хотим изменить выходные Маршруты, зависящие от субдомена, взаимодействуя с объектом истории до того, как компонент отобразит, мы можем эффективно перенаправить, оставляя наши маршруты в такте.

window.history.pushState('', '', './login');
li x
источник