por Michael Sargent

Una biblioteca de enrutamiento es un componente clave de cualquier aplicación compleja de una sola página. Si desarrolla aplicaciones web con React y Redux, es probable que haya utilizado, o al menos haya oído hablar de React Router. Es una biblioteca de enrutamiento bien conocida para React, y una gran solución para muchos casos de uso.

Pero el router React no es la única solución viable en el ecosistema React/Redux. De hecho, hay toneladas de soluciones de enrutamiento creadas para React y Redux, cada una con diferentes API, características y objetivos, y la lista no hace más que crecer. No hace falta decir que el enrutamiento del lado del cliente no desaparecerá pronto, y todavía hay mucho espacio para el diseño en las bibliotecas de enrutamiento del mañana.

Hoy, quiero llamar su atención sobre el tema del enrutamiento en Redux. Presentaré y presentaré un caso para Redux – first routing – un paradigma que hace de Redux la estrella del modelo de enrutamiento y el hilo común entre muchas soluciones de enrutamiento Redux. Demostraré cómo armar el núcleo de la API independiente del marco en menos de 100 líneas de código, antes de explorar opciones para el uso en el mundo real con React y otros marcos de interfaz.

Un Poco de Historia

En el navegador, la ubicación (información de URL) y el historial de sesiones (una pila de ubicaciones visitadas por la pestaña del navegador actual) se almacenan en el objeto global window. Son accesibles a través de:

  • window.location (API de Ubicación)
  • window.history (API de historial).

La API de historial ofrece los siguientes métodos de navegación de historial, que destacan por su capacidad para actualizar el historial y la ubicación del navegador sin necesidad de recargar la página:

  • pushState(href) — introduce una nueva ubicación en la pila de historial
  • replaceState(href) — sobrescribe la ubicación actual en la pila
  • back() — navega a la ubicación anterior en la pila
  • forward() — navega a la siguiente ubicación de la pila
  • go(index) — navega a una ubicación en la pila, en cualquier dirección.

Juntas, las API de Historial y ubicación permiten el moderno paradigma de enrutamiento del lado del cliente conocido como enrutamiento pushState, el primer protagonista de nuestra historia.

Ahora, es casi un delito mencionar las API de Historial y ubicación sin mencionar una biblioteca de envoltorios moderna como history.

ReactTraining / historial
Administrar el historial de sesiones con JavaScriptgithub.com

history proporciona una API sencilla pero potente para interactuar con el historial y la ubicación del navegador, a la vez que cubre las inconsistencias entre las diferentes implementaciones del navegador. Se usa como una dependencia interna o de pares en muchas bibliotecas de enrutamiento modernas, y haré múltiples referencias a él a lo largo de este artículo.

Enrutamiento Redux y pushState

El segundo protagonista de nuestra historia es Redux. Es 2017, así que te ahorraré la introducción y iré directo al grano:

Al usar enrutamiento pushState simple en una aplicación Redux, dividimos el estado de la aplicación en dos dominios: el historial del navegador y la tienda Redux.

Esto es lo que se ve con React Router, que crea instancias y envuelve history:

history → React Router ↘ view Redux ↗

Ahora, sabemos que no todos los datos tienen que residir en la tienda. Por ejemplo, el estado de componente local suele ser un lugar adecuado para almacenar datos específicos de un solo componente.

Pero los datos de ubicación no son triviales. Es una parte dinámica e importante del estado de la aplicación, el tipo de datos que pertenecen a la tienda. Mantenerlo en la tienda permite lujos Redux como la depuración de viajes en el tiempo y un fácil acceso desde cualquier componente conectado a la tienda.

Entonces, ¿cómo movemos la ubicación a la tienda?

No se puede obviar el hecho de que el navegador lee y almacena el historial y la información de ubicación en el window, pero lo que podemos hacer es mantener una copia de los datos de ubicación en la tienda y mantenerlos sincronizados con el navegador.

¿No es eso lo que react-router-redux hace para el router React?

Sí, pero solo para habilitar las capacidades de viaje en el tiempo de Redux DevTools. La aplicación aún depende de los datos de ubicación almacenados en el enrutador React:

history → React Router ↘ ↕ view Redux ↗

No se recomienda usar react-router-redux para leer datos de ubicación de la tienda en lugar del enrutador React (debido a fuentes de verdad potencialmente conflictivas).

¿Podemos hacerlo mejor?

¿Podemos construir un modelo de enrutamiento alternativo, uno que se construya desde cero para jugar bien con Redux, lo que nos permite leer y actualizar la ubicación de la manera Redux, con store.getState() y store.dispatch()?

Absolutamente podemos, y se llama enrutamiento Redux-first.

Redux-Primer enrutamiento

Redux-first routing es una variación del enrutamiento pushState que hace de Redux la estrella del modelo de enrutamiento.

Una solución de enrutamiento Redux-first cumple los siguientes criterios:

  • La ubicación se encuentra en la tienda Redux.
  • La ubicación se cambia enviando acciones Redux.
  • La aplicación lee los datos de ubicación únicamente de la tienda.
  • La tienda y el historial del navegador se mantienen sincronizados entre bastidores.

Aquí hay una idea básica de cómo se ve:

history ↕ Redux → router → view

Espera, ¿no hay todavía dos fuentes de datos de ubicación?

Sí, pero si podemos confiar en que el historial del navegador y la tienda Redux están sincronizados, podemos crear nuestras aplicaciones para que solo lean los datos de ubicación de la tienda. Luego, desde el punto de vista de la aplicación, solo hay una fuente de verdad: la tienda.

¿Cómo logramos el enrutamiento Redux primero?

Podemos comenzar creando un modelo conceptual, fusionando los elementos fundamentales de los modelos de ciclo de vida de datos de enrutamiento del lado del cliente y Redux.

Revisión del Modelo de Enrutamiento del Lado del Cliente

El enrutamiento del lado del cliente es un proceso de varios pasos que comienza con la navegación y termina con el renderizado, ¡el enrutamiento en sí es solo un paso en ese proceso! Repasemos los detalles:

  • Navegación: Todo comienza con un cambio de ubicación. Hay 2 tipos de navegación: interna y externa. La navegación interna se realiza desde la aplicación (p. ej. a través de la API de Historial), mientras que la navegación externa se produce cuando el usuario interactúa con la barra de navegación del navegador o ingresa a la aplicación desde un sitio externo.
  • Respuesta a la navegación: Cuando cambia la ubicación, la aplicación responde pasando la nueva ubicación al enrutador. Las técnicas de enrutamiento más antiguas se basaban en el sondeo window.location para lograr esto, pero hoy en día tenemos la útil utilidad history.listen.Enrutamiento
  • : A continuación, la nueva ubicación coincide con el contenido de su página correspondiente. El código que maneja este paso se llama enrutador, y generalmente toma un parámetro de entrada de rutas y páginas coincidentes llamado configuración de ruta. Renderizado
  • : Finalmente, el contenido se renderiza en el cliente. Este paso puede, por supuesto, ser manejado por un framework/librería front-end como React.

Tenga en cuenta que las bibliotecas de enrutamiento no tienen que manejar todas las partes del modelo de enrutamiento.

Algunas bibliotecas, como el enrutador React y el enrutador Vue, lo hacen, mientras que otras, como el enrutador Universal, se ocupan únicamente de un solo aspecto( como el enrutamiento), lo que proporciona flexibilidad en los otros aspectos:

Las bibliotecas de enrutamiento pueden tener diferentes ámbitos de responsabilidad. (Haga clic para ampliar)

Revisión del Modelo de Ciclo de Vida de Datos Redux

Redux cuenta con un modelo de flujo de datos/ciclo de vida unidireccional que probablemente no necesita introducción — pero aquí hay una breve descripción general para una buena medida:Acción

  • : Cualquier cambio en el estado comienza enviando una acción Redux (un objeto plano que contiene una carga útil type y opcional).
  • Middleware: La acción pasa a través de la cadena de middlewares de la tienda, donde se pueden interceptar acciones y ejecutar comportamientos adicionales. Los middlewares se utilizan comúnmente para manejar efectos secundarios en aplicaciones Redux.Reductor
  • : La acción llega al reductor de raíz, que calcula el siguiente estado de la tienda como una función pura del estado anterior y la acción recibida. El reductor de raíces puede estar compuesto de reductores individuales que cada uno maneja una porción del estado de la tienda.
  • Nuevo estado: La tienda guarda el nuevo estado devuelto por el reductor y notifica a sus suscriptores del cambio (en React, a través de connect).
  • Renderizado: Por último, la vista conectada a la tienda puede volver a renderizarse de acuerdo con el nuevo estado.

Creación de un modelo de enrutamiento Redux-First

La naturaleza unidireccional del enrutamiento del lado del cliente y los modelos de ciclo de vida de datos Redux se prestan bien a un modelo combinado que satisface los criterios que establecimos para el enrutamiento primero en Redux.

En este modelo, el enrutador está suscrito a la tienda, la navegación se realiza a través de acciones Redux y las actualizaciones del historial del navegador se gestionan mediante un middleware personalizado. Examinemos los detalles de este modelo:

  • Navegación interna a través de acciones Redux — En lugar de usar la API de historial directamente, la navegación interna se logra enviando una de las 5 acciones de navegación que reflejan los métodos de navegación del historial.
  • Actualizar el historial del navegador a través de middleware: Se utiliza un middleware para interceptar las acciones de navegación y manejar el efecto secundario de actualizar el historial del navegador. Dado que la nueva ubicación no es necesaria o fácilmente conocida sin consultar primero el historial del navegador (p. ej. en el caso de una acción go), se impide que las acciones de navegación lleguen al reductor.
  • Responder a la navegación: El flujo de ejecución continúa con un receptor history que responde a la navegación (desde el middleware y la navegación externa) enviando una segunda acción que contiene la nueva ubicación.
  • Reductor de ubicación: La acción enviada por el oyente llega al reductor de ubicación, que agrega la ubicación a la tienda. El reductor de ubicación también determina la forma del estado de ubicación.
  • Enrutamiento conectado: el enrutador conectado a la tienda puede determinar de forma reactiva el nuevo contenido de la página cuando se le notifica un cambio de ubicación en la tienda.
  • Renderizado: Finalmente, la página puede volver a renderizarse con el nuevo contenido.

Tenga en cuenta que esta no es la única manera de lograr el enrutamiento primero en Redux, algunas variaciones cuentan con el uso de un potenciador de tienda y/o lógica adicional en el middleware, pero es un modelo simple que cubre todas las bases.

A Implementación básica

Siguiendo el modelo que acabamos de ver, implementemos la API principal: las acciones, el middleware, el oyente y el reductor.

Usaremos el paquete history como dependencia interna y construiremos la solución de forma incremental. Si prefieres seguir el resultado final, puedes verlo aquí.

Acciones

Comenzaremos definiendo las 5 acciones de navegación que reflejan los métodos de navegación del historial:

// constants.jsexport const PUSH = 'ROUTER/PUSH';export const REPLACE = 'ROUTER/REPLACE';export const GO = 'ROUTER/GO';export const GO_BACK = 'ROUTER/GO_BACK';export const GO_FORWARD = 'ROUTER/GO_FORWARD';
// actions.jsexport const push = (href) => ({ type: PUSH, payload: href,});
export const replace = (href) => ({ type: REPLACE, payload: href,});
export const go = (index) => ({ type: GO, payload: index,});
export const goBack = () => ({ type: GO_BACK,});
export const goForward = () => ({ type: GO_FORWARD,});

Middleware

A continuación, definamos el middleware. Debe interceptar las acciones de navegación, llamar a los métodos de navegación history correspondientes y, a continuación, evitar que la acción llegue al reductor — pero dejar todas las demás acciones sin interrupciones:

// middleware.jsexport const routerMiddleware = (history) => () => (next) => (action) => { switch (action.type) { case PUSH: history.push(action.payload); break; case REPLACE: history.replace(action.payload); break; case GO: history.go(action.payload); break; case GO_BACK: history.goBack(); break; case GO_FORWARD: history.goForward(); break; default: return next(action); }};

Si no ha tenido la oportunidad de escribir o examinar el funcionamiento interno de un middleware Redux antes, consulte esta introducción.

Listener de historial

A continuación, necesitaremos un listener history que responda a la navegación enviando una nueva acción que contenga la nueva información de ubicación.

En primer lugar, agreguemos el nuevo tipo de acción y creador. Las partes interesantes de la ubicación son pathname, search y hash, por lo que eso es lo que incluiremos en la carga útil:

// constants.jsexport const LOCATION_CHANGE = 'ROUTER/LOCATION_CHANGE';
// actions.jsexport const locationChange = ({ pathname, search, hash }) => ({ type: LOCATION_CHANGE, payload: { pathname, search, hash, },});

A continuación, vamos a escribir la función de escucha:

// listener.jsexport function startListener(history, store) { history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

Haremos una pequeña adición, un envío inicial locationChange, para dar cuenta de la entrada inicial en la aplicación (que no es recogida por el oyente de historial):

// listener.jsexport function startListener(history, store) { store.dispatch(locationChange({ pathname: history.location.pathname, search: history.location.search, hash: history.location.hash, }));
 history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

Reductor

A continuación, definamos el reductor de ubicación. Usaremos una forma de estado simple y haremos un trabajo mínimo en el reductor:

// reducer.jsconst initialState = { pathname: '/', search: '', hash: '',};
export const routerReducer = (state = initialState, action) => { switch (action.type) { case LOCATION_CHANGE: return { ...state, ...action.payload, }; default: return state; }};

Código de aplicación

Finalmente, conectemos nuestra API al código de aplicación:

// index.jsimport { combineReducers, applyMiddleware, createStore } from 'redux'import { createBrowserHistory } from 'history'import { routerReducer } from './reducer'import { routerMiddleware } from './middleware'import { startListener } from './listener'import { push } from './actions'
// Create the history objectconst history = createBrowserHistory()
// Build the root reducerconst rootReducer = combineReducers({ // ...otherReducers, router: routerReducer,}) // Build the middlewareconst middleware = routerMiddleware(history)
// Create the storeconst store = createStore(rootReducer, {}, applyMiddleware(middleware))
// Start the history listenerstartListener(history, store)
// Now you can read location data from the store!let currentLocation = store.getState().router.pathname
// You can also subscribe to changes in the location!let unsubscribe = store.subscribe(() => { let previousLocation = currentLocation currentLocation = store.getState().router.pathname
 if (previousLocation !== currentLocation) { // You can render your application reactively here! }})
// And you can dispatch navigation actions from anywhere!store.dispatch(push('/about'))

¡Y eso es todo lo que hay que hacer! Usando nuestra pequeña API (menos de 100 líneas de código), hemos cumplido con todos los criterios para el enrutamiento Redux-first:

  • La ubicación se encuentra en la tienda Redux. ✔
  • La ubicación se cambia enviando acciones Redux. ✔
  • La aplicación lee los datos de ubicación únicamente de la tienda. ✔
  • La tienda y el historial del navegador se mantienen sincronizados entre bastidores. ✔

Vea todos los archivos juntos aquí: siéntase libre de importarlos a su proyecto o utilícelos como punto de partida para desarrollar su propia implementación.

El paquete redux-first-routing

También he unido la API en el paquete redux-first-routing, que puede usar npm install y de la misma manera.

mksarge / redux-first-routing
redux-first-routing: Una base mínima independiente del marco para realizar el enrutamiento Redux-first.github.com

Incluye una implementación similar a la que construimos aquí, pero con la notable adición de análisis de consultas a través del paquete query-string.

Espera – ¿qué pasa con el componente de enrutamiento real?

Es posible que haya notado que redux-first-routing solo se ocupa del aspecto de navegación del modelo de enrutamiento:

Al desacoplar el aspecto de navegación de los otros aspectos de nuestro modelo de enrutamiento, hemos ganado cierta flexibilidad: redux-first-routing es independiente del enrutador y del marco de trabajo.

Por lo tanto, puede emparejarlo con una biblioteca como Enrutador Universal para crear una solución de enrutamiento Redux completa para cualquier marco de interfaz:

Haga clic aquí para comenzar con redux-primer enrutamiento + enrutador universal.

O bien, puedes crear enlaces con opiniones para el marco que elijas — y eso es lo que haremos para React en la siguiente y última sección de este artículo.

Uso con React

Terminemos nuestra exploración mirando cómo podríamos construir componentes conectados a la tienda para navegación declarativa y enrutamiento en React.

Navegación declarativa

Para la navegación, podemos utilizar un componente <Link/> conectado a la tienda similar al de React Router y otras soluciones de enrutamiento de React.

Simplemente anula el comportamiento predeterminado del elemento de anclaje < a / > y dispatches una acción push cuando se hace clic:

// Link.jsimport React from 'react';import { connect } from 'react-redux';import { push as pushAction, replace as replaceAction } from './actions';
const Link = (props) => { const { to, replace, children, dispatch, ...other } = props;
 const handleClick = (event) => { // Ignore any click other than a left click if ((event.button && event.button !== 0) || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented === true) { return; } // Prevent the default behaviour (page reload, etc.) event.preventDefault();

 // Dispatch the appropriate navigation action if (replace) { dispatch(replaceAction(to)); } else { dispatch(pushAction(to)); } };
 return ( <a href={to} onClick={handleClick} {...other}> {children} </a>);};
export default connect()(Link);

Puede encontrar una implementación más completa aquí.

Enrutamiento declarativo

Aunque no hay mucho en un componente de navegación, hay innumerables formas de diseñar un componente de enrutamiento, lo que lo convierte en la parte más interesante de cualquier solución de enrutamiento.

¿Qué es un router, de todos modos?

Generalmente puede ver un enrutador como una función o una caja negra con dos entradas y una salida:

route configuration ↘ matched content current location ↗

Aunque el enrutamiento y el procesamiento posterior pueden ocurrir en pasos separados, React hace que sea fácil e intuitivo agruparlos en una API de enrutamiento declarativo. Veamos dos estrategias para lograr esto.

Estrategia 1: Un componente monolítico <Router/>

Podemos utilizar un componente monolítico <Router/> conectado a la tienda que:

  • acepta un objeto de configuración de ruta a través de accesorios
  • lee los datos de ubicación de la tienda Redux
  • calcula el nuevo contenido cada vez que cambia la ubicación
  • renderiza/vuelve a renderizar el contenido según corresponda.

La configuración de ruta puede ser un objeto JavaScript plano que contenga todas las rutas y páginas coincidentes (una configuración de ruta centralizada).

Así es como podría verse esto:

const routes = 
React.render( <Provider store={store}> <Router routes={routes}> </Provider>, document.getElementById('app'))

Bastante simple, ¿verdad? No se necesitan rutas JSX anidadas, solo un objeto de configuración de ruta y un componente de enrutador.

Si esta estrategia le resulta atractiva, consulte mi implementación más completa en la biblioteca redux-json-router. Envuelve redux-first-routing y proporciona enlaces de React para navegación declarativa y enrutamiento utilizando las estrategias que hemos examinado hasta ahora.

mksarge/redux-json-router
redux-json-router-Declarativo, Redux – primer enrutamiento para el navegador React / Redux applications.github.com

Estrategia 2: Components <Route/> components

Aunque un componente monolítico puede ser una forma sencilla de lograr un enrutamiento declarativo en React, definitivamente no es la única forma.

La naturaleza componible de React permite otra posibilidad interesante: usar JSX para definir rutas de manera descentralizada. Por supuesto, el ejemplo principal es la API <Route/> del router React:

React.render( <BrowserRouter> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </BrowserRouter>

Otras bibliotecas de enrutamiento también exploran esta idea. Aunque no he tenido la oportunidad de hacerlo, no veo ninguna razón por la que una API similar no pueda implementarse encima del paquete redux-first-routing.

En lugar de depender de los datos de ubicación proporcionados por <BrowserRoute r / > , the &l t; Ruta / > componente could si mply conéctese a la tienda:

React.render( <Provider store={store}> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </Provider>

Si eso es algo que te interesa construir o usar, ¡házmelo saber en los comentarios! Para obtener más información sobre las diferentes estrategias de configuración de rutas, consulta esta introducción en el sitio web de React Router.

Conclusión

Espero que esta exploración le haya ayudado a profundizar su conocimiento sobre el enrutamiento del lado del cliente y le haya mostrado lo simple que es lograrlo de la manera Redux.

Si está buscando una solución de enrutamiento Redux completa, puede usar el paquete redux-first-routing con un enrutador compatible que se muestra en el archivo readme. Y si necesitas desarrollar una solución a medida, espero que este artículo te haya dado un buen punto de partida para hacerlo.

Si desea obtener más información sobre el enrutamiento del lado del cliente en React y Redux, consulte los siguientes artículos — que fueron fundamentales para ayudarme a comprender mejor los temas que cubrí aquí:

  • Deje que La URL Hable por Tyler Thompson
  • Es Posible Que No Necesite React Router por Konstantin Tarkus
  • ¿Incluso Necesito una Biblioteca de Enrutamiento? por James K. Nelson
  • e innumerables discusiones informativas en los números react-router-redux.

El enrutamiento del lado del cliente es un espacio con infinitas posibilidades de diseño, y estoy seguro de que algunos de ustedes han jugado con ideas similares a las que he compartido aquí. Si quieres continuar la conversación, estaré encantado de conectarme contigo en los comentarios o a través de Twitter. Gracias por leer!

Editar 22/06/17: También consulte este artículo sobre redux-first-router, un proyecto separado que utiliza tipos de acción inteligentes para lograr potentes capacidades de enrutamiento.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.