Reescrevendo a biblioteca Recoil para React em 100 linhas
Tags: ptbr, react, javascript, webdev
Published: 2020-09-23 00:43:03

[Créditos da Imagem](https://unsplash.com/photos/_m94htQw9X8)

Recoil é uma nova biblioteca do React, escrita por algumas pessoas do Facebook que trabalham em uma ferramenta chamada ["Comparison View".](https://www.youtube.com/watch?v=_ISAA_Jt9kI) Ela surgiu devido a problemas de ergonomia e [desempenho com `context`](https://github.com/facebook/react/issues/14620) e `useState`. É uma biblioteca muito inteligente e quase todo mundo encontrará uma utilidade para ela - confira este [vídeo explicativo](https://www.youtube.com/watch?v=_ISAA_Jt9kI) se quiser saber mais.

No começo eu fiquei realmente surpreso com a conversa sobre teoria dos gráficos e a mágica maravilhosa que o Recoil executa, mas depois de um tempo comecei a ver que talvez não seja tão especial assim. Aqui está minha chance de implementar algo semelhante!

Antes de começar, observe que a maneira como implementei meu clone do Recoil é completamente diferente de como o Recoil real é implementado. Não presuma nada sobre Recoil disso.

## Átomos

O Recoil é construído em torno do conceito de “átomos”. Os átomos são pequenas partes atômicas de estado que você pode assinar e atualizar em seus componentes.

Para começar, vou criar uma classe chamada `Atom` que vai envolver algum valor `T`. Eu adicionei métodos auxiliares `update` e `snapshot`para permitir que você obtenha e defina o valor.

```ts
class Atom<T> {
  constructor(private value: T) {}

  update(value: T) {
    this.value = value;
  }

  snapshot(): T {
    return this.value;
  }
}
```

Para ouvir as mudanças no estado, você precisa usar [o padrão do observador](https://medium.com/@oieduardorabelo/padr%C3%B5es-em-js-observer-pattern-bff0ecc55d01) . Isso é comumente visto em bibliotecas como [RxJS](https://rxjs-dev.firebaseapp.com/guide/overview) , mas, neste caso, vou escrever uma versão síncrona simples do zero.

Para saber quem está ouvindo o estado, uso um `Set` com callbacks. Um `Set` (ou conjunto de hash) é uma estrutura de dados que contém apenas itens únicos. Em JavaScript, ele pode ser facilmente transformado em um array e possui métodos úteis para adicionar e remover itens rapidamente.

A adição de um ouvinte é feita por meio do método `subscribe`. O método subscribe retorna `Disconnecter` - uma interface contendo um método que impedirá um ouvinte de escutar. Isso é chamado quando um componente React é desmontado e você não deseja mais ouvir as alterações.

Em seguida, um método chamado `emit` é adicionado. Este método percorre cada um dos ouvintes e fornece a eles o valor atual do estado.

Por fim, atualizo o método `update` para emitir os novos valores sempre que o estado for definido.

```ts
type Disconnecter = { disconnect: () => void };

class Atom<T> {
  private listeners = new Set<(value: T) => void>();

  constructor(private value: T) {}

  update(value: T) {
    this.value = value;
    this.emit();
  }

  snapshot(): T {
    return this.value;
  }

  emit() {
    for (const listener of this.listeners) {
      listener(this.snapshot());
    }
  }

  subscribe(callback: (value: T) => void): Disconnecter {
    this.listeners.add(callback);
    return {
      disconnect: () => {
        this.listeners.delete(callback);
      },
    };
  }
}
```

Ufa!

É hora de escrever o átomo em nossos componentes React. Para fazer isso, criei um gancho chamado `useCoiledValue`. ( [soa familiar?](https://recoiljs.org/docs/api-reference/core/useRecoilValue/) )

Este gancho retorna o estado atual de um átomo, e escuta e renderiza novamente sempre que o valor mudar. Sempre que o gancho é desmontado, ele desconecta o ouvinte.

Uma coisa um pouco estranha aqui é o gancho `updateState`. Ao executar um estado definido com uma nova referência de objeto ( `{}`), o React irá renderizar novamente o componente. Isso é um pouco um hack, mas é uma maneira fácil de garantir que o componente seja renderizado novamente.

```ts
export function useCoiledValue<T>(value: Atom<T>): T {
  const [, updateState] = useState({});

  useEffect(() => {
    const { disconnect } = value.subscribe(() => updateState({}));
    return () => disconnect();
  }, [value]);

  return value.snapshot();
}
```

Em seguida, adicionei um método `useCoiledState`. Tem uma API muito semelhante a `useState`- fornece o valor atual do estado e permite que você defina um novo.

```ts
export function useCoiledState<T>(atom: Atom<T>): [T, (value: T) => void] {
  const value = useCoiledValue(atom);
  return [value, useCallback((value) => atom.update(value), [atom])];
}
```

Agora que implementamos esses ganchos, é hora de passar para os seletores. Antes disso, vamos refatorar um pouco o que temos.

Um seletor é um valor com estado, assim como um átomo. Para tornar a implementação deles um pouco mais fácil, moverei a maior parte da lógica de `Atom` para uma classe base chamada `Stateful`.

```ts
class Stateful<T> {
  private listeners = new Set<(value: T) => void>();

  constructor(private value: T) {}

  protected _update(value: T) {
    this.value = value;
    this.emit();
  }

  snapshot(): T {
    return this.value;
  }

  subscribe(callback: (value: T) => void): Disconnecter {
    this.listeners.add(callback);
    return {
      disconnect: () => {
        this.listeners.delete(callback);
      },
    };
  }
}

class Atom<T> extends Stateful<T> {
  update(value: T) {
    super._update(value);
  }
}
```

Seguindo em frente!

## Seletores

Um seletor é a versão de Recoil de “valores computados” ou “redutores”. Em suas [próprias palavras](https://recoiljs.org/docs/basic-tutorial/selectors) :

> Um seletor representa uma parte derivado do estado. Você pode pensar no estado derivado como a saída da passagem do estado para uma função pura que modifica o estado fornecido de alguma forma.

A API para seletores no Recoil é bastante simples, você cria um objeto com um método chamado `get` e tudo o que esse método retorna é o valor do seu estado. Dentro do método `get`, você pode assinar outras partes do estado e, sempre que elas forem atualizadas, o seu seletor também será.

Em nosso caso, vou renomear o método `get` a ser chamado de `generator`. Estou chamando-o assim porque é essencialmente uma função de fábrica que deve gerar o próximo valor do estado, com base em tudo o que é canalizado para ele.

![Um fluxograma demonstrando dois átomos (intitulados “hello” e “bob”) sendo canalizados para um seletor, com a saída se tornando “Hello, Bob”](https://bennetthardwick.com/blog/recoil-js-clone-from-scratch-in-100-lines/atom-selector-flow_hu6c1d970fc33d5ff12c990845e7422d9f_40682_694x0_resize_box_2.png)

No código, podemos capturar esse método `generate` com a seguinte assinatura de tipo.

```ts
type SelectorGenerator<T> = (context: GeneratorContext) => T;
```

Para aqueles que não estão familiarizados com TypeScript, essa é uma função que recebe um objeto de contexto ( `GeneratorContext`) como parâmetro e retorna algum valor `T`. Esse valor de retorno é o que se torna o estado interno do seletor.

O que o objeto `GeneratorContext` faz?

Bem, é assim que os seletores usam outras partes do estado ao gerar seu próprio estado interno. De agora em diante, vou me referir a essas partes do estado como “dependências”.

```ts
interface GeneratorContext {
  get: <V>(dependency: Stateful<V>) => V
}
```

Sempre que alguém chama o método `get` no `GeneratorContext`, ele adiciona um pedaço de estado como uma dependência. Isso significa que sempre que uma dependência for atualizada, o seletor também será.

Veja como fica a criação da função de geração de um seletor:

```ts
function generate(context) {
  // Registra "NameAtom" como dependência
  // e retorna seu valor
  const name = context.get(NameAtom);
  // Faz o mesmo para "AgeAtom"
  const age = context.get(AgeAtom);

  // Retorna um novo valor usando os átomos anteriores
  // Ex: "Bob is 20 years old"
  return `${name} is ${age} years old.`;
};
```

Com a função de geração de estado fora do caminho, vamos criar a classe `Selector`. Essa classe deve aceitar a função de geração como um parâmetro do construtor e usar um método `getDep` na classe para retornar o valor das `Atom` de dependências.

Você pode notar no construtor que escrevi `super(undefined as any)`. Isso ocorre porque `super` deve ser a primeira linha no construtor de uma classe derivada. Se ajudar, neste caso você pode pensar em `undefined` como uma memória não inicializada.

```ts
export class Selector<T> extends Stateful<T> {
  private getDep<V>(dep: Stateful<V>): V {
    return dep.snapshot();
  }

  constructor(
    private readonly generate: SelectorGenerator<T>
  ) {
    super(undefined as any);
    const context = {
      get: dep => this.getDep(dep) 
    };
    this.value = generate(context);
  }
}
```

Este seletor só é bom para gerar estado uma vez. Para reagir às mudanças nas dependências, precisamos assiná-las.

Para fazer isso, vamos atualizar o método `getDep` para assinar as dependências e chamar o método `updateSelector`. Para garantir que o seletor seja atualizado apenas uma vez a cada alteração, vamos acompanhar as dependências usando um `Set`.

O método `updateSelector` é muito semelhante ao construtor do exemplo anterior. Ele cria `GeneratorContext`, executa o método `generate` e então usa o método `update` da classe base `Stateful`.

```ts
export class Selector<T> extends Stateful<T> {
  private registeredDeps = new Set<Stateful>();

  private getDep<V>(dep: Stateful<V>): V {
    if (!this.registeredDeps.has(dep)) {
      dep.subscribe(() => this.updateSelector());
      this.registeredDeps.add(dep);
    }

    return dep.snapshot();
  }

  private updateSelector() {
    const context = {
      get: dep => this.getDep(dep)
    };
    this.update(this.generate(context));
  }

  constructor(
    private readonly generate: SelectorGenerator<T>
  ) {
    super(undefined as any);
    const context = {
      get: dep => this.getDep(dep) 
    };
    this.value = generate(context);
  }
}
```

Quase pronto! Recoil tem algumas funções auxiliares para criar átomos e seletores. Como a maioria dos desenvolvedores de JavaScript considera as classes como má prática, eles ajudarão a mascarar nossas atrocidades.

Um para criar um átomo ...

```ts
export function atom<V>(
  value: { key: string; default: V }
): Atom<V> {
  return new Atom(value.default);
}
```

E um para criar um seletor ...

```ts
export function selector<V>(value: {
  key: string;
  get: SelectorGenerator<V>;
}): Selector<V> {
  return new Selector(value.get);
}
```

Oh, lembra daquele gancho `useCoiledValue` de antes? Vamos atualizar isso para aceitar seletores também:

```ts
export function useCoiledValue<T>(value: Stateful<T>): T {
  const [, updateState] = useState({});

  useEffect(() => {
    const { disconnect } = value.subscribe(() => updateState({}));
    return () => disconnect();
  }, [value]);

  return value.snapshot();
}
```

É isso aí! Conseguimos! 🎉

Dê um tapinha nas suas costas!

Acabado?

Por uma questão de brevidade (e para usar aquele título de “100 linhas” para ganhar uns cliques), decidi omitir comentários, testes e exemplos. Se você quiser uma explicação mais completa (ou quiser brincar com exemplos), tudo isso está no meu [repositório “recoil-clone” do Github.](https://github.com/bennetthardwick/recoil-clone)

Também há um [exemplo de site](https://100-line-recoil-clone.netlify.app/) ao vivo para que você possa testá-lo.

## Conclusão

Uma vez li que todo bom software deve ser simples o suficiente para que qualquer pessoa possa reescrevê-lo, se necessário. O Recoil tem muitos recursos que não implementei aqui, mas é empolgante ver um design tão simples e intuitivo que _pode_ ser razoavelmente implementado manualmente.

Antes de decidir lançar meu bootleg Recoil em produção, certifique-se de verificar o seguinte:

-   Os seletores nunca cancelam a inscrição dos átomos. Isso significa que eles vazarão memória quando você parar de usá-los.
-   React [introduziu](https://github.com/facebook/react/pull/18000) um gancho chamado `useMutableSource`. Se você estiver usando uma versão recente do React, você deve usá-lo ao invés de `setState` em `useCoiledValue`.
-   Seletores e Átomos fazem apenas uma comparação superficial entre estados antes de renderizar novamente. Em alguns casos, pode fazer sentido alterar isso para uma comparação profunda.
-   O Recoil usa um campo `key` para cada átomo e seletor que é usado como metadados para um recurso chamado “observação em todo o aplicativo”. Eu o incluí apesar de não usá-lo para manter a API familiar.
-   O Recoil oferece suporte a seletores assíncronos, isso seria uma tarefa gigantesca, então fiz questão de excluí-lo.

Além disso, espero ter mostrado a você que nem sempre você precisa olhar para uma biblioteca ao decidir sobre as soluções de gerenciamento de estado. Na maioria das vezes, você pode projetar algo que se encaixa perfeitamente na sua solução - afinal, [foi assim que o Recoil nasceu](https://recoiljs.org/docs/introduction/motivation) .

----------

Depois de escrever este post, vi a biblioteca [jotai](https://github.com/react-spring/jotai) . É um conjunto de recursos muito semelhante ao meu clone e oferece suporte assíncrono!

# Créditos

- [Rewriting Facebook's "Recoil" React library from scratch in 100 lines](https://bennetthardwick.com/amp/blog/recoil-js-clone-from-scratch-in-100-lines/), escrito originalmente por [Bennett](https://twitter.com/bennettbackward).