path | title | subtitle | date | tags | banner | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
/recursao-com-componentes-react |
Recursão com componentes React |
Como utilizar recursividade para renderizar uma árvore de componentes pode deixar seu trabalho mais simples e enxuto |
2020-06-30 |
|
|
Nem sempre alguns conceitos teóricos sobre computação são aplicáveis ou comuns no dia-a-dia, principalmente quando você desenvolve front-end. Quando o assunto é recursividade, isso não é diferente.
Além de ser uma ótima forma de aplicar efetivamente um conceito teórico, também é uma forma um pouco mais enxuta (porém nem sempre a mais performática) de resolver algum problema, como esse aqui:
Imagine que você precisa renderizar uma árvore de componentes onde cada elemento pode ou não ter inúmeros filhos
Um pouco abstrato demais, eu sei, mas para deixar as coisas mais interessantes, vamos imaginar que para uma lista de itens como essa (onde as chaves dos objetos são os nomes dos links e os valores são as URLs finais):
[
{ "Link 1": "/link-1" },
{ "Link 2": "/link-2" },
{
"Sublist 1": [
{ "Sublink 1": "/sublink-1" },
{ "Sublink 2": "/sublink-2" },
{
"Another sublist": [
{ "Sublink 1": "/sublink-1" },
{ "Sublink 2": "/sublink-2" }
]
}
]
},
{ "Link 3": "/link-3" },
{
"Link 4": [
{ "Sublink 1": "/sublink-1" },
{ "Sublink 2": "/sublink-2" },
{ "Sublink 3": "/sublink-3" }
]
}
];
Você quer renderizar algo como:
<dl>
<dd><a href="/link-1">Link 1</a></dd>
<dd><a href="/link-2">Link 2</a></dd>
<dd>
<dl>
<dt>Sublist 1</dt>
<dd><a href="/sublink-1">Sublink 1</a></dd>
<dd><a href="/sublink-2">Sublink 2</a></dd>
<dd>
<dl>
<dt>Another sublist</dt>
<dd><a href="/sublink-1">Sublink 1</a></dd>
<dd><a href="/sublink-2">Sublink 2</a></dd>
</dl>
</dd>
</dl>
</dd>
<dd><a href="/link-3">Link 3</a></dd>
<dd>
<dl>
<dt>Link 4</dt>
<dd><a href="/sublink-1">Sublink 1</a></dd>
<dd><a href="/sublink-2">Sublink 2</a></dd>
<dd><a href="/sublink-3">Sublink 3</a></dd>
</dl>
</dd>
</dl>
Ou seja, cada item da lista pode ter ou não, outras diversas sub-listas e assim por diante.
Recursão (ou recursividade) é uma estratégia para resolver um problema, de forma repetitiva e iterativa, envolvendo uma função que chama a si mesma (com algum caso final que envolve a finalização da recursão).
É meio estranho olhando pela primeira vez, eu sei, não é a toa que existe uma frase que diz:
Para entender recursão, você primeiro precisa entender recursão
Os exemplos mais clássicos de recursão na internet envolvem, geralmente, alguns exemplos matemáticos como fibonacci
ou fatorial
.
Para não nos prender tanto em qualquer um desses exemplos, vamos criar uma função que recebe um número e faz um contador (com console.log
) contando até 0
, a partir desse número;
Utilizando laços de repetição, essa função ficaria mais ou menos assim:
// criamos a função contador
const contador = total => {
// rodamos um laço de repetição com uma variável de controle
// que vai sendo decrementada e logando o contador
for(let numero = total; numero >= 0; numero--) {
console.log(`Contador: ${numero}`);
}
};
contador(10);
// Contador: 10
// ...
// Contador: 0
E, ao utilizar recursão, teremos uma função mais ou menos assim:
// criamos a função contador
const contador = total => {
// verificamos nossa condição de parada
// para não caírmos em um loop infinito
if (total >= 0) {
// enquanto o total for >= 0, continuamos executando
console.log(`Contador: ${total}`);
// e por fim, chamamos a própria função contador
return contador(total - 1);
} else {
// condição de parada
// caso contador seja igual a zero
// encerramos a execução
return;
}
};
contador(10);
// Contador: 10
// ...
// Contador: 0
Recursividade por si só é um tópico extenso e nem sempre é a forma mais performática de resolver um problema iterativo, dependendo muito da linguagem que você utiliza. Para nosso exemplo, vai cair como uma luva.
Como no nosso exemplo, o array é composto por objetos onde, cada objeto, possui a estrutura:
[
{ 'Titulo do link': 'url' }
]
Podemos iniciar nosso raciocínio utilizando a função Object.entries
, que nos retornará um array de arrays, contendo as chaves e os valores dos nossos objetos. Ou seja, para um objeto como:
{ 'Titulo do link': 'url' }
Temos a seguinte saída:
const objeto = { 'Titulo do link': 'url' };
const entries = Object.entries(objeto)
console.log(entries);
/*
[
['Titulo do link', 'url']
]
*/
E, ao ter um objeto mais complexo (com vários links da sublista dentro):
const objeto = { 'Titulo do link': [
{ 'sublink 1': 'url 1' },
{ 'sublink 2': 'url 2' },
] };
const entries = Object.entries(objeto)
console.log(entries);
/*
[
['Titulo do link',
[
{ 'sublink 1': 'url 1' },
{ 'sublink 2': 'url 2' }
]
]
]
*/
Analisando essas estruturas (que são, basicamente, as estruturas das nossas listas) já podemos ter em mente como iremos resolver esse problema:
- Iremos fazer um
map
na lista de exemplo (para gerarmos componentes a partir dela); - Para cada item da lista, iremos verificar suas chaves e valores (com
Object.entries
); - Se os valores de cada um desses itens, forem arrays, iremos renderizar a chave como título e continuar renderizando a lista recursivamente;
- Caso os valores não sejam arrays, renderizamos o item final da lista com o link.
Bora lá!
Como iremos rodar um map
em nosso array, vamos criar de forma separada a função que será executada nesse map
para cada item do array:
const renderiza = item => {};
Podemos importar nossa estrutura de links e executar essa função dentro de um componente, da seguinte forma:
// imaginando que nossa estrutura com os links está no arquivo links.js
import links from './links';
const renderiza = item => {};
const Lista = () => <dl>{links.map(renderiza)}</dl>
Como nossa lista também possuirá títulos, semânticamente, os elementos <dl>
(para lista), <dt>
(para o título) e <dd>
(para os itens) são os mais adequados (acredito eu). Então vamos criar esses dois componentes.
Agora, vamos começar a implementar as condições que colocamos acima, validando as entradas (chave e valor) de cada um dos objetos:
const renderiza = item => {
const valores = Object.entries(item);
}
Como Object.entries
nos retorna um array de arrays, podemos fazer um map
nele também:
const renderiza = item => {
const valores = Object.entries(item);
return Object.entries(item).map((entradas) =>
// proximo passo
);
}
Agora precisamos verificar, se esses itens dentro do map
(que possuem o título e os links) possuem arrays na segunda posição do array (lembrando que a primeira é a chave do objeto e a segunda é o valor dessa chave).
const renderiza = item => {
const valores = Object.entries(item);
// podemos fazes desestruturação das entradas do objeto para "title" e "link"
return valores.map(([title, link]) =>
// verificamos se "link" é um array
Array.isArray(link) ? (
// se for array, renderizaremos uma lista
) : (
// se não for, renderizaremos o item final
)
);
}
O próximo passo é criar os componentes de item e lista. Podemos imaginar que o componente SubList
receberá um title
e um items
com prop e renderizará uma lista com um título:
const SubLista = ({ title, items }) => (
<dl>
<dt>{title}</dt>
</dl>
);
E o componente de item de lista, receberá somente um title
e um link
:
const Item = ({ title, link }) => (
<dd>
<a href={link}>{title}</a>
</dd>
);
Agora, podemos renderizar esses dois componentes na função renderiza
que criamos mais acima:
const renderiza = item => {
const valores = Object.entries(item);
return valores.map(([title, link]) =>
Array.isArray(link) ? (
// agora renderizamos a sublista
// passando o titulo e a lista de itens
<SubLista title={title} items={link} />
) : (
// e renderizamos o item final
<Item title={title} link={link} />
)
);
};
Para finalizarmos, basta chamarmos a função renderiza
dentro do nosso componente de sublista. Dessa forma, esse componente também mapeará cada um dos itens renderizando eles com a própria função que ele foi renderizado!
Para fazer isso, é só atualizar o componente de SubLista
realizando um map
nos itens, também usando a função renderiza
, da seguinte forma:
const SubLista = ({ title, items }) => (
<dl>
<dt>{title}</dt>
{items.map(renderiza)}
</dl>
);
Ao fim de todo esse processo, teremos um código mais ou menos assim:
import links from './links';
const SubLista = ({ title, items }) => (
<dl>
<dt>{title}</dt>
{items.map(renderiza)}
</dl>
);
const Item = ({ title, link }) => (
<dd>
<a href={link}>{title}</a>
</dd>
);
const renderiza = item => {
const valores = Object.entries(item);
return valores.map(([title, link]) =>
Array.isArray(link) ? (
<SubLista title={title} items={link} />
) : (
<Item title={title} link={link} />
)
);
};
const Lista = () => <dl>{links.map(renderiza)}</dl>
Agora você pode isolar os seus componentes nos arquivos que achar melhor e estilizá-los como for necessário.
Se você quiser ver o resultado final, deixei um exemplo pronto no CodeSandbox. Se preferir, pode brincar direto com a demo abaixo:
<iframe src="https://codesandbox.io/embed/recursive-rendering-react-components-ksmyb?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="Recursive rendering React components" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-autoplay allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe>A primeira vista é algo meio estranho e um pouco fora da maneira como estamos acostumados a resolver problemas de interface, mas tenho certeza que é um conceito que pode te ajudar algum dia.
Embora o exemplo não seja exatamente igual ao contador que criamos no começo, podemos perceber que a estrutura é a mesma.
Mesmo com a abstração dos componentes em React
, utilizamos uma função renderiza
que verifica se ainda temos uma lista de links para renderizar e, internamente, dentro de cada uma dessas listas, chamamos essa mesma função renderiza
, até que só sejam renderizados os itens finais com os links.
Bem mais enxuto do que utilizar laços de repetição, não acha?