Patterns avançados com React
16 de junho, 2023 — 6 min read
Neste artigo, exploraremos padrões avançados com ReactJS, que permitem reutilizar código e simplificar a estrutura de projetos. Faremos uso dos recursos oferecidos pelo JavaScript e pelo React, como funções, componentes, propriedades e estados. Ao aplicar esses padrões, poderemos melhorar a eficiência e a organização do nosso código. Através de exemplos práticos, irei apresentar alguns desses padrões, demonstrando sua aplicação e benefícios.
Render Props
O padrão Render Props é uma técnica muito útil no React que permite compartilhar lógica entre componentes, sem a necessidade de fazer o lifting state up. Em vez disso, podemos aproveitar as propriedades especiais do React para passar funções como propriedades para os componentes, permitindo que eles compartilhem dados e comportamentos.
A ideia básica por trás do padrão Render Props é que um componente pode aceitar uma função como propriedade e chamá-la para renderizar seu conteúdo interno. Dessa forma, o componente pai pode controlar o que é renderizado pelo componente filho, enquanto o filho tem acesso aos dados e funcionalidades necessárias.
Exemplo:
const RenderPropsParent = ({ render }) => {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter((state) => state + 1)
}
const decrement = () => {
setCounter((state) => state - 1)
}
return <div>{render({ counter, increment, decrement })}</div>
}
const RenderPropsChild = ({ counter, increment, decrement }) => {
return (
<div>
<button onClick={increment}>Increment</button>
<p>{counter}</p>
<button onClick={decrement}>Decrement</button>
</div>
)
}
const App = () => {
return (
<RenderPropsParent render={(props) => <RenderPropsChild {...props} />} />
)
}
Custom Hooks
Os hooks são, essencialmente, funções que podem utilizar outros hooks e são utilizados dentro de componentes do React. Um Custom Hook, por sua vez, é um hook que utiliza outros hooks e tem como propósito abstrair a lógica de componentes.
No exemplo mencionado anteriormente, utilizamos o padrão Render Props para manter o estado em um componente superior e utilizá-lo no componente dentro da Render Prop. No entanto, é possível isolar essa lógica em um Custom Hook e utilizá-lo apenas no componente filho.
const useCounter = () => {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter((state) => state + 1)
}
const decrement = () => {
setCounter((state) => state - 1)
}
return { counter, increment, decrement }
}
const CustomHookChild = () => {
const { counter, increment, decrement } = useCounter()
return (
<div>
<button onClick={increment}>Increment</button>
<p>{counter}</p>
<button onClick={decrement}>Decrement</button>
</div>
)
}
Props Getters
O padrão Props Getters é outro padrão interessante no React que permite manipular as propriedades de um componente antes de renderizá-lo. Com o Props Getters, podemos adicionar, modificar ou remover propriedades antes de passá-las para um componente filho.
Nesse primeiro exemplo podemos utilizar o Props Getters junto com o Render Props:
const PropsGettersParent = ({ render }) => {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter((state) => state + 1)
}
const decrement = () => {
setCounter((state) => state - 1)
}
const getButtonProps = ({ kind, ...props } = {}) => {
return {
className: 'button',
type: 'button',
...props,
onClick: kind === 'increment' ? increment : decrement,
}
}
return <div>{render({ counter, getButtonProps })}</div>
}
const PropsGettersChildren = ({ counter, getButtonProps }) => {
return (
<div>
<button {...getButtonProps({ kind: 'increment' })}>Increment</button>
<p>Counter: {counter}</p>
<button {...getButtonProps({ kind: 'decrement' })}>Decrement</button>
</div>
)
}
const App = () => {
return (
<PropsGettersParent
render={(props) => <PropsGettersChildren {...props} />}
/>
)
}
Nesse exemplo, podemos observar que definimos inicialmente algumas propriedades
padrão que serão passadas para os nossos botões por meio da função getButtonProps
.
No entanto, o interessante é que temos a flexibilidade de sobrescrever as
propriedades conforme necessário (nesse caso exceto pelo onClick
, que é uma
regra de negócio). Isso nos permite personalizar cada botão de acordo com as
nossas necessidades específicas.
Mas não precisamos utilizar o padrão Render Props. Uma abordagem mais moderna é criar um Custom Hook e mover a lógica do componente pai para esse Hook.
const useCounterProps = () => {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter((state) => state + 1)
}
const decrement = () => {
setCounter((state) => state - 1)
}
const getButtonProps = ({ kind, ...props } = {}) => {
return {
className: 'button',
type: 'button',
...props,
onClick: kind === 'increment' ? increment : decrement,
}
}
return { counter, getButtonProps }
}
const PropsGettersChildrenHook = () => {
const { counter, getButtonProps } = useCounterProps()
return (
<div>
<button {...getButtonProps({ kind: 'increment' })}>Increment</button>
<p>Counter: {counter}</p>
<button {...getButtonProps({ kind: 'decrement' })}>Decrement</button>
</div>
)
}
const App = () => {
return <PropsGettersChildrenHook />
}
Dessa forma, não precisamos utilizar o padrão Render Props. Isolamos a lógica em uma função com o hook e utilizamos apenas o componente.
High Order Components
High Order Components (HOC) são funções que recebem um componente como argumento e retornam um novo componente aprimorado. Essa abordagem é baseada nos conceitos das High Order Functions da programação funcional e do JavaScript.
Assim como as High Order Functions, os High Order Components permitem a reutilização de lógica e aprimoramento de componentes de forma modular. Eles encapsulam funcionalidades comuns em um componente e o retornam como um novo componente melhorado.
const withCounter = (WrappedComponent) => {
const EnhancedComponent = (...props) => {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter((state) => state + 1)
}
const decrement = () => {
setCounter((state) => state - 1)
}
return (
<WrappedComponent
counter={counter}
increment={increment}
decrement={decrement}
{...props}
/>
)
}
return EnhancedComponent
}
const Counter = ({ counter, increment, decrement }) => {
return (
<div>
<button onClick={increment}>Increment</button>
<p>Counter: {counter}</p>
<button onClick={decrement}>Decrement</button>
</div>
)
}
const CounterWithEnhancement = withCounter(Counter)
const App = () => {
return <CounterWithEnhancement />
}
Compound Components
O padrão de Compound Components é uma abordagem utilizada no desenvolvimento de componentes em React que permite agrupar diversos componentes relacionados em um único componente pai, chamado de Compound Component.
Ao utilizar esse padrão, os componentes filhos são projetados para trabalharem em conjunto e compartilharem informações através do componente pai. Cada componente filho representa uma parte específica do comportamento ou visualização do Compound Component.
O Compound Component fornece a estrutura e a lógica necessárias para coordenar e controlar os componentes filhos, enquanto os componentes filhos são projetados para serem flexíveis e reutilizáveis, com suas próprias responsabilidades bem definidas.
Essa abordagem permite uma maior flexibilidade e personalização do componente composto, pois cada componente filho pode ser configurado de maneira independente, alterando seu comportamento, aparência ou estado.
Um exemplo de tags nativas que seguem essa ideia é a tag select
e option
:
<select>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
</select>
Se não utilizasse essa ideia de compor as tags seria algo do tipo:
<select options="dog:Dog;cat:Cat"></select>
E no React conseguimos compor a interface da forma que desejarmos como nesse exemplo:
const counterContext = createContext()
const useCounterContext = () => useContext(counterContext)
const Counter = ({ children }) => {
const [count, setCount] = useState(0)
const increment = () => {
setCount((state) => state + 1)
}
const decrement = () => {
setCount((state) => state - 1)
}
return (
<counterContext.Provider
value={{
count,
increment,
decrement,
}}
>
{children}
</counterContext.Provider>
)
}
const DisplayCount = () => {
const { count } = useCounterContext()
return <p>{count}</p>
}
const IncrementButton = () => {
const { increment } = useCounterContext()
return <button onClick={increment}>Increment</button>
}
const DecrementButton = () => {
const { decrement } = useCounterContext()
return <button onClick={decrement}>Decrement</button>
}
const App = () => {
return (
<Counter>
<DisplayCount />
<DecrementButton />
<IncrementButton />
</Counter>
)
}
Uma outra forma bem comum é adicionar os componentes internos junto ao componente pai:
Counter.Display = DisplayCount
Counter.Increment = IncrementButton
Counter.Decrement = DecrementButton
<Counter>
<Counter.Display />
<Counter.Increment />
<Counter.Decrement />
</Counter>
Uma das maiores vantagens é poder construir a interface com os componentes na
ordem desejada. Podemos colocar o componente Display
no topo ou no final, adicionar
outro Display
em qualquer lugar da interface ou até mesmo receber estilos diferentes
como propriedades e aplicá-los apenas a um desses componentes. Como os componentes
estão compartilhando o estado de um contexto, tudo fica compartilhado e isolado.
A outra opção seria utilizar o Children.map
e cloneElement
do React, conforme
mencionado
neste link. No
entanto, com a Context API, o código fica mais legível, com a lógica isolada no
contexto, tornando-se uma abordagem mais elegante.
State Reducer
O padrão State Reducer é um padrão utilizado no desenvolvimento de aplicações em React para gerenciar o estado de um componente de forma mais flexível e extensível. Ele consiste em separar a lógica de atualização de estado em um objeto, que é responsável por receber a ação que ocorre no componente e determinar como o estado deve ser atualizado.
O state reducer é uma função que recebe o estado atual e a ação como argumentos, e retorna o novo estado com base na ação. Ele permite modificar o estado de forma customizada, aplicando transformações ou validações adicionais antes de atualizá-lo. Esse padrão é especialmente útil quando o estado do componente possui lógica complexa ou quando é necessário aplicar lógica de negócio específica durante as atualizações de estado.
const initialState = {
counter: 0,
}
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
counter: state.counter + 1,
}
case 'DECREMENT':
return {
...state,
counter: state.counter - 1,
}
default:
return state
}
}
const StateReducerCounter = () => {
const [state, dispatch] = useReducer(counterReducer, initialState)
const increment = () => {
dispatch({ type: 'INCREMENT' })
}
const decrement = () => {
dispatch({ type: 'DECREMENT' })
}
return (
<div>
<button onClick={increment}>Increment</button>
<p>{state.counter}</p>
<button onClick={decrement}>Decrement</button>
</div>
)
}
Nesse exemplo, criamos um estado inicial initialState
que contém a propriedade
counter
inicializada com 0. Em seguida, definimos um reducer counterReducer
que
recebe o estado atual e a ação, e retorna o novo estado com base na ação recebida.
No componente Counter
, utilizamos o hook useReducer
para criar o estado e a função
de dispatch
associada ao reducer counterReducer
. Ao chamar dispatch
com a ação
apropriada, o estado é atualizado de acordo com as regras definidas no reducer.
Lembrando que esse padrão é muito útil quando lidamos com estados complexos.
Podemos passar diversos valores juntamente com o type
no dispatch
, permitindo
assim a alteração desses valores no reducer
. Além disso, essa abordagem é conhecida
por aqueles que utilizam ou já utilizaram o Redux.
Conclusão e referências.
Exemplos rodando nesse link.
Não posso afirmar que existem muitos outros padrões a serem utilizados no React, levando em consideração essa lógica de abstração de código e composição de interfaces. Podemos considerar os modelos de renderização (Client Side, Server Side, etc.) como padrões, assim como alguns mencionados aqui. Além disso, você pode encontrar mais informações sobre esses padrões de renderização e muitos outros nesse site de padrões.