Como criar seu próprio sistema de gerenciamento de estado com vanilla JS?
Gerenciando o estado não é uma coisa nova em software, mas ainda é relativamente novo para construir software em JavaScript. Tradicionalmente, manteríamos o estado dentro do próprio DOM ou até mesmo o atribuiríamos a um objeto global na janela. Agora, porém, somos mimados com opções de bibliotecas e estruturas para nos ajudar com isso. Bibliotecas como Redux, MobX e Vuex(Ou Pinia) tornam o gerenciamento do estado entre componentes quase trivial. Isso é ótimo para a resiliência de um aplicativo e funciona muito bem com uma estrutura reativa de state-first, como React ou Vue.
Mas como essas bibliotecas funcionam? O que seria necessário para escrever uma nós mesmos?
Conceitos
Antes de começar, é importante conhecer alguns conceitos:
- Estado
O estado pode ser entendido como o local onde guardarmos dados da aplicação, este pode ser global onde toda a aplicação pode fazer uso ou local onde apenas uma parte ou componente da aplicação podem fazer uso.
- Observadores
São funções que são vinculadas a determinados valores para que possam ser chamadas sempre que esse valor mudar e, então, executar algo sobre a mudança.
Existem outros conceitos e provavelmente você poderá ver pequenas mudanças em cada biblioteca, mas com isso podemos iniciar o desenvolvimento do nosso próprio sistema.
Começando
De início vamos criar uma função store
que receberá um objeto normal:
function store(initStateObject) {
}
Agora que podemos acessar as propriedades que o “usuário” do nosso sistema vai precisar, devemos fazer algo com elas, devemos retornar algo que ele possa realmente usar como sistema de gerenciamento de estado, para isso devemos definir como irá funcionar:
-
Devemos permitir obter e alterar o valor atual da propriedade.
-
Devemos permitir a criação de observadores para cada propriedade.
Para realizar a primeira parte, podemos criar um novo objeto derivado do objeto passado como argumento, este novo objeto conterá todas as propriedades, porém, elas serão convertidas em funções que quando chamadas sem argumento, retornarão o valor atual da propriedade e quando chamado com um argumento, este passará a ser o novo valor da propriedade:
function store(initStateObject) {
// Obtém uma lista com todas as propriedades no objeto.
const keys = Object.keys(initStateObject);
// O objeto que terá as propriedades como funções.
const realStateObject = {};
// Útil para evitar alterá o objeto original.
const initStateObjectClone = { ...initStateObject };
for (const key of keys) {
// A função que será usada no lugar da propriedade.
const propertyFn = (newValue) => {
// Se não houver um novo valor, retorne o valor atual.
if (newValue === undefined) return initStateObjectClone[key];
// Atualiza o valor da propriedade
initStateObjectClone[key] = newValue;
return newValue;
};
// Adiciona a propriedade como uma função ao objeto de estado.
realStateObject[key] = propertyFn;
}
// Retorna o objeto de estado.
/**
* ? O objeto é congelado para evitar inserção de propriedades
* ? já que não lidaremos com elas.
* */
return Object.freeze(realStateObject);
}
Com isso podemos saber quando um valor de uma propriedade foi consultado ou alterado.
Agora devemos permitir a anexação de observadores para que qualquer parte da aplicação interessada em uma propriedade saiba que ela foi alterada e faça algo a respeito.
Para isso, vamos aproveitar o fato de que as funções Javascript podem receber propriedades:
function store(initStateObject) {
// Obtém uma lista com todas as propriedades no objeto.
const keys = Object.keys(initStateObject);
// O objeto que terá as propriedades como funções.
const realStateObject = {};
// Útil para evitar alterá o objeto original.
const initStateObjectClone = { ...initStateObject };
for (const key of keys) {
//! Novo:
// Armazena os observadores para cada propriedadade.
const watchers = new Set();
// A função que será usada no lugar da propriedade.
const propertyFn = (newValue) => {
// Se não houver um novo valor, retorne o valor atual.
if (newValue === undefined) return initStateObjectClone[key];
// Atualiza o valor da propriedade
initStateObjectClone[key] = newValue;
//! Novo:
// Percorre todos os observadores e passa a eles o novo valor.
watchers.forEach((fn) => fn(newValue));
return newValue;
};
//! Novo:
// Método `watch()` que será responsável por adicionar os observadores.
propertyFn.watch = (observer) => {
//! Novo:
// Chama o observador imediatamente na primeira vez.
observer(initStateObjectClone[key]);
// Armazena o observador.
watchers.add(observer);
// Uma função responsável por remover o observado.
return () => watchers.delete(observer);
};
// Adiciona a propriedade como uma função ao objeto de estado.
realStateObject[key] = propertyFn;
}
// Retorna o objeto de estado.
/**
* ? O objeto é congelado para evitar inserção de propriedades
* ? já que não lidaremos com elas.
* */
return Object.freeze(realStateObject);
}
O que fizemos foi adicionar um método watch()
nas funções que foram criadas a partir das propriedades, ele é responsável por adicionar o observador, o método watch()
também retorna uma função que quando chamada remove o observador.
Exemplo de uso:
const userData = store({ username: undefined });
userData.username.watch((u) => u && alert(`Òlá, ${u}!`));
setTimeout(() => {
userData.username("Diogo Neves");
}, 2000);
Fechamento
Isso é apenas o básico, mas com isso temos um sistema de gerenciamento de estado simples e útil!
Poderíamos usar Proxies Javascript, no entanto, acredito que dessa forma fica mais fácil de entender e pensar por si próprio como um sistema de gerenciamento de estado funciona, mas recomendo a leitura sobre Proxies.
Obrigado por chegar até aqui!
Qualquer dúvida deixe um comentário, até mais!