Definición de rutas con React Router

En una aplicación React se utilizan “rutas” para definir URLs que, a su vez, activarán ciertos componentes.

Lo habitual es que nuestra aplicación tendrá un componente principal, comunmente llamado App, y será aquí donde realizaremos la definición de las rutas y otras configuraciones como el layout.

Como pre-requisito para utilizar esta funcionalidad, debemos importar los componentes BrowserRouter, Switch y Route con la siguiente directiva:

import {BrowserRouter, Route, Switch} from 'react-router-dom';

La implementación se realiza dentro del componente función App (o sea, que no lleva render()):

function App() {
  return (
    <BrowserRouter>
      <Layout>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/posts" component={Posts} />
          <Route exact path="/posts/new" component={PostsNew} />
          <Route exact path="/posts/:postId/edit" component={PostsEdit} />
          <Route component={NotFound} />
        </Switch>
      </Layout>
    </BrowserRouter>
  );
}

En este ejemplo, tenemos 5 rutas. Las rutas son elegidas por React en base a un elemento Switch. Según la ruta que escribamos en el navegador, si encuentra una coincidencia exacta con la propiedad path, activará el component definido en la propiedad component.

Por ejemplo, si escribimos la ruta /posts/new en el navegador, se activará el componente PostsNew (obviamente, todos los componentes que se llaman aquí deben ser importados en el archivo JS).

Tenemos también un ejemplo con una ruta que recibe una variable postId. Esta variable se indica con dos puntos adelante. También tenemos el caso por descarte, si no encuentra ninguna de las rutas indicadas, mostrará el componente NotFound.

Nótese que el componente que se active estará inserto dentro del componente Layout. Este es un componente especial que se encarga de mostrar el diseño común para todas las páginas del sitio. El código completo del componente Layout podría ser algo así:

import React from 'react';

import MyNavbar from './MyNavbar';

function Layout(props) {
  return (
    <React.Fragment>
      <h1>Centraldev</h1>
      <MyNavbar />
      {props.children}
      <footer>&copy; 2021 Centraldev - All rights reserved</footer>
    </React.Fragment>
  );
}

export default Layout;

Igualmente, tengan en cuenta que esto, a su vez, irá dentro del div#app que normalmente encontramos en el archivo public/index.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <title>Mi página</title>
</head>

<body>
  <div id="app"><!-- aquí se inserta todo --></div>
</body>

</html>

¿Por qué es esto? Esto es porque en el archivo index.js (en src) tenemos lo siguiente:

const container = document.getElementById('app');
ReactDOM.render(<App />, container);

Este diagrama explica cómo se intercomunican los distintos archivos:

Archivos básicos en React.

Consumo de una API con React

Vamos a ver un ejemplo muy simple y básico de cómo proceder a implementar el consumo de una API REST desde React. Este artículo asume que ya se tienen algunos conocimientos básicos de React.

El primer paso consiste en inicializar las variables que necesitaremos para manejar los distintos estados de nuestro componente, para guardar los datos que recibimos de la API y el número de página.

Estas variables las crearemos como variables de estado pero antes vamos a inicializarlas con sus valores predeterminados:

state = {
    loading: true,
    error: null,
    data: {
      results:[]
    },
    nextPage: 1,
  };

Este código también puede ir en el constructor.

Los datos los vamos a solicitar a la API en el evento componentDidMount(), es decir, cuando el componente se ha insertado en el DOM. Para esto vamos a llamar a una función fetchData() que también crearemos:

componentDidMount(){
  this.fetchData();
}

fetchData = async () => {
  this.setState({ loading: true, error: null });

  try {
    const response = 
      await fetch(${url}?page=${nextPage});    const data await response.json();

    this.setState({
      data: { results: [].concat(this.state.data.results, data.results) },
      loading: false,
      nextPage: this.state.nextPage + 1
    });
  }
  catch(error){
    this.setState({loading: false, error: error});
  }
}

La función fetchData es asíncrona porque a su vez llama a métodos asíncronos, como es fetch. Por eso la debemos declarar con la palabra clave async.

En la función empezamos asignando el estado de loading en true ya que queremos indicar que estamos realizando la carga de datos desde la API. Luego utilizaremos esta variable para mostrar un mensaje de “loading” o también se puede emplear una animación. La variable de estado error la asignamos en null porque no tenemos ningún error de carga.

Dentro de un bloque try llamamos a la función fetch, la cual es asíncrona, por eso debemos llamarla con la palabra clave await. El método fetch es el que realiza la petición a la API y recibe, como argumento, la URL hacia donde debemos enviar la petición. El resultado de esta petición lo guardamos en la variable response.

Para obtener los datos a partir de la variable response llamamos a otra función asíncrona que es json().

Los datos obtenidos los guardaremos en la variable de estado data, mediante el método setState(). En nuestro ejemplo, el array con los resultados son un objeto results dentro de data.

Para ir sumando los datos que obtenemos en sucesivas paginaciones utilizamos el método concat() y vamos agregando estos resultados a this.state.data.results.

Render

El listado de los resultados es muy sencillo. Lo haremos en la función render(). Para esto utilizaremos map():

<ul>
  {this.state.data.results.map(character => (
    <li key={character.id}>{character.name}</li>
  ))}
</ul>

En el método render también vamos a mostrar el texto (o animación) para el estado “loading” y el mensaje de error, si existiese:

{this.state.loading && (
  <div>Loading...</div>
)}

if (this.state.error) {
  return (
  <div>An error has occurred: {this.state.error.message}</div>
)}

Por último, no debemos de olvidarnos del botón para cargar los resultados de las siguientes páginas. Este será un botón que solo se mostrará cuando this.state.loading sea false y llamará a la función fetchData() en su evento onClick:

{!this.state.loading && (
  <button onClick={() => this.fetchData()}>Load more</button>
)}