Omar Díaz Hernández
Andres Eduardo Maneiro Antunez
Hola omar, a mi tambien me paso lo mismo y era porque no me di cuenta que el app.get('galaxias',...)
tiene que ir antes del otra app,get('*',...)
William Stiven Laya Quintero
El prefetching desde el servidor en una aplicación con server-side rendering permite enviar datos ya consultados al cliente, evitando que el navegador gaste recursos repitiendo la petición. Si trabajas con React y quieres acelerar la carga inicial de tu app, esta técnica te muestra cómo mover el fetch al backend y entregar la información lista a través del objeto window.
Por qué mover el fetch al servidor mejora la carga inicial
Cuando delegas las peticiones al cliente, cada usuario paga el costo de esperar a que el navegador consulte la API antes de pintar contenido. En el ejemplo de la app que consume la API de la NASA, al entrar a la ruta de galaxias se ve el típico estado de cargando mientras el cliente resuelve la petición.
Mover esa misma petición al servidor cambia el flujo: el backend consulta la API una vez, embebe la respuesta en el HTML y el cliente recibe los datos sin tener que volver a pedirlos.
¿Qué es el prefetching en SSR? Es la técnica de hacer la petición a una API desde el servidor antes de enviar el HTML al cliente, para que la información llegue ya cargada y la app no tenga que pedirla de nuevo en el navegador.
Cómo hacer una petición desde el servidor con Axios
La idea es definir una ruta GET en el servidor que intercepte la URL antes del render. En el archivo index del servidor declaras el manejador con request y response, y dentro haces la llamada de forma asíncrona [02:15].
Para las peticiones del lado del servidor, Axios es una librería cómoda y bien tipada. La instalas con yarn add axios y la importas en tu archivo de servidor.
ts import axios from 'axios';
app.get('/galaxies', async (req, res) => { try { const { data } = await axios.get(QUERY_URL); const initialProps = { galaxies: data.collection.items, }; res.send(render(req.url, initialProps)); } catch (error) { throw new Error('An error occurred in galaxies'); } });
Un detalle clave: el servidor debe enviar la respuesta, no retornarla. Confundir return con res.send deja al cliente esperando indefinidamente, un error fácil de cometer cuando vienes de escribir funciones puras [05:40].
Cómo estructurar las initialProps
Las initialProps son el objeto que viajará desde el servidor hasta el cliente. En este caso contienen la propiedad galaxies con el resultado de data.collection.items, exactamente la misma estructura que antes consumía el hook de React desde el navegador.
Esa simetría es lo que permite reemplazar el fetch del cliente sin reescribir la lógica de presentación.
Cómo inyectar datos en el DOM con window y JSON.stringify
El truco para pasar las initialProps al cliente es inyectarlas como un script dentro del HTML que renderiza el servidor. La función render recibe la URL y las initialProps (con valor por defecto de objeto vacío), y las pasa al template.
Dentro del template añades una etiqueta <script> sin atributo src, ubicada antes de la ejecución del bundle de la aplicación, para que la variable global esté disponible cuando React arranque.
html
<script> window.__initialProps__ = ${JSON.stringify(initialProps)}; </script>JSON.stringify es indispensable porque el HTML es texto: convierte el objeto JavaScript en una cadena válida que el navegador puede parsear de vuelta. Sin esa conversión, el objeto se imprimiría como [object Object] y romperías el script.
¿Para qué sirve window.__initialProps__? Es una propiedad que añades al objeto global del navegador para que el cliente lea los datos que el servidor ya consultó, sin tener que repetir la petición a la API.
Cómo verificar que la inyección funciona
Después de reiniciar el servidor con yarn start, abres la pestaña Network del navegador con el filtro Fetch/XHR activo. En la ruta /galaxies, al inspeccionar el HTML deberías ver la etiqueta <script> con el objeto serializado, y al ejecutar window.__initialProps__ en la consola obtienes las galaxias listas.
En rutas que no hacen prefetch, el mismo window.__initialProps__ devolverá un objeto vacío gracias al valor por defecto.
Qué riesgos de seguridad tiene inyectar datos en el DOM
Inyectar información en window abre un nuevo vector de ataque: el servidor. Por eso esta técnica funciona bien para datos públicos, como un catálogo de galaxias de la NASA, pero nunca debes inyectar información sensible como datos de usuarios, tokens o credenciales.
Algunas reglas prácticas:
window las propiedades una vez que React las haya consumido.El siguiente paso lógico es leer window.__initialProps__ desde el cliente y usar esos datos para renderizar las cards de galaxias sin volver a llamar a la API. ¿Ya intentaste aplicar este patrón en tu propio proyecto? Cuéntame en los comentarios qué API estás consumiendo.
Omar Díaz Hernández
Andres Eduardo Maneiro Antunez
Hola omar, a mi tambien me paso lo mismo y era porque no me di cuenta que el app.get('galaxias',...)
tiene que ir antes del otra app,get('*',...)
William Stiven Laya Quintero
Mauricio Combariza
Este es el código de esta clase:
app.get('/galaxias', async (req: Request, res: Response) => { try { const { data } = await axios.get("https://images-api.nasa.gov/search?q=galaxies") const initalProps = { galaxies: data?.collection?.items } res.send(render(req.url, initalProps)) } catch (error) { throw new Error("An error ocurred in /galaxias", error) } })
Raycris Maldonado
Eres un gran hombre! Gracias por el código
Jeison David Gonzalez Lopez
me quede en esta clase estoy trabajando con npm y tengo exactamente todo pero en esdte curso el window.__INITIAL_PROPS__no me esta funcionando
David Rangel
César Augusto Cortés Labrada
No me quedó claro si en esta clase apenas estamos comprobando que en la consola ya podemos ver los datos inyectados en la propiedad declarada arbitrariamente como __INITIAL_PROPS__ para el objeto window, de manera que, aún el cliente sigue consumiendo sus propios recursos para obtener los datos de las galaxias y en la próxima clase veremos como ahora sí hacemos que el cliente no sea quién use sus recursos. = |
Mauricio Combariza
Este es el script
<script> window.__INITIAL_PROPS__= ${JSON.stringify(initalProps)}</script>
Conmigo no funcionó Esto me sale en mi terminal
$ yarn start yarn run v1.22.19 $ cross-env NODE_ENV=production npm run build && cross-env NODE_ENV=production node dist/index.js > helix-eye@' build > cross-env NODE_ENV=production npm run build-client && npm run build-server > helix-eye@' build-client > cross-env webpack --config webpack.config.client.js > helix-eye@' build-server > cross-env webpack --config webpack.config.server.js listening in http://localhost:3500
Y esto en cosola
Me paso lo mismo, lo que hice fue dejar solo un get así
app.get('*', async (req: Request, res: Response) => { const { data } = await axios.get("https://images-api.nasa.gov/search?q=galaxies") const initialProps = { galaxies: data?.collection?.items } res.send(render(req.url, initialProps)) })
A mi modo de ver, como el segundo app.get volvía a hacer render y ese no tenia initialprops , se sobrescribía como vacío.
No logré que renderizara las galaxias desde el servidor, siempre me devolvió un objeto vacio
// src/server/index.ts import express, { Express, Request, Response } from 'express'; import { config } from './config'; import { render } from '../render'; const app: Express = express(); app.use(express.static('dist')); app.get('/galaxies', async (req: Request, res: Response) => { try { const response = await fetch( 'https://images-api.nasa.gov/search?q=galaxies', ); const data: any = await response.json(); const items = data?.collection?.items || []; const initialProps = { galaxies: items.length > 0 ? items.slice(0, 10) : [], }; const html = render(req.url, initialProps); res.send(html); } catch (error) { res.status(500).send('Error en el cosmos'); } }); app.use((req: Request, res: Response, next: any) => { res.setHeader( 'Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + "font-src 'self' https://fonts.gstatic.com; " + "img-src 'self' data: https://i.imgur.com https://images-assets.nasa.gov; " + "connect-src 'self' http://localhost:* ws://localhost:* https://images-api.nasa.gov;", ); next(); }); app.get('/{\*splat}', (req: Request, res: Response) => { res.send(render(req.url)); }); app.use((err: any, res: Response) => { console.error('🌊 Alerta de naufragio:', err.message); res.status(500).send('Algo salió mal en la bodega del barco.'); }); app.listen(config.PORT, () => { console.log(`Listenting in http://localhost:${config.PORT}`); }); // src/render/index.tsx import React from 'react'; import { StaticRouter } from 'react-router-dom/server'; import { renderToString } from 'react-dom/server'; import { App } from '../app/containers/App'; import { template } from './template'; export const render = (url: string, initialProps = {}) => { const stream = renderToString( <StaticRouter location={url}> <App /> </StaticRouter>, ); const html = template(stream, initialProps); return html; }; // src/render/template.ts export const template = (html: string, initialProps = {}): string => { console.log(initialProps) return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@200;300;400;600&display=swap" rel="stylesheet"> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"> <title>Helix App</title> </head> <body> <div id="app">${html}</div> <script> window.__INITIAL_PROPS_=${JSON.stringify(initialProps)} </script> <script src="/client.bundle.js" type="text/javascript"></script> </body> </html>`; }; // src/app/hooks/useFetchGalaxiesInfo.ts import { useEffect, useState } from 'react'; import { getGalaxiesJSON } from '../api'; import { off } from 'node:cluster'; export const COMPLETED_STATUS = 'COMPLETED'; export const LOADING_STATUS = 'LOADING'; export const ERROR_STATUS = 'ERROR'; export const useFetchGalaxiesInfo = () => { const [requestStatus, setRequestStatus] = useState(LOADING_STATUS); const getInitialData = () => { if (typeof window !== 'undefined' && window.__INITIAL_PROPS__?.galaxies) { return window.__INITIAL_PROPS__.galaxies; } return null; }; const initialData = getInitialData(); const [galaxies, setGalaxies] = useState(initialData || []); useEffect(() => { setRequestStatus(LOADING_STATUS); getGalaxiesJSON() .then((data) => { setGalaxies(data); setRequestStatus(COMPLETED_STATUS); }) .catch((err) => { setRequestStatus(ERROR_STATUS); console.error('Error in request galaxies', err); }); }, []); return { galaxies, requestStatus }; };