Utilizando Ports e Flags para salvar e ler dados do localStorage
Neste tutorial vamos salvar os valores que recebemos do servidor utilizando a API de localStorage
. No episódio 5 ao introduzir o Browser.element
, falei sobre as diferentes formas de comunicar com o mundo exterior: Cmd
, Sub
, flags
e ports
. Hoje vamos aprender flags
e ports
, são as duas features que nos possibilitam comunicar com JavaScript.
O que é localStorage
Local storage é um mini banco de dados que vem incluso no navegador, podemos persistir dados no formato de chave e valor (o valor só pode ser do tipo string) e esses dados não serão perdidos quando o usuário sair da aplicação e nem quando o browser for encerrado.
Podemos salvar dados no localStorage
utilizando JavaScript dessa maneira:
window.localStorage.setItem('myName', 'Clayton')
E podemos recuperar essa informação utilizando o getItem
, só precisamos utilizar a mesma chave que utilizamos para salvar a informação:
window.localStorage.getItem('myName')
A API é bem simples e objetiva, mas infelizmente o Elm ainda não possui acesso nativo a essa, e muitas outras, API do browser. Para isso, vamos precisar utilizar Ports
, que é uma feature do Elm que nos permite fazer comunicação com o JS.
Utilizando Ports
Para possibilitar a criação de uma Port, precisamos adicionar a keyword port
na primeira linha do módulo:
- module Main exposing (main)
+ port module Main exposing (main)
Em seguida, quase no final do arquivo (antes da função main
), vamos criar uma port
:
+ port saveCurrencies : List CurrencyRate -> Cmd msg
main : Program () Model Msg
main =
Agora vamos chamar essa Port assim que recebemos os dados da API de currencies
:
GotCurrencyRates response ->
case response of
Ok data ->
- ( { model | currencies = Success data }, Cmd.none )
+ ( { model | currencies = Success data }, saveCurrencies data )
Err _ ->
( { model | currencies = Error "Erro ao carregar as moedas" }, Cmd.none )
Tá quase tudo pronto, mas ainda falta o principal: escrever o código JS que vai se comunicar com essa Port. Para fazer isso, vamos abrir o arquivo src/index.js
e adicionar algumas linhas de código:
import './main.css';
import { Elm } from './Main.elm';
import * as serviceWorker from './serviceWorker';
- Elm.Main.init({
+ const app = Elm.Main.init({
node: document.getElementById('root')
});
+
+app.ports.saveCurrencies.subscribe(function (currencies) {
+ window.localStorage.setItem('currencies', JSON.stringify(currencies));
+});
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Agora, ao abrir o conversor, aperte F2
, abra a aba Application
e clique em Local Storage
, se tudo der certo, veremos os dados que foram salvos:
Agora vamos entender, parte por parte, o que fizemos:
port module Main exposing (main)
Primeiro adicionamos a keyword port
para informar ao compilador que o módulo Main
possui ports.
port saveCurrencies : List CurrencyRate -> Cmd msg
Em seguida, criamos uma port chamada saveCurrencies
. Ela é uma função que vai receber uma lista de CurrencyRate
(os dados que salvaremos no localStorage) e retorna uma Cmd
.
( { model | currencies = Success data }, saveCurrencies data )
Depois disso, ao receber as currencies
do servidor, utilizamos essa função, com os dados que acabamos de receber, no segundo parâmetro da tupla, assim o Elm saberá que desejamos que ele execute esse comando. Quando esse comando é executado, uma mensagem será emitida de forma que o JS consiga interpretar.
app.ports.saveCurrencies.subscribe(function (currencies) {
Quando iniciamos uma aplicação Elm, a função init
nos retorna um objeto app
que possui várias informações sobre o módulo que foi iniciado. Dentre essas informações estão as ports, que podemos acessar através de app.ports
. Cada port tem um método subscribe
, ele serve para que o JS saiba quando alguma mensagem foi enviada pelo Elm. Assim, passamos uma função como callback para o subscribe
, e essa função será executada sempre que essa port for chamada no Elm.
app.ports.saveCurrencies.subscribe(function (currencies) {
window.localStorage.setItem('currencies', JSON.stringify(currencies));
});
No callback da função nós salvamos os dados que enviamos dentro do Elm.
Utilizando Flags para passar valores iniciais
Agora que já temos os dados salvos no localStorage
, podemos pegar esses dados e passar para a nossa aplicação assim que ela iniciar. Faremos isso utilizando Flags, elas são bastante utilizadas para passar API Keys de serviços, variáveis de ambiente e informações do usuário.
Para passar flags para o Elm, fazemos isso através do atributo flag
da função init
:
const app = Elm.Main.init({
node: document.getElementById('root'),
+ flags: {},
})
Vamos criar uma função no JS responsável por pegar os dados no localStorage
:
function getCurrencies() {
try {
const currencies = window.localStorage.getItem('currencies')
return JSON.parse(currencies)
} catch (e) {
return []
}
}
E então utilizamos ela no Elm.Main.init
:
const app = Elm.Main.init({
node: document.getElementById('root'),
- flags: {},
+ flags: getCurrencies(),
})
Quase pronto. Agora no init
do Elm, precisamos utilizar essas flags e salvá-las na model:
- init : () -> ( Model, Cmd Msg )
+ init : List CurrencyRate -> ( Model, Cmd Msg )
- init _ =
+ init currencies =
( { from = "BRL"
, to = "EUR"
, amount = 1
- , currencies = Loading
+ , currencies = Success currencies
}
, getCurrencyRates
)
Também precisamos atualizar a assinatura de tipo da função main
:
- main : Program () Model Msg
+ main : Program (List CurrencyRate) Model Msg
main =
Agora, se você já tiver com os dados no localStorage
, perceberá que a tela de carregamento nem aparece. Isso é porque a aplicação iniciou com os dados que recebemos nas flags.
Mas o que acontece se a gente não tiver dados no localStorage
? Vamos testar:
Como pudemos ver, a aplicação nem inicia! Precisamos tomar todo cuidado possível quando utilizamos flags, pois caso o valor que passamos for diferente do que o init
espera, a aplicação vai crashar. Podemos corrigir isso de duas maneiras. A primeira é corrigindo a função getCurrencies
no JS, e a segunda é utilizando Decoders para decodificar o valor que recebemos nas flags. Vamos fazer os dois.
Primeiro vamos corrigir a função getCurrencies
de forma que ela retorne uma lista vazia, ao invés de null
, quando não houver dados.
// src/index.js
function getCurrencies() {
try {
const currencies = window.localStorage.getItem('currencies')
- return JSON.parse(currencies)
+ return JSON.parse(currencies) || []
} catch (e) {
return []
}
}
Com isso, o conversor já volta a funcionar corretamente. Mas vamos implementar a segunda maneira, pois para mim, é a forma mais correta de lidar com flags
.
Decodificando Flags para evitar problemas
Vamos começar alterando o tipo das Flags, agora será do tipo Json.Encode.Value
. Basicamente, esse tipo representa um valor do JavaScript (null
, undefined
, string
, etc).
Primeiro passo: importar o pacote Json.Decode
:
port module Main exposing (main)
import Browser
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (class, selected, type_, value)
import Html.Events exposing (onInput, onSubmit)
import Http
import Json.Decode
+ import Json.Encode
Em seguida vamos alterar a assinatura da função init
e main
:
- init : List CurrencyRate -> ( Model, Cmd Msg )
+ init : Json.Encode.Value -> ( Model, Cmd Msg )
- init currencies =
+ init flags =
- main : Program (List CurrencyRate) Model Msg
+ main : Program Json.Encode.Value Model Msg
main =
Quase lá! Agora só precisamos decodificar essas flags e salvar na model. Essa tarefa vai ficar a cargo da função Json.Decode.decodeValue
, que é responsável por decodificar valores do tipo Json.Encode.Value
. A função init
ficará dessa maneira:
init : Json.Encode.Value -> ( Model, Cmd Msg )
init flags =
let
currencies =
case Json.Decode.decodeValue (Json.Decode.list currencyRateDecoder) flags of
Ok decodedCurrencies ->
Success decodedCurrencies
_ ->
Loading
in
( { from = "BRL"
, to = "EUR"
, amount = 1
, currencies = currencies
}
, getCurrencyRates
)
Pronto! Agora nossa aplicação está segura novamente (jajá faremos alguns testes).
Agora vamos entender o que ta acontecendo.
case Json.Decode.decodeValue (Json.Decode.list currencyRateDecoder) flags of
Aqui utilizamos a função decodeValue
dizendo que esperamos que o valor seja uma lista de CurrencyRate
através do decoder que já tínhamos criado antes (Json.Decode.list currencyRateDecoder
) e passamos as flags
no último parâmetro. O resultado dessa função é do tipo Result
, então precisamos tratar a possibilidade de Result.Ok
e Result.Err
.
case Json.Decode.decodeValue (Json.Decode.list currencyRateDecoder) flags of
Ok decodedCurrencies ->
Success decodedCurrencies
_ ->
Loading
Se o valor for decodificado corretamente, vamos utilizar o valor decodificado, quando algum erro ocorrer, definimos o valor inicial de currencies
como Loading
.
Pronto, depois de configurado as portas dessa maneira, nossa aplicação se torna bem mais resiliente à erros do JavaScript e não irá quebrar caso algum valor seja inesperado.
Para testar isso, basta passar undefined
como flag
e abrir a aplicação. O esperado é que ela inicie normalmente.
Código atualizado: https://github.com/FidelisClayton/elm-currency-app/tree/parte-7
Conclusão
Aprendemos a utilizar Ports
e Flags
, que são duas funcionalidades que serão muito utilizadas quando você estiver trabalhando com grandes aplicações ou estiver migrando uma aplicação JS para Elm. É importante lembrar que com a utilização do JS, tornaremos nossa aplicação mais suscetível a erros, então tome todo cuidado possível e lembre-se de sempre utilizar Json.Encode.Value
quando for mandar algum valor para dentro do Elm.
Por hoje é isso, espero que tenha gostado. Até a próxima!