Universal Data Loading
O Slash oferece um sistema de Universal Data Loading que funciona tanto no servidor quanto no cliente, com cache automático e hidratação de dados.
O que é Universal Data Loading?
Section titled “O que é Universal Data Loading?”Universal Data Loading (ou Data Fetching Isomórfico) permite que você escreva código de carregamento de dados uma única vez e ele funcione em ambos os ambientes:
- No servidor (SSR): Busca dados durante a renderização inicial
- No cliente: Reutiliza dados hidratados ou busca novos dados com cache
Benefícios
Section titled “Benefícios”- ✅ Código Único: Mesma lógica funciona em servidor e cliente
- ✅ Cache Inteligente: Reduz requisições desnecessárias
- ✅ Hidratação Automática: Dados do servidor são reutilizados no cliente
- ✅ TTL Configurável: Controle fino sobre expiração de cache
- ✅ Invalidação Seletiva: Limpar cache por chave ou tudo
createLoader()
Section titled “createLoader()”A função createLoader() cria um loader universal com cache automático.
Sintaxe
Section titled “Sintaxe”function createLoader<T>( fn: LoaderFunction<T>, options: LoaderOptions): LoaderFunction<T>
type LoaderFunction<T> = (ctx: LoaderContext) => T | Promise<T>
type LoaderContext = { params: Record<string, string> request?: Request isServer: boolean}
type LoaderOptions = { key: string ttl?: number revalidate?: boolean}Parâmetros
Section titled “Parâmetros”- fn: Função que busca os dados (síncrona ou assíncrona)
- options: Configurações do loader
- key: Identificador único do loader
- ttl: Time-to-live do cache em milissegundos (padrão: 5 minutos)
- revalidate: Se
true, sempre revalida dados (padrão:false)
Retorno
Section titled “Retorno”Retorna uma função loader que pode ser chamada com um contexto.
Exemplo Básico
Section titled “Exemplo Básico”import { createLoader } from '@ezbug/slash'
// Criar loaderconst userLoader = createLoader( async ({ params }) => { const res = await fetch(`/api/users/${params.id}`) return res.json() }, { key: 'user', ttl: 60000 } // 1 minuto de cache)
// Usar loaderconst user = await userLoader({ params: { id: '123' }, isServer: false})
console.log(user) // { id: 123, name: 'John', ... }Como Funciona o Cache
Section titled “Como Funciona o Cache”No Servidor
Section titled “No Servidor”No servidor, o loader sempre executa a função, sem usar cache:
const loader = createLoader( async () => { console.log('Executando no servidor...') return { data: 'from server' } }, { key: 'data' })
// Servidor: sempre executaawait loader({ params: {}, isServer: true })// Log: "Executando no servidor..."
await loader({ params: {}, isServer: true })// Log: "Executando no servidor..." (novamente)No Cliente
Section titled “No Cliente”No cliente, o loader usa cache inteligente:
const loader = createLoader( async () => { console.log('Buscando dados...') return { data: Date.now() } }, { key: 'data', ttl: 5000 } // 5 segundos)
// Cliente: primeira chamadaconst result1 = await loader({ params: {}, isServer: false })// Log: "Buscando dados..."
// Cliente: segunda chamada (dentro do TTL)const result2 = await loader({ params: {}, isServer: false })// Nenhum log - usou cache
// result1 === result2 (mesmo objeto do cache)Cache por Parâmetros
Section titled “Cache por Parâmetros”O cache é baseado na chave + parâmetros:
const userLoader = createLoader( async ({ params }) => { return { id: params.id, name: `User ${params.id}` } }, { key: 'user', ttl: 60000 })
// Busca user 1 (cache miss)await userLoader({ params: { id: '1' }, isServer: false })
// Busca user 2 (cache miss - params diferentes)await userLoader({ params: { id: '2' }, isServer: false })
// Busca user 1 novamente (cache hit - mesmos params)await userLoader({ params: { id: '1' }, isServer: false })TTL (Time-To-Live)
Section titled “TTL (Time-To-Live)”O TTL controla por quanto tempo os dados ficam no cache.
TTL Padrão
Section titled “TTL Padrão”O TTL padrão é 5 minutos (300.000ms):
const loader = createLoader( async () => getData(), { key: 'data' } // TTL padrão: 5 minutos)TTL Customizado
Section titled “TTL Customizado”const shortCacheLoader = createLoader( async () => getLiveData(), { key: 'live', ttl: 10000 } // 10 segundos)
const longCacheLoader = createLoader( async () => getStaticData(), { key: 'static', ttl: 3600000 } // 1 hora)
const noCacheLoader = createLoader( async () => getAlwaysFresh(), { key: 'fresh', ttl: 0 } // Sem cache)TTL Expirado
Section titled “TTL Expirado”Quando o cache expira, o loader executa novamente:
const loader = createLoader( async () => ({ timestamp: Date.now() }), { key: 'time', ttl: 1000 } // 1 segundo)
const result1 = await loader({ params: {}, isServer: false })console.log(result1.timestamp) // 1234567890000
// Esperar TTL expirarawait new Promise(resolve => setTimeout(resolve, 1100))
const result2 = await loader({ params: {}, isServer: false })console.log(result2.timestamp) // 1234567891100 (novo timestamp)invalidateLoader()
Section titled “invalidateLoader()”Remove dados do cache de forma seletiva ou total.
Sintaxe
Section titled “Sintaxe”function invalidateLoader(key?: string): voidParâmetros
Section titled “Parâmetros”- key (opcional): Chave do loader a ser invalidado. Se omitido, limpa todo o cache.
Invalidar Loader Específico
Section titled “Invalidar Loader Específico”const userLoader = createLoader( async ({ params }) => ({ id: params.id }), { key: 'user', ttl: 60000 })
// Carregar dadosawait userLoader({ params: { id: '1' }, isServer: false })await userLoader({ params: { id: '2' }, isServer: false })
// Invalidar apenas userinvalidateLoader('user')
// Próxima chamada vai buscar novamenteawait userLoader({ params: { id: '1' }, isServer: false })Invalidar Todo o Cache
Section titled “Invalidar Todo o Cache”import { invalidateLoader } from '@ezbug/slash'
// Limpar todo o cacheinvalidateLoader()
// Todos os loaders vão buscar dados novamenteExemplo Prático: Invalidação após Mutação
Section titled “Exemplo Prático: Invalidação após Mutação”import { createLoader, invalidateLoader } from '@ezbug/slash'
const todosLoader = createLoader( async () => { const res = await fetch('/api/todos') return res.json() }, { key: 'todos', ttl: 60000 })
async function addTodo(text: string) { await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) })
// Invalidar cache após mutação invalidateLoader('todos')}
async function deleteTodo(id: string) { await fetch(`/api/todos/${id}`, { method: 'DELETE' })
// Invalidar cache invalidateLoader('todos')}Hidratação de Dados
Section titled “Hidratação de Dados”serializeLoaderData()
Section titled “serializeLoaderData()”Serializa dados de loaders para enviar ao cliente:
function serializeLoaderData(data: Record<string, unknown>): stringimport { serializeLoaderData } from '@ezbug/slash'
const loaderData = { 'user:{"id":"123"}': { id: 123, name: 'John' }, 'posts:{}': [{ id: 1, title: 'Post 1' }]}
const serialized = serializeLoaderData(loaderData)// '{"user:{\"id\":\"123\"}":{"id":123,"name":"John"},"posts:{}":[{"id":1,"title":"Post 1"}]}'deserializeLoaderData()
Section titled “deserializeLoaderData()”Deserializa dados no cliente:
function deserializeLoaderData(serialized: string): Record<string, unknown>import { deserializeLoaderData } from '@ezbug/slash'
const serialized = document.getElementById('__LOADER_DATA__')?.textContent || '{}'const data = deserializeLoaderData(serialized)
console.log(data)// {// 'user:{"id":"123"}': { id: 123, name: 'John' },// 'posts:{}': [...]// }hydrateLoaderCache()
Section titled “hydrateLoaderCache()”Injeta dados pré-carregados no cache do cliente:
function hydrateLoaderCache(data: Record<string, unknown>): voidimport { hydrateLoaderCache, deserializeLoaderData } from '@ezbug/slash'
// Recuperar dados serializadosconst serialized = document.getElementById('__LOADER_DATA__')?.textContent || '{}'const data = deserializeLoaderData(serialized)
// Hidratar cachehydrateLoaderCache(data)
// Agora os loaders vão usar os dados hidratadosFluxo Completo SSR + Data Loading
Section titled “Fluxo Completo SSR + Data Loading”Servidor
Section titled “Servidor”import { renderToString, htmlString, createLoader, serializeLoaderData} from '@ezbug/slash'
// Criar loaderconst userLoader = createLoader( async ({ params }) => { const res = await fetch(`https://api.example.com/users/${params.id}`) return res.json() }, { key: 'user', ttl: 60000 })
// Renderizar páginaasync function renderPage(userId: string) { // Buscar dados no servidor const user = await userLoader({ params: { id: userId }, isServer: true })
// Renderizar componente const App = () => htmlString` <div id="app"> <h1>${user.name}</h1> <p>Email: ${user.email}</p> </div> `
const { html } = renderToString(App)
// Serializar dados do loader const loaderData = serializeLoaderData({ [`user:${JSON.stringify({ id: userId })}`]: user })
// HTML completo return ` <!DOCTYPE html> <html> <body> ${html} <script id="__LOADER_DATA__" type="application/json"> ${loaderData} </script> <script src="/client.js" type="module"></script> </body> </html> `}Cliente
Section titled “Cliente”import { createLoader, hydrateLoaderCache, deserializeLoaderData} from '@ezbug/slash'
// Hidratar cache com dados do servidorconst serialized = document.getElementById('__LOADER_DATA__')?.textContent || '{}'const loaderData = deserializeLoaderData(serialized)hydrateLoaderCache(loaderData)
// Criar o mesmo loaderconst userLoader = createLoader( async ({ params }) => { const res = await fetch(`/api/users/${params.id}`) return res.json() }, { key: 'user', ttl: 60000 })
// Usar loader (vai usar dados hidratados do servidor!)const user = await userLoader({ params: { id: '123' }, isServer: false})
console.log(user) // Dados do servidor, sem nova requisiçãoisServer()
Section titled “isServer()”Detecta se o código está rodando no servidor ou cliente.
Sintaxe
Section titled “Sintaxe”function isServer(): booleanimport { isServer } from '@ezbug/slash'
if (isServer()) { console.log('Executando no servidor') // Usar APIs do Node.js, acessar filesystem, etc.} else { console.log('Executando no cliente') // Usar APIs do navegador, window, localStorage, etc.}Exemplo Prático
Section titled “Exemplo Prático”import { createLoader, isServer } from '@ezbug/slash'
const dataLoader = createLoader( async () => { if (isServer()) { // No servidor: ler do filesystem const fs = await import('fs/promises') const data = await fs.readFile('./data.json', 'utf-8') return JSON.parse(data) } else { // No cliente: fazer fetch const res = await fetch('/api/data') return res.json() } }, { key: 'data', ttl: 60000 })Padrões Avançados
Section titled “Padrões Avançados”Loader com Autenticação
Section titled “Loader com Autenticação”const authenticatedLoader = createLoader( async ({ request, params }) => { const token = request?.headers.get('Authorization')
const res = await fetch(`/api/protected/${params.id}`, { headers: token ? { Authorization: token } : {} })
if (!res.ok) throw new Error('Unauthorized')
return res.json() }, { key: 'protected', ttl: 30000 })Loader com Retry
Section titled “Loader com Retry”const retryLoader = createLoader( async ({ params }) => { let attempts = 0 const maxAttempts = 3
while (attempts < maxAttempts) { try { const res = await fetch(`/api/data/${params.id}`) if (res.ok) return res.json() attempts++ } catch (error) { attempts++ if (attempts >= maxAttempts) throw error await new Promise(resolve => setTimeout(resolve, 1000 * attempts)) } }
throw new Error('Max retries exceeded') }, { key: 'retry', ttl: 60000 })Loader com Debounce
Section titled “Loader com Debounce”let debounceTimer: NodeJS.Timeout | null = null
const searchLoader = createLoader( async ({ params }) => { // Debounce de 300ms await new Promise(resolve => { if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = setTimeout(resolve, 300) })
const res = await fetch(`/api/search?q=${params.query}`) return res.json() }, { key: 'search', ttl: 10000 })Loader com Dependências
Section titled “Loader com Dependências”const userLoader = createLoader( async ({ params }) => { const res = await fetch(`/api/users/${params.userId}`) return res.json() }, { key: 'user', ttl: 60000 })
const postsLoader = createLoader( async ({ params }) => { // Carregar user primeiro const user = await userLoader({ params: { userId: params.userId }, isServer: params.isServer })
// Depois carregar posts do user const res = await fetch(`/api/users/${user.id}/posts`) return res.json() }, { key: 'posts', ttl: 30000 })Exemplo Completo: Blog SSR
Section titled “Exemplo Completo: Blog SSR”Servidor
Section titled “Servidor”import { renderToString, htmlString, createLoader, serializeLoaderData} from '@ezbug/slash'
type Post = { id: string; title: string; content: string }
const postsLoader = createLoader( async () => { const res = await fetch('https://api.example.com/posts') return res.json() as Promise<Post[]> }, { key: 'posts', ttl: 300000 } // 5 minutos)
const postLoader = createLoader( async ({ params }) => { const res = await fetch(`https://api.example.com/posts/${params.id}`) return res.json() as Promise<Post> }, { key: 'post', ttl: 600000 } // 10 minutos)
async function renderBlogPage(postId: string) { // Carregar dados const post = await postLoader({ params: { id: postId }, isServer: true })
// Renderizar const App = () => htmlString` <article> <h1>${post.title}</h1> <div>${post.content}</div> </article> `
const { html } = renderToString(App)
// Serializar dados const loaderData = serializeLoaderData({ [`post:${JSON.stringify({ id: postId })}`]: post })
return ` <!DOCTYPE html> <html> <body> ${html} <script id="__LOADER_DATA__">${loaderData}</script> <script src="/client.js" type="module"></script> </body> </html> `}Cliente
Section titled “Cliente”import { createLoader, hydrateLoaderCache, deserializeLoaderData, invalidateLoader} from '@ezbug/slash'
// Hidratar cacheconst loaderData = deserializeLoaderData( document.getElementById('__LOADER_DATA__')?.textContent || '{}')hydrateLoaderCache(loaderData)
// Recriar loadersconst postLoader = createLoader( async ({ params }) => { const res = await fetch(`/api/posts/${params.id}`) return res.json() }, { key: 'post', ttl: 600000 })
// Usar loader (vai usar dados hidratados!)const post = await postLoader({ params: { id: '123' }, isServer: false})
// Adicionar interatividadedocument.querySelector('.refresh-btn')?.addEventListener('click', () => { invalidateLoader('post') // Recarregar post postLoader({ params: { id: '123' }, isServer: false })})Boas Práticas
Section titled “Boas Práticas”✅ Use TTL Apropriado
Section titled “✅ Use TTL Apropriado”// Dados estáticos: TTL longoconst staticLoader = createLoader(fn, { key: 'static', ttl: 3600000 }) // 1 hora
// Dados dinâmicos: TTL curtoconst liveLoader = createLoader(fn, { key: 'live', ttl: 5000 }) // 5 segundos
// Dados em tempo real: sem cacheconst realtimeLoader = createLoader(fn, { key: 'realtime', ttl: 0 })✅ Invalide Cache Após Mutações
Section titled “✅ Invalide Cache Após Mutações”async function updateUser(id: string, data: any) { await fetch(`/api/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
// Invalidar cache invalidateLoader('user')}✅ Compartilhe Loaders Entre Componentes
Section titled “✅ Compartilhe Loaders Entre Componentes”export const userLoader = createLoader(...)export const postsLoader = createLoader(...)
// component-a.tsimport { userLoader } from './loaders'const user = await userLoader(...)
// component-b.tsimport { userLoader } from './loaders' // Mesmo loader, compartilha cache!const user = await userLoader(...)✅ Trate Erros Adequadamente
Section titled “✅ Trate Erros Adequadamente”const safeLoader = createLoader( async ({ params }) => { try { const res = await fetch(`/api/data/${params.id}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } catch (error) { console.error('Loader error:', error) return null // Ou valor padrão } }, { key: 'safe', ttl: 60000 })❌ Evite Loaders sem Chave Única
Section titled “❌ Evite Loaders sem Chave Única”// ❌ ERRADO: Mesma chave para dados diferentesconst loader1 = createLoader(fn1, { key: 'data' })const loader2 = createLoader(fn2, { key: 'data' }) // Conflito!
// ✅ CORRETO: Chaves únicasconst userLoader = createLoader(fn1, { key: 'user' })const postLoader = createLoader(fn2, { key: 'post' })Debugging
Section titled “Debugging”Ver Estado do Cache
Section titled “Ver Estado do Cache”// Adicionar log ao loaderconst loader = createLoader( async (ctx) => { console.log('Loader executou:', ctx) const data = await fetchData() console.log('Dados carregados:', data) return data }, { key: 'debug', ttl: 60000 })Verificar Dados Hidratados
Section titled “Verificar Dados Hidratados”const serialized = document.getElementById('__LOADER_DATA__')?.textContentconsole.log('Dados hidratados:', serialized)
const data = deserializeLoaderData(serialized || '{}')console.log('Parsed:', data)Próximos Passos
Section titled “Próximos Passos”- Veja SSR para renderização no servidor
- Explore Hydration para reconectar estados
- Confira exemplos no template slash-ssr