Notas sobre Fetch Isomórfico

Depois de trabalhar em dois projetos Vue seguidos, agora me vejo no meu primeiro projeto de React server-side rendered, ou, React isomórfico. Reaprender React tem sido uma dádiva — acumulei experiência suficiente em projetos reais para ter uma opinião mais informada quando tiver que escolher entre React Native ou Weex (Vue Native) em projetos futuros.

Tem sido também desafiador — gastamos duas semanas apenas lendo artigos e testando alguns boilerplates diferentes — sabíamos que queríamos usar server-side rendering para este projeto, mas não sabíamos por onde começar.

Eventualmente nos decidimos em começar com o react-starter-kit. Depois dos testes iniciais, o removemos completamente do repositório e readicionamos ponto por ponto, cuidadosamente pesquisando tudo e nos certificando que entendíamos o que estava acontecendo. Desde o nosso fork, houve updates no sistema de builds e também um upgrade para Webpack v3 — fica claro que é ativamente desenvolvido e uma escolha segura para qualquer um começando hoje.

Não fizemos muito além de migrar para Koa e reestruturar o lugar onde view components são mantidos. Uma das coisas que de fato abandonamos foi seu fetch wrapper, que é disponibilizado no objeto de contexto de aplicação. Nós queríamos uma interface universal para o fetch que nos permitiria usá-lo no client side tão bem quanto para múltiplos microservices no backend.

Depois de uma breve batalha de refactoring com um colega, chegamos à uma implementação que nos deixou bem satisfeitos. Nós introduzimos um módulo helpers/network que provê uma função makeClient(). A função wrapper por ela gerada é então exportada por um módulo client local conforme as necessidades, em arquivos como esse pela codebase:

import { makeClient, defaultHeaders } from ‘helpers/network’
import { MICROSERVICE_HOST } from ‘constants/env’
export default makeClient(MICROSERVICE_HOST, defaultHeaders)

Note que importamos MICROSERVICE_HOST de constants/env. Nós realmente aderimos ao esforço de eliminar magic strings, e ter defaults definidos através de constantes caiu bem com o conceito.

Quanto à interface, nos distanciamos da API original do fetch e provemos constantes representando cada método HTTP, para se utilizar na assinatura de método (path, method, params). Aceitamos a limitação de forçar a definição de headers na chamada de makeClient(), para que chamadas subsequentes fossem mais simples — e tornamos a conversão de objetos para JSON implícita quando se passa dados POST. A função wrapper também verifica status de resposta e processa o documento JSON. Isso nos permitiu escrever códigos de proxy de API como o seguinte:

import { GET, POST } from ‘helpers/network’
import client from ‘./client’
export default {
someMethodWithGET: async (ctx, next) => {
const res = await client(‘method/with/GET’, GET)
ctx.status = res.status
ctx.body = res.data
},
someMethodWithPOST: async (ctx, next) => {
const res = await client(‘method/with/POST’, POST, ctx.json)
ctx.status = res.status
ctx.body = res.data
}
}

O código-fonte completo está disponível no GitHub. Um agradecimento especial ao meu colega Jan Cássio por refactorar isso comigo infinitamente (e eventualmente ter me convencido de que uma abordagem funcional era melhor quando minha primeira tentativa foi uma classe HTTPClient).