Trabalhando com React Hooks e TypeScript
Publicados: 2022-03-11Hooks foram introduzidos no React em fevereiro de 2019 para melhorar a legibilidade do código. Já discutimos os ganchos do React em artigos anteriores, mas desta vez estamos examinando como os ganchos funcionam com o TypeScript.
Antes dos ganchos, os componentes React costumavam ter dois sabores:
- Classes que lidam com um estado
- Funções que são totalmente definidas por seus adereços
Um uso natural disso foi construir componentes de contêiner complexos com classes e componentes de apresentação simples com funções puras.
O que são Hooks React?
Os componentes do contêiner tratam do gerenciamento de estado e das solicitações ao servidor, que serão chamados neste artigo de efeitos colaterais . O estado será propagado para os filhos do contêiner por meio dos adereços.
Mas à medida que o código cresce, os componentes funcionais tendem a ser transformados em componentes de contêiner.
Atualizar um componente funcional para um mais inteligente não é tão doloroso, mas é uma tarefa demorada e desagradável. Além disso, distinguir apresentadores e contêineres de forma muito rigorosa não é mais bem-vindo.
Hooks podem fazer as duas coisas, então o código resultante é mais uniforme e tem quase todos os benefícios. Aqui está um exemplo de adição de um estado local a um pequeno componente que está manipulando uma assinatura de cotação.
// put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }
Há um grande bônus nisso - codificar com TypeScript foi ótimo com Angular, mas inchado com React. No entanto, codificar hooks React com TypeScript é uma experiência agradável.
TypeScript com Old React
O TypeScript foi projetado pela Microsoft e seguiu o caminho do Angular quando o React desenvolveu o Flow , que agora está perdendo força. Escrever classes React com TypeScript ingênuo foi bastante doloroso porque os desenvolvedores do React tiveram que digitar props
e state
mesmo que muitas chaves fossem as mesmas.
Aqui está um objeto de domínio simples. Fazemos um aplicativo de cotação , do tipo Quotation
, gerenciado em alguns componentes brutos com estado e props. A Quotation
pode ser criada e seu status associado pode mudar para assinado ou não.
interface Quotation{ id: number title:string; lines:QuotationLine[] price: number } interface QuotationState{ readonly quotation:Quotation; signed: boolean } interface QuotationProps{ quotation:Quotation; } class QuotationPage extends Component<QuotationProps, QuotationState> { // ... }
Mas imagine que o QuotationPage agora pedirá ao servidor um id : por exemplo, a 678ª cotação feita pela empresa. Bem, isso significa que o QuotationProps não conhece esse número importante - ele não está encapsulando exatamente um Quotation . Temos que declarar muito mais código na interface QuotationProps:
interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }
Copiamos todos os atributos, exceto o id, em um novo tipo. Hum. Isso me faz pensar no Java antigo, que envolvia escrever um monte de DTOs. Para superar isso, aumentaremos nosso conhecimento de TypeScript para contornar a dor.
Benefícios do TypeScript com Hooks
Usando ganchos, poderemos nos livrar da interface QuotationState anterior. Para fazer isso, dividiremos QuotationState em duas partes diferentes do estado.
interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Ao dividir o estado, não precisamos criar novas interfaces. Os tipos de estado local geralmente são inferidos pelos valores de estado padrão.
Componentes com ganchos são todas funções. Assim, podemos escrever o mesmo componente retornando o tipo FC<P>
definido na biblioteca React. A função declara explicitamente seu tipo de retorno, definindo junto com o tipo de props.
const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Evidentemente, usar TypeScript com ganchos React é mais fácil do que usá-lo com classes React. E como a tipagem forte é uma segurança valiosa para a segurança do código, você deve considerar o uso do TypeScript se seu novo projeto usar ganchos. Você definitivamente deve usar ganchos se quiser algum TypeScript.
Existem inúmeras razões pelas quais você pode evitar o TypeScript, usando React ou não. Mas se você optar por usá-lo, definitivamente deve usar ganchos também.
Recursos específicos do TypeScript Adequado para Hooks
No exemplo anterior do TypeScript de ganchos React, eu ainda tenho o atributo number no QuotationProps, mas ainda não há nenhuma pista do que esse número realmente é.
O TypeScript nos fornece uma longa lista de tipos de utilitários, e três deles nos ajudarão com o React, reduzindo o ruído de muitas descrições de interface.
-
Partial<T>
: qualquer subchave de T -
Omit<T, 'x'>
: todas as chaves de T exceto a chavex
-
Pick<T, 'x', 'y', 'z'>
: Exatamente as teclasx, y, z
deT
No nosso caso, gostaríamos que Omit<Quotation, 'id'>
omitisse o id da cotação. Podemos criar um novo tipo rapidamente com a palavra-chave type
.
Partial<T>
e Omit<T>
não existe na maioria das linguagens tipadas, como Java, mas ajuda muito para exemplos com Forms no desenvolvimento front-end. Simplifica a carga de digitação.
type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }
Agora temos uma cotação sem id. Então, talvez pudéssemos projetar um Quotation
e PersistedQuotation
extends
Quotation
. Além disso, resolveremos facilmente alguns problemas recorrentes if
undefined
. Devemos ainda chamar a variável de cotação, embora não seja o objeto completo? Isso está além do escopo deste artigo, mas vamos mencioná-lo mais tarde de qualquer maneira.

No entanto, agora temos certeza de que não divulgaremos um objeto que pensávamos ter um number
. Usar Partial<T>
não traz todas essas garantias, portanto, use-o com discrição.
Pick<T, 'x'|'y'>
é outra maneira de declarar um tipo em tempo real sem o ônus de ter que declarar uma nova interface. Se for um componente, basta editar o título da cotação:
type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>
Ou apenas:
function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}
Não me julgue, sou um grande fã de Domain Driven Design. Não sou preguiçoso a ponto de não querer escrever mais duas linhas para uma nova interface. Eu uso interfaces para descrever precisamente os nomes de domínio, e essas funções utilitárias para correção do código local, evitando ruídos. O leitor saberá que Quotation
é a interface canônica.
Outros benefícios dos ganchos React
A equipe do React sempre viu e tratou o React como um framework funcional. Eles usaram classes para que um componente pudesse lidar com seu próprio estado e agora os ganchos como uma técnica que permite que uma função acompanhe o estado do componente.
interface Place{ city:string, country:string } const initialState:Place = { city: 'Rosebud', country: 'USA' }; function reducer(state:Place, action):Partial<Place> { switch (action.type) { case 'city': return { city: action.payload }; case 'country': return { country: action.payload }; } } function PlaceForm() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input type="text" name="city" onChange={(event) => { dispatch({ type: 'city',payload: event.target.value}) }} value={state.city} /> <input type="text" name="country" onChange={(event) => { dispatch({type: 'country', payload: event.target.value }) }} value={state.country} /> </form> ); }
Aqui está um caso em que usar Partial
é seguro e uma boa escolha.
Embora uma função possa ser executada várias vezes, o gancho useReducer
associado será criado apenas uma vez.
Ao extrair naturalmente a função redutora do componente, o código pode ser dividido em várias funções independentes em vez de várias funções dentro de uma classe, todas vinculadas ao estado dentro da classe.
Isso é claramente melhor para testabilidade - algumas funções lidam com JSX, outras com comportamento, outras com lógica de negócios e assim por diante.
Você (quase) não precisa mais de componentes de ordem superior. O padrão Render props é mais fácil de escrever com funções.
Assim, a leitura do código é mais fácil. Seu código não é um fluxo de classes/funções/padrões, mas um fluxo de funções. No entanto, como suas funções não estão anexadas a um objeto, pode ser difícil nomear todas essas funções.
TypeScript ainda é JavaScript
JavaScript é divertido porque você pode rasgar seu código em qualquer direção. Com o TypeScript, você ainda pode usar keyof
para brincar com as teclas dos objetos. Você pode usar uniões de tipo para criar algo ilegível e insustentável - não, eu não gosto disso. Você pode usar alias de tipo para fingir que uma string é um UUID.
Mas você pode fazer isso com segurança nula. Certifique-se de que seu tsconfig.json
tenha a opção "strict":true
. Verifique antes do início do projeto, ou você terá que refatorar quase todas as linhas!
Há debates sobre o nível de digitação que você coloca em seu código. Você pode digitar tudo ou deixar o compilador inferir os tipos. Depende da configuração do linter e das escolhas da equipe.
Além disso, você ainda pode cometer erros de tempo de execução! TypeScript é mais simples que Java e evita problemas de covariância/contravariância com Generics.
Neste exemplo de Animal/Gato, temos uma lista de animais idêntica à lista de gatos. Infelizmente, é um contrato na primeira linha, não na segunda. Então, adicionamos um pato à lista Animal, então a lista de Gato é falsa.
interface Animal {} interface Cat extends Animal { meow: () => string; } const duck = {age: 7}; const felix = { age: 12, meow: () => "Meow" }; const listOfAnimals: Animal[] = [duck]; const listOfCats: Cat[] = [felix]; function MyApp() { const [cats , setCats] = useState<Cat[]>(listOfCats); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }
O TypeScript tem apenas uma abordagem bivariante para genéricos que é simples e ajuda na adoção do desenvolvedor JavaScript. Se você nomear suas variáveis corretamente, raramente adicionará um duck
a um listOfCats
.
Além disso, há uma proposta de adição e saída de contratos para covariância e contravariância.
Conclusão
Recentemente, voltei ao Kotlin, que é uma boa linguagem tipada, e era complicado digitar genéricos complexos corretamente. Você não tem essa complexidade genérica com o TypeScript por causa da simplicidade da tipagem de pato e da abordagem bivariante, mas você tem outros problemas. Talvez você não tenha encontrado muitos problemas com o Angular, projetado com e para TypeScript, mas você os teve com as classes React.
O TypeScript foi provavelmente o grande vencedor de 2019. Ganhou o React, mas também está conquistando o mundo back-end com o Node.js e sua capacidade de digitar bibliotecas antigas com arquivos de declaração bastante simples. Está enterrando o Flow, embora alguns vão até o fim com o ReasonML.
Agora, sinto que hooks+TypeScript é mais agradável e produtivo que o Angular. Eu não teria pensado nisso seis meses atrás.