Navegue mediante programación usando el router de reacción V4

336

Acabo de reemplazar react-routerde v3 a v4.
Pero no estoy seguro de cómo navegar mediante programación en la función miembro de a Component. es decir, en la handleClick()función que quiero navegar /path/some/wheredespués de procesar algunos datos. Solía ​​hacer eso por:

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

Pero no puedo encontrar tales interfaces en v4.
¿Cómo puedo navegar usando v4?

Colin Witkamp
fuente
3
Puede acceder al objeto de historial en v4 a través de accesorios: this.props.history.push ('/')
colemerrick
66
¿Qué pasa si desea acceder desde un lugar diferente que un Component? Por ejemplo, dentro de las acciones redux.
Maximus S
73
a veces me pregunto por qué es tan complicado pasar de un enlace a otro =))
Thai Tran
12
Uno pensaría que hoy en día la redirección no sería tan complicada.
JohnAllen
Esta publicación me pareció útil: medium.com/@anneeb/redirecting-in-react-4de5e517354a
BioData41

Respuestas:

414

Si se dirige a entornos de navegador, debe usar el react-router-dompaquete, en lugar de react-router. Están siguiendo el mismo enfoque que React, para separar el núcleo, ( react) y el código específico de la plataforma, ( react-dom, react-native) con la sutil diferencia de que no necesita instalar dos paquetes separados, por lo que los paquetes de entorno contienen todo necesitas. Puedes agregarlo a tu proyecto como:

yarn add react-router-dom

o

npm i react-router-dom

Lo primero que debe hacer es proporcionar un <BrowserRouter>como el componente principal más alto en su aplicación. <BrowserRouter>usa la historyAPI HTML5 y la administra por usted, por lo que no tiene que preocuparse por instanciarlo usted mismo y pasarlo al <BrowserRouter>componente como accesorio (como lo hizo en versiones anteriores).

En V4, para navegar programáticamente, necesita acceder al historyobjeto, que está disponible a través de React context, siempre que tenga un componente de <BrowserRouter> proveedor como el principal superior en su aplicación. La biblioteca expone a través del contexto el routerobjeto, que en sí mismo contiene historycomo una propiedad. La historyinterfaz ofrece varios métodos de navegación, como push, replacey goBack, entre otros. Puede consultar la lista completa de propiedades y métodos aquí .

Nota importante para los usuarios de Redux / Mobx

Si está utilizando redux o mobx como su biblioteca de administración de estado en su aplicación, es posible que haya encontrado problemas con los componentes que deberían tener en cuenta la ubicación pero que no se vuelven a reproducir después de activar una actualización de URL

Eso sucede porque react-routerpasa locationa los componentes utilizando el modelo de contexto.

Tanto connect como observador crean componentes cuyos métodos shouldComponentUpdate hacen una comparación superficial de sus accesorios actuales y sus próximos accesorios. Esos componentes solo se volverán a renderizar cuando haya cambiado al menos un accesorio. Esto significa que para garantizar que se actualicen cuando cambie la ubicación, deberán recibir un accesorio que cambie cuando cambie la ubicación.

Los 2 enfoques para resolver esto son:

  • Envuelva su componente conectado de una manera sin caminos <Route />. El locationobjeto actual es uno de los accesorios que un <Route>pasa al componente que representa
  • Envuelva su componente conectado con el withRoutercomponente de orden superior, que de hecho tiene el mismo efecto e inyecta locationcomo accesorio

Dejando eso de lado, hay cuatro formas de navegar programáticamente, ordenadas por recomendación:

1.- Usando un <Route>componente

Promueve un estilo declarativo. Antes de v4, los <Route />componentes se colocaban en la parte superior de la jerarquía de componentes, teniendo que pensar de antemano en la estructura de sus rutas. Sin embargo, ahora puede tener <Route>componentes en cualquier parte de su árbol, lo que le permite tener un control más preciso para la representación condicional dependiendo de la URL. Routeinyecta match, locationy historycomo accesorios en su componente. Los métodos de navegación (tales como push, replace, goBack...) están disponibles como propiedades del historyobjeto.

Hay 3 formas de renderizar algo con a Route, usando cualquiera de los dos component, rendero childrenaccesorios, pero no use más de uno en el mismo Route. La elección depende del caso de uso, pero básicamente las dos primeras opciones solo mostrarán su componente si pathcoincide con la ubicación de la URL, mientras que con childrenel componente se mostrará si la ruta coincide con la ubicación o no (útil para ajustar la interfaz de usuario basada en la URL pareo).

Si desea personalizar la salida de representación de componente , es necesario envolver su componente en una función y utiliza la renderopción, a fin de pasar a su componente cualesquiera otros apoyos que usted desea, además de match, locationy history. Un ejemplo para ilustrar:

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.- Usando withRouterHoC

Este componente de orden superior inyectará los mismos accesorios que Route. Sin embargo, conlleva la limitación de que solo puede tener 1 HoC por archivo.

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.- Usando un Redirectcomponente

La representación de a <Redirect>navegará a una nueva ubicación. Pero tenga en cuenta que, de forma predeterminada , la ubicación actual se reemplaza por la nueva, como las redirecciones del lado del servidor (HTTP 3xx). La nueva ubicación es proporcionada por toprop, que puede ser una cadena (URL para redirigir a) o un locationobjeto. Si quieres insertar una nueva entrada en el historial , pasa también un pushaccesorio y configúralo entrue

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

4.- Accediendo routermanualmente a través del contexto

Un poco desanimado porque el contexto sigue siendo una API experimental y es probable que se rompa / cambie en futuras versiones de 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,
  }),
};

No hace falta decir que también hay otros componentes del enrutador destinados a ecosistemas que no son del navegador, como el <NativeRouter>que replica una pila de navegación en la memoria y apunta a la plataforma React Native, disponible a través del react-router-nativepaquete.

Para cualquier referencia adicional, no dude en consultar los documentos oficiales . También hay un video hecho por uno de los coautores de la biblioteca que proporciona una introducción bastante buena al react-router v4, destacando algunos de los principales cambios.

rgommezz
fuente
2
Estoy usando V4 y lo anterior funciona bien. He pasado bastante tiempo revisando el enrutador V4, ya que parece haber algunas opciones extrañas, pero lo anterior definitivamente funciona. Supongo que está importando withRouterdereact-router-dom
Sam Parmenter
3
Estoy importando withRouterde react-router-dom. El history.push cambia la url, pero no parece cargar <Route>hasta que
fuerzo
1
@rauliyohmc Me equivoqué sobre esto. El problema es que tengo <Router> dentro un componente React con el que está decorado @observer, lo que desencadena este problema . La solución es tener @withRouteren cada componente de React.
BreakDS
3
Para aquellos que se encuentran con esta gran respuesta, withRouteres simplemente un HOC que utiliza un Routebajo el capó. Lo que significa que solo usa 3 accesorios, historia, partido y ubicación . En el ejemplo anterior, parece que pushes un accesorio que withRouterse agregaría ButtonToNavigate, pero ese no es el caso. props.history.pushtendría que ser utilizado en su lugar. Espero que esto ayude a otros que estaban un poco confundidos.
vinnymac
39
Guau. browserHistory.push('/path/some/where')simplemente parece mucho más simple. Los autores están tratando de desalentar la programación imperativa, ¡pero a veces simplemente funciona mejor!
lux
149

La forma más fácil de hacerlo:

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

Nota:

  • Es posible que desee pasar history propdel componente primario al componente en el que desea invocar la acción si no está disponible.
Jikku Jose
fuente
10
Uso el enrutador 4.0, pero no hay clave de historial en los accesorios. ¿Cómo puedo arreglarlo?
Nicolas S.Xu
41
Si no tiene this.props.historydisponible en su componente, puede import {withRouter} from 'react-router-dom'y luego export default withRouter(MyComponent)(o const MyComponent = withRouter(...)) e insertará el historyelemento en los accesorios de su componente.
Malvineous
@Malvineous eso es interesante, no sabía sobre esto! Lo intentare!
Jikku Jose
2
Debo estar perdiendo algo fundamental, porque esto funciona muy bien para mí, así que no tengo idea de por qué todas las respuestas requieren explicaciones tan largas.
Joshua Pinter
cómo evitar que se vuelva a montar el componente history.pushy simplemente activar la actualización como cuando hacemos clic en<Link>
Rahul Yadav
55

Tuve un problema similar al migrar a React-Router v4, así que intentaré explicar mi solución a continuación.

No considere esta respuesta como la forma correcta de resolver el problema, imagino que hay una buena posibilidad de que surja algo mejor a medida que React Router v4 se vuelva más maduro y deje beta (incluso puede existir y simplemente no lo descubrí) .

Por contexto, tuve este problema porque ocasionalmente uso Redux-Sagapara cambiar programáticamente el objeto de historial (por ejemplo, cuando un usuario se autentica con éxito).

En los documentos de React Router, eche un vistazo al <Router> componente y verá que tiene la capacidad de pasar su propio objeto de historia a través de un accesorio. Esta es la esencia de la solución - que la oferta del objeto historia a React-Routerpartir de un mundial módulo.

Pasos:

  1. Instale el módulo de historial npm - yarn add history o npm install history --save
  2. crear un archivo llamado history.jsen su App.jscarpeta de nivel (esta era mi preferencia)

    // src/history.js
    
    import createHistory from 'history/createBrowserHistory';
    export default createHistory();`
    
  3. Agregue este objeto de historial a su componente de enrutador de esta manera

    // src/App.js
    
    import history from '../your/path/to/history.js;'
    <Router history={history}>
    // Route tags here
    </Router>
    
  4. Ajuste la URL como antes importando su objeto de historial global:

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

Todo debería estar sincronizado ahora, y también tiene acceso a una forma de configurar el objeto de historial mediante programación y no a través de un componente / contenedor.

Alex Mann
fuente
55
Este cambio funcionó para mí, pero solo porque estoy navegando fuera de un componente. Si estuviera navegando dentro de un componente como el OP, usaría el enfoque sugerido por @rauliyohmc usando los accesorios transmitidos por el Routecomponente.
Dan Ellis
2
este es el enfoque recomendado a partir del
17/08
2
@Spets En mi caso, si uso este tipo de enfoque, el enlace se actualizará correctamente después de la inserción, pero los componentes no se representan correctamente (por ejemplo, después de actualizar el enlace, el componente no se actualiza a menos que fuerce la actualización de la página). ¿Dónde encontró que este es el enfoque recomendado? ¿Algún enlace / fuente?
genial
2
@ScottCoates Lo resolví usando el ejemplo anterior, de hecho, proporcionando el historial como parámetro, pero después de depurar los módulos de nodo por mí mismo. Hay un error común cometido en toda la web, al importar "BrowserHistory as Router", cuando en su lugar hay otro objeto llamado Router en la última versión de react-router-dom. Usar eso en combinación con el historial creado en el ejemplo anterior funciona bien.
genial
2
la url se actualiza, pero la página no se representa en función de la nueva raíz. ¿Alguna solución? ¿Por qué la ruta de activación es tan difícil? ¿La gente que diseña reaccionar está fuera de su mente?
Nicolas S.Xu
43

TL; DR:

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

La respuesta simple y declarativa es que debe usar <Redirect to={URL} push={boolean} />en combinación consetState()

push: boolean: cuando es verdadero, la redirección empujará una nueva entrada al historial en lugar de reemplazar la actual.


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>
    )
  }
}

Ejemplo completo aquí . Lee más aquí .

PD. El ejemplo utiliza los inicializadores de propiedades ES7 + para inicializar el estado. Mira aquí también, si estás interesado.

Lyubomir
fuente
55
Esta debe ser la respuesta aceptada. ¡La solución más simple y elegante! +1 para @lustoykov
Ali Abbas Jaffri
1
También configuré navegar de nuevo a falso en componentDidUpdate ya que mi botón está en un encabezado y de lo contrario solo navegaría una vez.
peterorum
Esto solo funciona si conoce la redirección cuando se carga la página. Si está esperando que regrese una llamada asincrónica (es decir, autenticación con Google o algo así), deberá usar uno de los history.push()métodos.
brittohalloran
En realidad no, todavía puede aprovechar la naturaleza declarativa de reaccionar combinada con el componente <Redirect />. Si la página no se carga, puede recurrir a otra <Redirect />
Lyubomir
@brittohalloran la idea de este método usando el enrutador de reacción adecuado, es asegurarse de usar setState para forzar una nueva representación.
Alguien especial
16

Use useHistoryhook si está usando componentes de función

Puede usar useHistoryhook para obtener historyinstancia.

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

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

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

El useHistorygancho le da acceso a la instancia de historial que puede usar para navegar.

Usar historypropiedad dentro de los componentes de la página

React Router inyecta algunas propiedades, incluidos los historycomponentes de la página.

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

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

Envuelva los componentes secundarios withRouterpara inyectar las propiedades del enrutador

withRouterwrapper inyecta las propiedades del enrutador a los componentes. Por ejemplo, puede usar este contenedor para inyectar el enrutador para cerrar el componente del botón ubicado dentro del menú del usuario.

import { withRouter } from "react-router";

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

export default LogoutButton;
TheMisir
fuente
11

También puede simplemente usar accesorios para acceder al objeto de historial: this.props.history.push('new_url')

usuario1445685
fuente
55
Útil solo cuando el componente desciende directamente del enrutador. Para que no pase el accesorio de historial a cada componente en el que necesita esta funcionalidad.
mwieczorek
3
Si no tiene this.props.historydisponible en su componente, puede import {withRouter} from 'react-router-dom'y luego export default withRouter(MyComponent)(o const MyComponent = withRouter(...)) e insertará el historyelemento en los accesorios de su componente.
Malvineous
9

Paso 1: solo hay una cosa para importar:

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

Paso 2: en tu ruta, pasa el historial:

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

Paso 3: el historial se acepta como parte de los accesorios en el siguiente Componente, por lo que puede simplemente:

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

Eso fue fácil y realmente poderoso.

cdi-meo
fuente
7

Esto funciona:

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

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

export default SomeComponent;
Tostada Kaya
fuente
5

Mi respuesta es similar a la de Alex . No estoy seguro de por qué React-Router hizo esto tan innecesariamente complicado. ¿Por qué debería tener que envolver mi componente con un HoC solo para obtener acceso a lo que es esencialmente global?

De todos modos, si echas un vistazo a cómo se implementaron <BrowserRouter>, es solo una pequeña envoltura alrededor de la historia .

Podemos extraer ese historial para poder importarlo desde cualquier lugar. Sin embargo, el truco es que si está haciendo renderizado del lado del servidor e intenta con importel módulo de historial, no funcionará porque usa API solo para el navegador. Pero eso está bien porque generalmente solo redirigimos en respuesta a un clic o algún otro evento del lado del cliente. Por lo tanto, probablemente esté bien simularlo:

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

Con la ayuda de webpack, podemos definir algunos variables para saber en qué entorno estamos:

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

Y ahora puedes

import history from './history';

De donde sea. Simplemente devolverá un módulo vacío en el servidor.

Si no desea utilizar estos vars mágicos, solo tendrá que hacerlo requireen el objeto global donde sea necesario (dentro de su controlador de eventos). importno funcionará porque solo funciona en el nivel superior.

mpen
fuente
66
maldita sea, lo hicieron tan complicado.
Abhishek
44
Estoy totalmente de acuerdo contigo. esto es tan complicado solo para una navegación
Thai Tran
5

Creo que @rgommezz cubre la mayoría de los casos menos uno que creo que es bastante importante.

// 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");

Esto me permite escribir un servicio simple con acciones / llamadas a las que puedo llamar para hacer la navegación desde cualquier componente que desee sin hacer mucho HoC en mis componentes ...

No está claro por qué nadie ha proporcionado esta solución antes. Espero que ayude, y si ve algún problema con esto, hágamelo saber.

usuario3053247
fuente
4

He estado probando v4 durante unos días y ... ¡hasta ahora me encanta! Simplemente tiene sentido después de un tiempo.

También tenía la misma pregunta y encontré que manejarlo como el siguiente funcionó mejor (y podría ser incluso cómo se pretende). Utiliza estado, un operador ternario y <Redirect>.

En el constructor ()

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

En el render ()

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

En el clickhandler ()

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

Espero eso ayude. Házmelo saber.

jar0m1r
fuente
¿Hay alguna dificultad con este método? En mi proyecto con trabajos solo cuando configuro el estado en el constructor, desde allí puedo redirigir a cualquier página que desee. Pero cuando configuro el estado en el evento (los accesorios cambiaron, por ejemplo), veo el método de renderizado llamado con un nuevo estado, pero la redirección no sucede, veo la misma página
Dmitry Malugin
La trampa es que reemplazará el historial, por lo que no podrá contraatacar, así que, básicamente, esta no es una buena solución.
dannytenaglias
4

Luché con esto por un tiempo, algo tan simple, pero tan complicado, porque ReactJS es una forma completamente diferente de escribir aplicaciones web, ¡es muy extraño para nosotros, las personas mayores!

Creé un componente separado para abstraer el desastre:

// 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
};

Luego agréguelo a su render()método:

<LinkButton className="btn btn-primary" to="/location">
    Button Text
</LinkButton>
mwieczorek
fuente
Encuentro esta solución bastante útil. Estoy copiando el código. Avísame si lo pones en github, te lo atribuiré directamente.
Abhinav Manchanda
3

Como no hay otra forma de lidiar con este diseño horrible, escribí un componente genérico que utiliza el enfoque withRouter HOC . El siguiente ejemplo es envolver un buttonelemento, pero puede cambiar a cualquier elemento en el que pueda hacer clic:

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);

Uso:

<NavButton to="/somewhere">Click me</NavButton>
Rodrigo
fuente
3
this.props.history.push("/url")

Si no ha encontrado this.props.history disponible en su componente, intente esto

import {withRouter} from 'react-router-dom'
export default withRouter(MyComponent)  
Shashwat Gupta
fuente
¡Muchas gracias!
QauseenMZ
1

Como a veces prefiero cambiar las rutas por aplicación y luego por botones, este es un ejemplo de trabajo mínimo que funciona para mí:

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
fuente
0

Para aquellos de ustedes que requieren redirigir antes de iniciar completamente un enrutador usando React Routero React Router Dompuede proporcionar una redirección simplemente accediendo al objeto de historial y presionando un nuevo estado dentro de su construcción app.js. Considera lo siguiente:

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>)
    }


}

Aquí queremos cambiar las Rutas de salida que dependen de un subdominio, al interactuar con el objeto de historial antes de que el componente se procese, podemos redirigir efectivamente sin dejar nuestras rutas intactas.

window.history.pushState('', '', './login');
li x
fuente