Shiny namespaces
Namespaces
This is a quick post about namespaces in shiny modules. I’ll cover how the NS()
function helps organize IDs in the UI, and how these IDs are accessed in the server with moduleServer()
.
Keeping track of all those inputIds/outputIds
As you can imagine, keeping track of all the IDs (inputId
/input$
and outputId
/output$
) becomes difficult and time consuming. It’s also hard to anticipate which names we’ll need in the future, or the level of precision these names require. This is where modules and namespaces come in handy.
What is a namespace in a shiny app?
“…a namespace is to an ID as a directory is to a file…”
The quote above is from the NS()
help files, and I’ve found it’s a great way to conceptualize what NS()
is creating.
If we imagine creating two files with identical names (file.R
and file.R
) we quickly discover they can’t exist in the same directory. See what happens below when we try to create them in a directory named folder
:
fs::dir_create("folder") # create directory
fs::file_create("folder/file.R") # create file 1
fs::file_create("folder/file.R") # create file 2 (same name)
fs::dir_tree("folder") # check files in directory
folder/
└── file.R
We can see there is only one file.R
in the folder/
directory (the second fs::file_create("folder/file.R")
overwrote the first file.R
). You’ve probably seen an error like this when trying to save two files with the same name into the same folder.
However, we can have files with identical names in two different folders, but we have to reorganize the structure. Below we’ve created a parent folder (folder/
) with two sub-folders (sub-folder-A/
and sub-folder-B/
), where we can place each file:
folder/
├── sub-folder-A/
│ └── file.R
└── sub-folder-B/
└── file.R
By placing the files in sub-directories, we’ve made the path to each file unique, which makes it possible for them to have identical names. We can access these files using their unique path (not their unique name). The NS()
function works in a similar way by creating a unique ‘path’ for each ID the module.
Using NS()
Sometimes you’ll see NS()
used to create a separate ns()
function, which is then used to define IDs in the module (i.e. ns <- shiny::NS(id)
then ns("[name]"))
:
text_example_01_UI <- function(id) {
ns <- shiny::NS(id)
tagList(
textAreaInput(inputId = ns("text_in"),
label = "Text"),
verbatimTextOutput(ns("text_out"))
)
}
text_example_01_UI(id = "text")
Other times you’ll see it written explicitly (with NS(namespace = id, id = "[name]")
).
text_example_02_UI <- function(id) {
tagList(
textAreaInput(inputId = NS(namespace = id,
id = "text_in"),
label = "Text"),
verbatimTextOutput(outputId = NS(namespace = id,
id = "text_out"))
)
}
text_example_02_UI(id = "text")
As you can see, both methods work. This is because “if id
is missing, [NS()
] returns a function that expects an id
string as its only argument and returns that id
with the namespace prepended.”
To see what the unique ‘location’ for a namespaced ID looks like, we can define our namespace in a string (“text”), then create a new object from NS()
(name_Spaceded()
) and examine it’s contents:
# define a namespace
id <- "text"
# create the function
name_Spaceded <- NS(namespace = id)
# view the contents
name_Spaceded
## function (id)
## {
## if (length(id) == 0)
## return(ns_prefix)
## if (length(ns_prefix) == 0)
## return(id)
## paste(ns_prefix, id, sep = ns.sep)
## }
## <bytecode: 0x7fe6365d39f0>
## <environment: 0x7fe63eb85b30>
As we can see, the function created by NS()
is pasting together two strings: the ns_prefix
(the namespace prefix) and the id
.
So if we supply an empty id
to name_Spaceded()
# empty id
name_Spaceded(id = "")
## [1] "text-"
We can see the first id
we passed was the name for the namespace. When we supply a character string to name_Spaceded()
(imitating an inputId
or outputId
)…
# "namespace" the id
name_Spaceded("text_in")
## [1] "text-text_in"
name_Spaceded()
has replaced what would be input$text_in
–shared globally across our entire application–with input$text_in
, which is isolated in the text
module.
Module ‘directory’ structure
Just like we can’t have identical file names in the same folder, we can’t have two IDs with the same name in the same app. To get around this, we create modules by pairing NS()
with moduleServer()
:
In the UI module,
NS()
isolates the IDs into a namespace (pasting togetherns_prefix
andid
), which we can access viamoduleServer()
.In the server module, the
moduleServer()
function includes bothid
andmodule
arguments. Theid
will be linked to it’s complimentary UI function, andmodule
is defined just like the standard shinyserver
function (function(input, output, session)
). There is also asession
argument, but it’s almost always set to the default value.
Below is an application folder-tree that mimics how IDs are contained within a module namespaces:
app/
└── module-plot/ # plot module UI namespace
└── UI/
│ ├── inputId=NS(namespace = id, id = "x_variable")
│ ├── inputId=NS(namespace = id, id = "y_variable")
│ ├── inputId=NS(namespace = id, id = "color_variable")
│ └── outputId=NS(namespace = id, id = "table")
└── server/ # plot module (server)
└── moduleServer(id = id,
module = function(input, output, session))
Within our
app/
directory, we have theplot
module with **inputId
**s (referenced in the server asinput$
) and anoutputId
(referenced in the server asoutput$
)These IDs must be unique within the module’s namespace (created with
NS()
), but they no longer have to be unique within the app)We use the
NS()
function to isolate and name the IDs into a namespace (i.e.id = "[name]"
), then we access these IDs in the server withmoduleServer()
(which we will cover below).
Example UI
Below is an example UI function for a module with input and output functions and their IDs (input$x_variable
, input$y_variable
, and output$plot
).
example_module_UI <- function(id) {
tagList(
# input
selectInput(
inputId = NS(namespace = id,
id = "x_variable")
# ...additional arguments omitted...
),
# input
selectInput(
inputId = NS(namespace = id,
id = "y_variable")
# ...additional arguments omitted...
),
# input
selectInput(
inputId = NS(namespace = id,
id = "color_variable")
# ...additional arguments omitted...
),
# output
plotOutput(
outputId = NS(namespace = id,
id = "plot"))
)
}
Example server
When we build the server component, we include a call to moduleServer()
, and assume the server will have access to the same inputs and outputs defined in the UI:
example_module_Server <- function(id) {
moduleServer(id = id, module = function(input, output, session) {
# data reactive
data <- reactive(
select(
<data>,
all_of(c(input$x_variable,
input$y_variable,
input$color_variable))
)
)
# render plot
output$plot <- Output({
data()
})
})
}
Example app
When we build the app, we know the id
argument will be shared between the UI (example_module_UI()
) and server (example_module_Server()
) functions:
ui <- fluidPage(
example_module_UI(id = "example")
)
server <- function(input, output, session) {
example_module_Server(id = "example")
}
shinyApp(ui, server)
The link between the IDs created with NS()
in the ui
and the moduleServer()
function in the server
is the id
.
Recap
Modules make it easier to combine inputs and outputs into the same app without having to worry about namespace collision. Read more about modules in Engineering Production-Grade Shiny Apps and Mastering Shiny.