Buenas prácticas en React: Link Components

Las aplicaciones web tienen enlaces por todas partes, y estos enlaces en sí información relevante: el path relativo o absoluto a las páginas. También tienen otros elementos importantes como los atributos aria-*, o rel. ¿Cómo controlo que todo esto es consistente?

El problema

Sin el control de nuestros enlaces vamos a tener problemas de páginas rotas (que llevarán a nuestros usuarios a páginas de error 404) y tampoco vamos a tener control de nuestro SEO.

Para cada enlace en el que usamos directamente los tags a o Link en nuestros componentes, en vez de usar un LinkComponent propio, estamos ensuciando nuestro código y provocando los siguientes problemas:

  • Tags a y/o Link con atributos href apuntando a direcciones (internas y externas) en múltiples puntos de la aplicación. Un cambio de una URL implicará un realizar una estrategia de buscar y reemplazar todo para cambiar todos esos links de manera manual. Mantener esto es una pesadilla en sitios de tamaño medio y grande.
  • Falta de control al establecer qué enlaces son follow o no-follow. El control pasa una vez más por un trabajo manual de búsqueda y validación.
  • Si estamos usando Link en Next.js, además hay que tener en cuenta que tenemos el atributo as que usaremos para pasar valores al construir el path de las URLs en el framework.
  • Testing manual de los enlaces y su correcta construcción. ¿Estás pasando bien los query params propios y los parámetros utm en cada enlace?
  • A nivel de arquitectura de la información, hay un problema de falta cohesión.

Cómo lo vamos a resolver

Vamos a aplicar la Ley de Demeter, un principio de diseño que intenta limitar el acoplamiento entre componentes mediante la aplicación de unas reglas muy simples.

O lo que es lo mismo y seguramente se entienda más, La Ley de Demeter básicamente nos viene a decir que nuestro objeto no debería conocer las entrañas de otros objetos con los que interactúa.

Si queremos que haga algo, ¿por qué no se lo pedimos directamente en vez de configurarlo en cada instancia? Esto se traduce en alta cohesión y bajo acoplamiento:

  • Alta cohesión: Vamos a aumentar la cohesión para reducir los puntos de fallo y coger control de todos los enlaces de la aplicación, pudiendo realizar cambios sin asumir un riesgo alto de fallo. Tendremos componentes autocontenidos, reutilizables y localizables en un punto concreto.
  • Bajo acoplamiento: aunque pueda sonar contraintuitivo, vamos a construir componentes pequeños, con responsabilidad bien definida y, por tanto más fáciles de entender por separado. Habrá más componentes, pero serán pequeños y localizables en un punto concreto.

Resumiendo:

  • Por cada página, vamos a tener un LinkComponent propio.
  • Para cada LinkComponent vamos a escribir un test.
  • Vamos a usar los LinkComponent en vez de usar a o el componente Link de next.js, que vamos a encapsular dentro de nuestros LinkComponent.
  • Sólo vamos a ver next/link dentro de LinkComponent, evitando así que todos los componentes con enlaces queden acoplados con next.js.

Show me the code (con Next.js)

Todo el código se puede encontrar en Github: https://github.com/nilportugues-com/reactjs-next-link-components

nilportugues-com/reactjs-next-link-components
Demo repository for: https://nilportugues.com/blog/react/buenas-practicas-nextjs-link-components - nilportugues-com/reactjs-next-link-components

Vamos a empezar creando un nuevo proyecto con Next.js.

npx create-next-app reactjs-next-link-components

Vamos a ir a la página que nos proporciona Next.js por defecto, ubicada en pages/index.js y que os dejo el código fuente a continuación:

import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.js</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h3>Documentation &rarr;</h3>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h3>Learn &rarr;</h3>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/master/examples"
            className={styles.card}
          >
            <h3>Examples &rarr;</h3>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            className={styles.card}
          >
            <h3>Deploy &rarr;</h3>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
        </a>
      </footer>
    </div>
  )
}

Como se puede observar tenemos varios enlaces:

  • 3 enlaces externos a Next.js
  • 1 enlace externo al repositorio de Github.
  • 2 enlace externo a Vercel, con parámetros utm.

Como nos falta 1 ejemplo, un enlace interno crearemos un último enlace con una segunda página pages/second-page.jsx que se podrá acceder como http://localhost:3000/second-page

// pages/second-page.jsx
import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Second page</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Second page
        </h1>

        <p className={styles.description}>
          I'm a second page for demo purposes
        </p>

      </main>
    </div>
  )
}

Vamos a crear 4 Link Components y los vamos a crear bajo `components/Links`:

InternalLinks

SecondPage es un nombre algo malo para una página. Si por temas de SEO quisiéramos por ejemplo renombrarla por todo el site, solo habría que cambiar este componente y todas las páginas que usasen este componente apuntarían correctamente a la nueva ubicación.

// components/Links/InternalLinks.jsx

import Link from 'next/link';

export const SecondPageLink = ({children, ...props}) => (
<Link href="/secondary-page" as="/secondary-page">
    <a {...props} >{children}</a>
</Link>
)

NextLinks

Centralizar los enlaces hacia Next nos permite cambiar de dominio base o añadir parámetros sencillamente actualizando el componente base NextLink que no queremos exportar ni exponer hacia afuera.

// components/Links/NextLinks.jsx

const NextLink = ({children, page, ...props}) => (
<a {...props} href={`https://nextjs.org${page}`}>
{children}
</a>
)
export const NextHomeLink = (props) => (
    <NextLink {...props} page="/" />
)
export const NextDocsLink = (props) => (
    <NextLink {...props} page="/docs" />
)

export const NextLearnLink = (props) => (
    <NextLink {...props} page="/learn" />
)

GithubLink

En enlace a Gitub no presenta ninguna complicación. Si un día cambia el acceso a los ejemplos lo podemos actualizar desde aquí.

// components/Links/GithubLink.jsx

export const GithubExamplesLink = ({children, ...props}) => (
    <a {...props} href="https://github.com/vercel/next.js/tree/master/example">
    {children}
    </a>
)

VercelLinks

Los enlaces hacia Vercel los queremos segurizar frente a ataques con rel="noopener noreferrer".

Igualmente se podría trabajar más los utm y empujar la lógica hacia VercelLink, que no lo queremos exponer hacia afuera.

//components/Links/VercelLinks.jsx

const VercelLink = ({children, page, utm, ...props}) => (
<a 
    {...props} 
    href={`https://vercel.com{$page}${utm.join("&")}`}
    rel="noopener noreferrer"
>
{children}
</a>
)
    
export const VercelHomepageLink = (props) => (
<VercelLink 
    {...props} 
    page="/" 
    utm={[
        "utm_source=create-next-app",
        "utm_medium=default-template",
        "utm_campaign=create-next-app"
    ]}
    target="_blank"
/>
)

export const VercelDeployLink = (props) => (
<VercelLink 
    {...props} 
    page="/new" 
    utm={[
        "utm_source=create-next-app",
        "utm_medium=default-template",
        "utm_campaign=create-next-app"
    ]}
/>
)

Aplicando los cambios, como beneficio extra, el código de la página queda más semántico.

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { NextDocsLink, NextHomeLink, NextLearnLink } from '../components/Links/NextLinks'
import { GithubExamplesLink } from '../components/Links/GithubLink'
import { VercelDeployLink, VercelHomepageLink } from '../components/Links/VercelLinks'
import { SecondPageLink } from '../components/Links/InternalLinks'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <NextHomeLink>Next.js!</NextHomeLink>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.js</code>
        </p>

        <div className={styles.grid}>
          <SecondPageLink className={styles.card}>
            <h3>Second Page &rarr;</h3>
            <p>This is a second page</p>
          </SecondPageLink>

          <NextDocsLink className={styles.card}>
            <h3>Documentation &rarr;</h3>
            <p>Find in-depth information about Next.js features and API.</p>
          </NextDocsLink>

          <NextLearnLink className={styles.card}>
            <h3>Learn &rarr;</h3>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </NextLearnLink>

          <GithubExamplesLink className={styles.card}>
            <h3>Examples &rarr;</h3>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </GithubExamplesLink>

          <VercelDeployLink>
            <h3>Deploy &rarr;</h3>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </VercelDeployLink>
        </div>
      </main>

      <footer className={styles.footer}>
        <VercelHomepageLink>
          Powered by{' '}
          <img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
        </VercelHomepageLink>
      </footer>
    </div>
  )
}

Cuándo aplicar esta solución

  • No recomiendo esta solución para una aplicación pequeña pero si la aconsejo para proyectos de tamaño medio y grande.
  • Vale la pena aplicarla en cualquier aplicación React, independientemente del framework, ya sea Next.js, CRA o React Router .
  • Cualquier aplicación móvil escrita con React Native y React Navigation se puede beneficiar de estos conceptos para enlazar la aplicación de manera limpia y elegante.
  • Evidentemente, estos conceptos se pueden extrapolar y usar en otros frameworks front-end en otros lenguajes como PHP, Java, Python, etc.

Maximizando el uso de esta estrategia:

Podemos seguir aumentando esta solución con casos de uso tan interesantes como usar un contexto para recuperar de un contexto a nivel de página los parámetros utm y aplicarlos a nivel de LinkComponent sin tener que arrastrar por todo el código estos valores.

Espero que este artículo os haya sido de interés y ayuda.

Comparte este artículo