-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathreatividade-mais-pecas.qmd
289 lines (190 loc) · 12.1 KB
/
reatividade-mais-pecas.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
```{r, echo = FALSE}
knitr::opts_chunk$set(
fig.align = "center"
)
```
# Reatividade: mais peças {#sec-reatividade2}
No [Capítulo -@sec-reatividade], vimos que o fluxo de reatividade é disparado por uma mudança em um valor reativo e que termina na atualização de uma função observadora^[Isto é, todos os caminhos de um diagrama de reatividade devem começar com um valore reativo e terminar com uma função observadora]. Também vimos que os inputs fazem o papel de valores reativos e as funções `render*()` que criam nossos outputs fazem o papel de funções observadoras.
A depender do tipo de interação que queremos construir no app, vamos precisar de valores reativos que não são inputs e de funções observadoras que não estão associadas a um output. Felizmente o pacote `shiny` possui formas de fazer isso. Apresentar essas novas peças será o objetivo deste capítulo.
## Mais funções observadoras
As funções observadoras são o ponto final do diagrama de reatividade e sem eles o fluxo reativo não acontece. As funções `render*()`, que geram os nossos outputs, são o tipo mais comum de funções observadoras, mas não são o único.
Muitas vezes queremos usar a reatividade para disparar ações que não estão ligadas à geração de outputs, como o registro de informações em bases de dados, o envio de e-mails ou a atualização de informações nos inputs^[Sem a necessidade de recriá-los do zero com `uiOutput()` e `renderUI()`.]. Nesses casos, precisamos utilizar as funções `observe()` e `observeEvent()`.
A função `observe()` monitora os valores e expressões reativas que estão dentro dela e roda seu código quando algum desses valores são modificados. Ao contrário da função `reactive()`, ela não cria um novo valor reativo. O código atribuído a ela é o ponto chegada de um fluxo reativo, isto é, a ação que a função `observe()` executa é o objetivo final do fluxo.
Essa função é muito utilizada com as funções da família `update*()`, que servem para atualizar valores de um input na UI. Na segunda caixa de seleção do exemplo a seguir, queremos selecionar apenas os filmes do diretor ou diretora que selecionamos na primeira caixa. Veja que usamos o texto `Carregando...` como um *placeholder* para o segundo `selectInput()`.
```{r, eval = FALSE}
library(shiny)
# install.packages("dados")
carros <- dados::comuns
ui <- fluidPage(
selectInput(
"marca",
"Selecione uma marca",
choices = sort(unique(carros$marca))
),
selectInput(
"modelo",
"Selecione um modelo",
choices = "Carregando..."
)
)
server <- function(input, output, session) {
observe({
opcoes <- carros |>
dplyr::filter(marca == input$marca) |>
dplyr::pull(modelo)
updateSelectInput(
session,
inputId = "modelo",
choices = opcoes
)
})
}
shiny::shinyApp(ui, server)
```
Na função `server`, atualizamos as escolhas da segunda caixa de seleção com a função `updateSelectInput()`. Veja que, como essa função está dentro de um `observe`, esse código será rodado novamente sempre que o valor de `input$direcao` mudar.
Nesse exemplo, o objetivo final do fluxo reativo é atualizar as opções da segunda caixa de seleção sempre que alterarmos o valor da primeira. Repare que não há nenhum output. A reatividade só funciona nesse caso porque a função `observe()` é uma função observadora.
A função `observeEvent()` funciona assim como a `observe()`, mas ela escuta apenas um valor ou expressão reativa, que é definido em seu primeiro argumento, assim como na função `eventReactive()`. Ela é muito utiliza para disparar ações, como gravar informações em uma base de dados, a partir de botões.
No exemplo a seguir, queremos salvar o e-mail de uma pessoa quando ela clicar no botão "Enviar dados". A função `observeEvent()` roda o código definido dentro dela quando o botão é clicado, salvando o e-mail em um arquivo de texto.
```{r, eval = FALSE}
library(shiny)
ui <- fluidPage(
textInput("email", "Informe seu e-mail"),
actionButton("enviar", "Enviar dados")
)
server <- function(input, output, session) {
observeEvent(input$enviar, {
write(input$email, "emails.txt", append = TRUE)
})
}
```
As funções `observe()` e `observeEvent()` aumentam bastante o leque de opções dos nossos aplicativos. Agora conseguimos criar fluxos reativos que não estão associados necessariamente a um output.
## Mais valores reativos
Já discutimos anteriormente que os valores reativos são o início do diagrama de reatividade e que os valores da lista `input` são o principal tipo de valor reativo em um shiny app.
Em alguns casos, no entanto, vamos precisar de valores reativos que não são inputs, isto é, não estão associados a ações vindas da UI. Esses valores reativos servirão para contralar a reatividade, disparando-a diretamente a partir do servidor. Como não podemos escrever na lista `input`, precisamos de uma nova peça para criar esses valores: a função `reactiveVal()`.
Para criar um valor reativo utilizando essa função, utilizamos a seguinte notação:
```{r, eval = FALSE}
vr <- reactiveVal(1)
```
Isso criará um valor reativo chamado `vr` que possui, inicialmente, o valor 1.
Para acessar esse valor, fazemos:
```{r, eval = FALSE}
vr()
```
Esse código retornará o valor 1. Repare que é a mesma notação das expressões reativas, criadas com as funções `reactive()` ou `eventReactive()`.
Para alterar o valor de um valor reativo, fazemos:
```{r, eval = FALSE}
vr(2)
```
Dessa maneira, o `vr` passa a guardar o valor 2 e, se rodarmos `vr()` novamente, receberemos o valor 2 dentro de 1. Além disso, sempre que alteramos o valor de um valor reativo, ele vai disparar reatividade. Isso quer dizer que todos as expressões reativas e funções observadoras que dependerem de `vr()` serão invalidadas e seus códigos rodados novamente.
Também podemos usar a função `reactiveValues()` para criar valores reativos. Com ela, podemos criar uma lista de valores, em vez de apenas um. A notação nesse caso será a seguinte:
```{r, eval = FALSE}
# Para criar os valores reativos
rv <- reactiveValues(a = 1, b = 2)
# Para acessar os valores
rv$a
rv$b
# Para atualizar os valores
rv$a <- 3
rv$b <- 4
```
Um caso em que criar valores reativos no servidor se torna útil aparece quando precisamos modificar a base de dados que alimenta os outputs a partir de alguma ação na UI, como a possibilidade de adicionar ou remover uma linha. Veja o exemplo abaixo.
```{r, eval = FALSE}
library(shiny)
ui <- fluidPage(
titlePanel("Exemplo reactValues"),
sidebarLayout(
sidebarPanel(
h3("Remover uma linha"),
numericInput(
"linha",
label = "Escolha uma linha para remover",
value = 1,
min = 1,
max = nrow(mtcars)
),
actionButton("remover", label = "Clique para remover"),
),
mainPanel(
reactable::reactableOutput("tabela")
)
)
)
server <- function(input, output, session) {
rv_mtcars <- reactiveVal(value = mtcars)
observeEvent(input$remover, {
nova_mtcars <- rv_mtcars() |>
dplyr::slice(-input$linha)
rv_mtcars(nova_mtcars)
})
output$tabela <- reactable::renderReactable({
rv_mtcars() |>
reactable::reactable(width = 600)
})
}
shinyApp(ui, server)
```
Repare que a base mtcars foi transformada em um valor reativo chamado `rv_mtcars`. Assim, sempre que a instrução de remover uma linha é feita na UI a partir do botão `remover`, o `rv_mtcars` é atualizado com a base sem a linha escolhida. Como o output `tabela` depende de `rv_mtcars`, a tabela na tela também é atualizada^[Se você não conhece o pacote `reactable`, falaremos dele na Seção @sec-reactable.].
Como desafio, tente refazer esse app sem a criar valores reativos no servidor, utilizando expressões reativos, por exemplo. É possível?
## Validação {#sec-validacao}
O pacote `shiny` possui algumas funções que nos ajudam a validar valores reativos antes de rodarmos um código que gera um output. Na UI, isso impede que mensagens de erros internas do R apareçam na tela e nos possibilita enviar mensagens quando quem está usando o app faz algo que não deveria. Internamente, nos permite controlar melhor a reatividade e deixa o app mais eficiente.
Nas próximas seções falaremos da função `req()` e da função `validate()`.
### A função `req()`
A função `req(x)` retorna um erro silencioso caso `x` seja inválido. O erro é silencioso pois não possui mensagem, então nada aparecerá na tela. Aqui, *inválido* indica qualquer um dos seguintes valores:
- `FALSE`
- `NULL`
- `""`, uma string vazia
- Um vetor vazio (e.g., `character(0)`)
- Um vetor que contenha apenas `NA`
- Um vetor lógico que contenha apenas `FALSE` ou `NA`
- Um objeto com classe `try-error`
- Um valor reativo que represente um `actionButton()` que ainda não foi clicado
Você também pode testar diretamente se um valor é *inválido* utilizando a função `isTruthy`.
O erro silencioso é passado adiante, até o *observer* que está sendo recalculado. Se você utilizar a opção `cancelOutput = TRUE` e estiver recalculando uma função `render`, o output associado será mantido no estado atual, isto é, não será substituído por uma tela vazia caso o valor testado seja inválido.
Veja um exemplo de utilização da função `req()`. No código abaixo, a `infoBox` só será criada se o valor reativo `input$filme` tiver um valor válido (no caso, uma string não vazia). Caso o valor seja inválido, a infoBox não será mostrada no app. Nenhuma mensagem de erro ou aviso será retornado.
```{r, eval = FALSE}
# server
output$orcamento <- renderInfoBox({
req(input$filme)
orcamento <- imdb %>%
filter(titulo == input$filme) %>%
pull(orcamento)
infoBox(
title = "Orçamento",
value = orcamento
)
})
```
### Mensagens de erro personalizadas
Em vez de gerar um erro silencioso, não mostrando nada na tela, podemos criar uma mensagem de erro customizada quando um valor for inválido ou não cumprir algum requisito. Para isso, utilizamos a função `validate()`. Essa função deve receber um dos três seguintes valores:
- `NULL`, se o valor for válido;
- `FALSE`, se você quiser retornar um erro silencioso, assim como na função `req()`;
- uma string, que será transformada em uma mensagem de erro e mostrada na tela^[Em cor cinza, não o vermelho padrão das mensagens de erro.].
Essa função é muito utilizada com a função `need()`, que recebe um teste lógico e uma string. Se o teste for verdadeiro, ela retorna `NULL` e, se for falso, retorna a string.
No exemplo abaixo, se o `input$filme` não for válido, além de o aplicativo não mostrar a `infoBox`, a mensagem "Nenhum filme selecionado." é mostrada na tela explicando o porquê. No código, utilizamos `isTruthy(input$filme)` para testar se `input$filme` é válido.
```{r, eval = FALSE}
#server
output$orcamento <- renderInfoBox({
validate(
need(isTruthy(input$filme), message = "Nenhum filme selecionado.")
)
orcamento <- imdb %>% filter(titulo == input$filme) %>% pull(orcamento)
infoBox(
title = "Orçamento",
value = orcamento
)
})
```
## Exercícios
1 - Por que precisamos das funções `observe()` e `observeEvent()`? Qual a diferença entre elas?
___
2 - Para que serve a função `reactiveVal()`?
___
3 - Qual a diferença entre as funções `reactiveVal()` e `reactiveValues()`?
___
4 - Quais valores retornam `FALSE` na função `isTruth()`?
___
5 - Para que serve a função `req()`?
___
6 - Utilizando a base `dados::dados_gapminder`, construa um app que tenha um filtro de `continente` e outro de `pais`. Escolhido um `continente`, apenas países do continente escolhido devem permanecer no filtro de `pais`. Como output, seu app deve apresentar as séries de `populacao`, `expectativa_de_vida` e `pib_per_capita` ao longo dos anos disponíveis.
___
7 - Faça um app que contenha um formulário de cadastro com os campos "nome", "e-mail", "idade" e "cidade" e um botão de salvar dados que faça o app salvar as informações em uma planilha no computador. O app também deve mostrar a tabela mais atualizada de pessoas cadastradas.