Pitch: NanoStore - Meu primeiro, pequeno e ambicioso pacote NPM (TypeScript)
Olá, pessoal! Venho apresentar o NanoStore, meu primeiro, pequeno (com apenas 129 linhas de código) e ambicioso (uma alternativa simples ao ReduxJS) pacote NPM. O NanoStore é uma alternativa para gerenciamento de estado global em React, que utiliza somente APIs nativas do React (ContextAPI e o hook useRedux), ele tem tipagem forte e inferência inteligente. O objetivo principal do NanoStore é simplificar o processo para o desenvolvedor, 100% da energia gasta na criação dele foi focado em experiência de uso.
OBS.: Caso você seja um iniciante em TypeScript, este pacote foi feito pensando principalmente em você, toda a complexidade de tipagem (e foram muitas) está sob o capô do pacote, para que você faça um código mais próximo do JS do que do TS.
Sem mais delongas, vou demonstrar como ele funciona. Para o exemplo a seguir vou iniciar um projeto react com vite (até para avaliar o uso), mas o desenvolvimento do pacote foi no NextJS então pode ser configurado exatamente igual.
npm create vite@latest nano-store-example -- --template react-ts
npm install
npm run dev
npm install
npm install @jeff.carlosbd/nano-store
Projeto criado e pacote instalado, agora vamos criar nosso store, começando pelo arquivo state.ts
. É onde vamos deixar nosso estado inicial. Caso tenha uma pasta src
, recomendo a criar na pasta src/store/state.ts
ou na raiz do projeto (caso não tenha uma pasta src) store/state.ts
:
// src/store/state.ts
import { initState } from "@jeff.carlosbd/nano-store";
interface InitialState {
pokemons: string[]
}
const initialState: InitialState = {
pokemons: []
}
export const { createReducer, mountStore } = initState(initialState)
Basicamente criamos uma interface para definir a estrutura do nosso estado e logo depois uma variável que armazenará um ojeto que atende esta interface. Por fim iniciamos o nosso estado com a função initState()
do pacote, esta retorna duas funções que vamos usar para dar continuidade.
Agora vamos iniciar a montagem da nossa store num arquivo separado (isso é importante para evitar conflitos de imports) src/store/index.ts
:
// src/store/index.ts
import { mountStore } from "./state";
export const { StoreProvider, useStoreActions, useStoreSelects } = mountStore(
[],
[],
[]
);
Utilizando a função mountStore
que foi exportada pelo nosso src/store/state.ts
, montamos nosso store que retornou mais 3 itens que vai nos auxiliar em outras etapas, por enquanto os 3 arrays que mountStore precisa vamos utilizar arrays vazios, mas logo abaixo vamos atualizar isso.
Agora vamos usar nosso StoreProvider no nosso src/main.tsx
( src/app/layout.tsx
ou app/layout.tsx
do NextJS) para envolver nossa aplicação com nosso provider, dessa forma:
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { StoreProvider } from './store/index.ts'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<StoreProvider>
<App />
</StoreProvider>
</StrictMode>,
)
Agora vamos dar uma limpada no src/App.tsx
ficando dessa forma:
import './App.css'
function App() {
return (
<h1>NanoStore - Example</h1>
)
}
export default App
Nesse ponto já temos nossa store configurada e pronta para uso. Para começar, vamos fazer um teste de busca de dados, vamos alterar nosso src/store/state.ts
para conter uma informação inicial e vamos recuperar ela no nosso src/App.tsx
:
// src/store/state.ts
// ...
const initialState: InitialState = {
pokemons: ["bulbasaur", "ivysaur"],
};
// ...
Agora no nosso src/App.tsx
vamos usar o hook useStoreSelects
para recuperar os dados da nossa store. Este hook recebe uma função que tem nosso state como parâmetro e que retorna o retorno da função (o que vc retornar na função, é o que o hook te retorna):
const pokemons = useStoreSelects((state) => state.pokemons)
Agora vamos exibir esse array de strings:
return (
<ul>
{pokemons.map((pkm) => <li key={pkm}>{pkm}</li>)}
</ul>
)
src/App.tsx
completo:
// src/App.tsx
import './App.css'
import { useStoreSelects } from './store'
function App() {
const pokemons = useStoreSelects((state) => state.pokemons)
return (
<ul>
{pokemons.map((pkm) => <li key={pkm}>{pkm}</li>)}
</ul>
)
}
export default App
Legal, estamos recebendo e exibindo os dados, mas agora vamos criar uma função que altera esse estado, vamos começar zerando nosso estado inicial novamente:
// src/store/state.ts
// ...
const initialState: InitialState = {
pokemons: [],
};
// ...
Agora precisamos criar "reducers" que vão manipular nosso estado, para que fique organizado recomendo criar uma pasta de reducers src/store/reducers/
e criar arquivos para separar os reducers por assunto como por exemplo src/store/reducers/pokemons.ts
que será onde ficará nossos reducers que alteram o estado do array de pokemons, com isso vamos ao nosso primeiro reducer que irá adicionar um pokemon no estado:
// src/store/reducers/pokemon.ts
import { createReducer } from "../state";
const addPokemon = createReducer(
"add-pokemon",
(store, payload: { newPokemonName: string }) => {
store.state.pokemons.push(payload.newPokemonName);
}
);
export const pokemonsReducers = [addPokemon];
Começamos importando a função createReducer()
do nosso state.ts
, essa função recebe dois parametros, o primeiro é uma string de identificação do reducer, devem ser strings únicas (atento nessa parte pois a biblioteca ainda não verifica isso), já o segundo parâmetro é a função que irá receber e manipular nosso estado, essa função recebe como primeiro parâmetro o nosso store e o segundo parâmetro é o nosso payload (informação que irá ser injetada no estado global.
⚠️ A tipagem manual do payload é muito importante, ela te auxiliará a saber o que cada reducer precisa para funcionar. Isso ficará mais nítido ao usar o reducer.
Por fim basta fazer um .push()
no nosso array de pokemons que está no nosso estado. cnostruido o reducer, agora é interessante adicionar ele num array de reducers que no caso chamamos de pokemonsReducers. É interessante ter um arquivo que irá centralizar todos os nossos reducers, para isso estou utilizando o arquivo src/store/reducers/index.ts
:
// src/store/reducers/index.ts
import { pokemonsReducers } from "./pokemons";
export const allReducers = [...pokemonsReducers];
Precisamos agora disponibilizar essa variável "allReducers" para nossa aplicação no arquivo src/store/index.ts
, mas a nossa função createReducer
cria um objeto com várias informações, então precisaremos desestruturar elas e passar adequadamente para nosso mountStore()
:
// src/store/index.ts
import { allReducers } from "./reducers";
import { mountStore } from "./state";
const reducers = allReducers.map((r) => r.reducer);
const actions = allReducers.map((r) => r.action);
export const { StoreProvider, useStoreActions, useStoreSelects } = mountStore(
reducers,
actions,
[]
);
Estamos importando a variável que contem todos os nossos "reducers" e logo após fazendo map's para extrair os reducers
de fato e suas respectivas actions
, ainda não falamos sobre as actions, mas ela simplesmente são informações que indicam para os hooks qual reducer deve chamar e quais parametros devem enviar, veremos no próximo código.
Agora vamos voltar ao nosso src/App.tsx
para criar um input que usará esse reducer:
// src/App.tsx
import { useState } from 'react'
import './App.css'
import { useStoreActions, useStoreSelects } from './store'
function App() {
const action = useStoreActions()
const pokemons = useStoreSelects((state) => state.pokemons)
const [newPokemonName, setNewPokemonName] = useState('')
return (
<>
<form onSubmit={(e) => {
e.preventDefault()
action({ type: 'add-pokemon', payload: { newPokemonName } })
}}>
<label htmlFor="add-pokemon-input" style={{ display: 'block' }}>Adicionar pokemon:</label>
<input
id="add-pokemon-input"
value={newPokemonName}
onChange={(e) => setNewPokemonName(e.target.value)}
/>
</form>
<ul>
{pokemons.map((pkm) => <li key={pkm}>{pkm}</li>)}
</ul>
</>
)
}
export default App
Começamos importando nosso hook que nos dá a função action()
ela serve para disparar uma atualização no estado, após o nosso antigo select criei um estado para controlar um input que iremos usar para adicionar itens, e para finalizar tem o HTML de um input simples que ao pressionar enter causa um submit, no caso o pedacinho que precisamos é o que acontece no onSubmit:
// src/App.tsx
// ...
action({ type: 'add-pokemon', payload: { newPokemonName } })
// ...
Basicamente estamos usando a função action()
e passando um objeto que segue a estrutura pré definida, você verá que a tipagem já está toda pronta, ao digitar o type e iniciar a string, o VSCode já trará todas as as opções de reducers criados, ao definir um type, imediatamente o VSCode já definirá como o payload deve ser estruturado seguindo a tipagem que definimos na criação do reducer.
Bom pessoal, esse é o uso básico desse pacote que recem criei, ainda existe um segundo tipo de reducer que é o reducer com efeito colateral, perfeito para criar reducers com Promises, ainda há muita melhoria nesse código e adoraria ter contribuições caso tenham interesse, muito obrigado por ler até aqui e dependendo da aceitação planejo uma parte dois demonstrando efeitos colaterais.