diff --git a/.Rbuildignore b/.Rbuildignore
index c96266c5..6bcbf8b8 100644
--- a/.Rbuildignore
+++ b/.Rbuildignore
@@ -21,3 +21,4 @@ tests/circleci
 gulp-assets
 node_modules
 scripts
+^LICENSE\.md$
diff --git a/LICENSE b/LICENSE
index f8dd2456..b0fc8b2a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,2 @@
-MIT License
-
-Copyright (c) 2019 Plotly
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+YEAR: 2021
+COPYRIGHT HOLDER: Plotly, Inc.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..9c0d8835
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,23 @@
+Note: Per CRAN rules the file LICENSE has specific requirements, so we include this file LICENSE.md with the complete license text.
+
+# MIT License
+
+Copyright (c) 2022 Plotly, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/R/dash.R b/R/dash.R
index 59a44f1a..d9d0f21c 100644
--- a/R/dash.R
+++ b/R/dash.R
@@ -780,7 +780,7 @@ Dash <- R6::R6Class(
     #'
     #' For detailed examples of how to use pattern-matching callbacks, see the
     #' entry for \link{selectors} or visit our interactive online
-    #' documentation at \url{https://dashr.plotly.com}.
+    #' documentation at \url{https://dash.plotly.com/r/}.
     #'
     #' The `output` argument defines which layout component property should
     #' receive the results (via the [output] object). The events that
@@ -1149,7 +1149,7 @@ Dash <- R6::R6Class(
     #' @param block Logical. Start the server while blocking console input? Default is `TRUE`.
     #' @param showcase Logical. Load the Dash application into the default web browser when server starts? Default is `FALSE`.
     #' @param use_viewer Logical. Load the Dash application into RStudio's viewer pane? Requires that `host` is either `127.0.0.1` or `localhost`, and that Dash application is started within RStudio; if `use_viewer = TRUE` and these conditions are not satisfied, the user is warned and the app opens in the default browser instead. Default is `FALSE`.
-    #' @param debug Logical. Enable/disable all the Dash developer tools (and the within-browser user interface for the callback graph visualizer and stack traces) unless overridden by the arguments or environment variables. Default is `FALSE` when called via `run_server`. For more information, please visit \url{https://dashr.plotly.com/devtools}. Environment variable: `DASH_DEBUG`.
+    #' @param debug Logical. Enable/disable all the Dash developer tools (and the within-browser user interface for the callback graph visualizer and stack traces) unless overridden by the arguments or environment variables. Default is `FALSE` when called via `run_server`. For more information, please visit \url{https://dash.plotly.com/r/devtools}. Environment variable: `DASH_DEBUG`.
     #' @param dev_tools_ui Logical. Show Dash's developer tools UI? Default is `TRUE` if `debug == TRUE`, `FALSE` otherwise. Environment variable: `DASH_UI`.
     #' @param dev_tools_hot_reload Logical. Activate hot reloading when app, assets, and component files change? Default is `TRUE` if `debug == TRUE`, `FALSE` otherwise. Requires that the Dash application is loaded using `source()`, so that `srcref` attributes are available for executed code. Environment variable: `DASH_HOT_RELOAD`.
     #' @param dev_tools_hot_reload_interval Numeric. Interval in seconds for the client to request the reload hash. Default is `3`. Environment variable: `DASH_HOT_RELOAD_INTERVAL`.
diff --git a/R/utils.R b/R/utils.R
index 8e0063a2..d7eea0e5 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -338,12 +338,26 @@ clean_dependencies <- function(deps) {
 }
 
 insertIntoCallbackMap <- function(map, inputs, output, state, func, clientside_function) {
-  map[[createCallbackId(output)]] <- list(inputs=inputs,
-                                          output=output,
-                                          state=state,
-                                          func=func,
-                                          clientside_function=clientside_function
-                                          )
+  output_id <- createCallbackId(output)
+
+  if (output_id %in% names(map)) {
+    stop(
+      sprintf(
+        "One or more of the following outputs are duplicated across callbacks: %s. Please ensure that all ID and property combinations are unique.",
+        output_id
+      ),
+      call. = FALSE
+    )
+  }
+
+  map[[output_id]] <- list(
+    inputs = inputs,
+    output = output,
+    state = state,
+    func = func,
+    clientside_function = clientside_function
+  )
+
   if (length(map) >= 2) {
     ids <- lapply(names(map), function(x) getIdProps(x)$ids)
     props <- lapply(names(map), function(x) getIdProps(x)$props)
@@ -1139,7 +1153,7 @@ createCallbackId <- function(output) {
 }
 
 getIdProps <- function(output) {
-  output_ids <- strsplit(substr(output, 3, nchar(output)-2), '...', fixed=TRUE)
+  output_ids <- strsplit(gsub("^\\.{2}|\\.{2}$", "", output), '...', fixed=TRUE)
   idprops <- lapply(output_ids, strsplit, '.', fixed=TRUE)
   ids <- vapply(unlist(idprops, recursive=FALSE), '[', character(1), 1)
   props <- vapply(unlist(idprops, recursive=FALSE), '[', character(1), 2)
diff --git a/README.md b/README.md
index 6e03dd36..831e5667 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,18 @@
-[![CircleCI](https://circleci.com/gh/plotly/dashR/tree/master.svg?style=svg)](https://circleci.com/gh/plotly/dashR/tree/master)
+[![CircleCI](https://circleci.com/gh/plotly/dashR/tree/master.svg?style=svg)](https://app.circleci.com/pipelines/github/plotly/dashR?branch=master)
 [![GitHub](https://img.shields.io/github/license/plotly/dashR.svg?color=dark-green)](https://github.com/plotly/dashR/blob/master/LICENSE)
-[![CRAN status](https://www.r-pkg.org/badges/version-ago/dash)](https://cran.r-project.org/web/packages/dash/index.html)
-[![](http://cranlogs.r-pkg.org/badges/grand-total/dash)](https://cran.r-project.org/package=dash)
-[![](https://cranlogs.r-pkg.org/badges/dash)](https://cran.r-project.org/package=dash)
+[![CRAN status](https://www.r-pkg.org/badges/version-ago/dash)](https://CRAN.R-project.org/package=dash)
+[![](http://cranlogs.r-pkg.org/badges/grand-total/dash)](https://CRAN.R-project.org/package=dash)
+[![](https://cranlogs.r-pkg.org/badges/dash)](https://CRAN.R-project.org/package=dash)
 
 # Dash for R
 
 #### Create beautiful, analytic web applications in R.
 
-[Documentation](https://dashr.plotly.com/) | [Gallery](https://dash-gallery.plotly.host/Portal/)
+[Documentation](https://dash.plotly.com/r/) | [Gallery](https://dash.gallery/Portal/)
 
 ## Installation
 
-<https://dashr.plotly.com/installation>
+<https://dash.plotly.com/r/installation>
 
 > 🛑 Make sure you're on at least version `3.0.2` of R. You can see what version of R you have by entering `version` in the R CLI. [CRAN](https://cran.r-project.org/bin/) is the easiest place to download the latest R version.
 
@@ -42,9 +42,9 @@ That's it!
 
 ## Getting Started
 
-<https://dashr.plotly.com/getting-started>
+<https://dash.plotly.com/r/layout>
 
-The R package **dash** makes it easy to create reactive web applications powered by R. It provides an [R6](https://cran.r-project.org/web/packages/R6/index.html) class, named `Dash`, which may be initialized via the `new()` method.
+The R package **dash** makes it easy to create reactive web applications powered by R. It provides an [R6](https://CRAN.R-project.org/package=R6) class, named `Dash`, which may be initialized via the `new()` method.
 
 ```r
 library(dash)
@@ -55,24 +55,28 @@ app <- Dash$new()
 Similar to [Dash for Python](https://github.com/plotly/dash) and [Dash for Julia](https://github.com/plotly/Dash.jl), every Dash for R application needs a layout (i.e., user interface) and a collection of callback functions which define the updating logic to perform when input value(s) change. Take, for instance, this basic example of formatting a string:
 
 ```r
-app$layout(
-  htmlDiv(
-    list(
-      dccInput(id = "inputID", value = "initial value", type = "text"),
-      htmlDiv(id = "outputID")
-    )
-  )
-)
-
-app$callback(output = list(id="outputID", property="children"),
-             params = list(input(id="inputID", property="value"),
-                      state(id="inputID", property="type")),
-  function(x, y) {
-    sprintf("You've entered: '%s' into a '%s' input control", x, y)
-  }
-)
+library(dash)
 
-app$run_server(showcase = TRUE)
+dash_app() %>%
+  set_layout(
+    dccInput(id = "text", "sample"),
+    div("CAPS: ", span(id = "out1")),
+    div("small: ", span(id = "out2"))
+  ) %>%
+  add_callback(
+    list(
+      output("out1", "children"),
+      output("out2", "children")
+    ),
+    input("text", "value"),
+    function(text) {
+      list(
+        toupper(text),
+        tolower(text)
+      )
+    }
+  ) %>%
+  run_app()
 ```
 
 Here the `showcase = TRUE` argument opens a browser window and automatically loads the Dash app for you.
@@ -80,71 +84,38 @@ Here the `showcase = TRUE` argument opens a browser window and automatically loa
 ## Hello world example using `dccGraph`
 
 ```r
-app <- Dash$new()
+library(dash)
 
-app$layout(
-  htmlDiv(
-    list(
-      dccInput(id = "graphTitle",
-               value = "Let's Dance!",
-               type = "text"),
-      htmlDiv(id = "outputID"),
-      dccGraph(id = "giraffe",
-               figure = list(
-                 data = list(x = c(1,2,3), y = c(3,2,8), type = "bar"),
-                 layout = list(title = "Let's Dance!")
-               )
-      )
+# Create a Dash app
+app <- dash_app()
+
+# Set the layout of the app
+app %>% set_layout(
+  h1('Hello Dash'),
+  div("Dash: A web application framework for your data."),
+  dccGraph(
+    figure = list(
+      data = list(
+        list(
+          x = list(1, 2, 3),
+          y = list(4, 1, 2),
+          type = 'bar',
+          name = 'SF'
+        ),
+        list(
+          x = list(1, 2, 3),
+          y = list(2, 4, 5),
+          type = 'bar',
+          name = 'Montr\U{00E9}al'
+        )
+      ),
+      layout = list(title = 'Dash Data Visualization')
     )
   )
 )
 
-app$callback(output = list(id = "giraffe", property = "figure"),
-             params = list(input("graphTitle", "value")),
-             function(newTitle) {
-
-                 rand1 <- sample(1:10, 1)
-
-                 rand2 <- sample(1:10, 1)
-                 rand3 <- sample(1:10, 1)
-                 rand4 <- sample(1:10, 1)
-
-                 x <- c(1,2,3)
-                 y <- c(3,6,rand1)
-                 y2 <- c(rand2,rand3,rand4)
-
-                 df = data.frame(x, y, y2)
-
-                 list(
-                   data =
-                     list(
-                       list(
-                         x = df$x,
-                         y = df$y,
-                         type = "bar"
-                       ),
-                       list(
-                         x = df$x,
-                         y = df$y2,
-                         type = "scatter",
-                         mode = "lines+markers",
-                         line = list(width = 4)
-                       )
-                     ),
-                   layout = list(title = newTitle)
-                 )
-               }
-)
-
-app$callback(output = list(id = "outputID", property = "children"),
-             params = list(input("graphTitle", "value"),
-                           state("graphTitle", "type")),
-             function(x, y) {
-                 sprintf("You've entered: '%s' into a '%s' input control", x, y)
-             }
-)
-
-app$run_server(showcase = TRUE)
+# Run the app
+app %>% run_app()
 ```
 
 ---
diff --git a/man/Dash.Rd b/man/Dash.Rd
index e10406c3..31bd2b8b 100644
--- a/man/Dash.Rd
+++ b/man/Dash.Rd
@@ -500,7 +500,7 @@ information but do not trigger the callback directly.
 
 For detailed examples of how to use pattern-matching callbacks, see the
 entry for \link{selectors} or visit our interactive online
-documentation at \url{https://dashr.plotly.com}.
+documentation at \url{https://dash.plotly.com/r/}.
 
 The \code{output} argument defines which layout component property should
 receive the results (via the \link{output} object). The events that
@@ -857,7 +857,7 @@ Start the Fiery HTTP server and run a Dash application.
 
 \item{\code{dev_tools_prune_errors}}{Logical. Reduce tracebacks such that only lines relevant to user code remain, stripping out Fiery and Dash references? Only available with debugging. \code{TRUE} by default, set to \code{FALSE} to see the complete traceback. Environment variable: \code{DASH_PRUNE_ERRORS}.}
 
-\item{\code{debug}}{Logical. Enable/disable all the Dash developer tools (and the within-browser user interface for the callback graph visualizer and stack traces) unless overridden by the arguments or environment variables. Default is \code{FALSE} when called via \code{run_server}. For more information, please visit \url{https://dashr.plotly.com/devtools}. Environment variable: \code{DASH_DEBUG}.}
+\item{\code{debug}}{Logical. Enable/disable all the Dash developer tools (and the within-browser user interface for the callback graph visualizer and stack traces) unless overridden by the arguments or environment variables. Default is \code{FALSE} when called via \code{run_server}. For more information, please visit \url{https://dash.plotly.com/r/devtools}. Environment variable: \code{DASH_DEBUG}.}
 
 \item{\code{dev_tools_ui}}{Logical. Show Dash's developer tools UI? Default is \code{TRUE} if \code{debug == TRUE}, \code{FALSE} otherwise. Environment variable: \code{DASH_UI}.}
 
diff --git a/man/dash-package.Rd b/man/dash-package.Rd
index 15d5ef73..3ad3c077 100644
--- a/man/dash-package.Rd
+++ b/man/dash-package.Rd
@@ -15,12 +15,12 @@ Dash apps are rendered in the web browser. You can deploy your apps to servers a
 
 There is a lot behind the framework. To learn more about how it is built and what motivated Dash, watch our talk from \href{https://youtu.be/5BAthiN0htc}{Plotcon} or read our \href{https://medium.com/@plotlygraphs/introducing-dash-5ecf7191b503}{announcement letter}.
 
-Dash is an open source package, released under the permissive MIT license. Plotly develops Dash and offers a \href{https://plotly.com/dash/pricing/}{platform for easily deploying Dash apps in an enterprise environment}. If you're interested, \href{https://plotly.typeform.com/to/rkO85m?_ga=2.223907347.9240264.1560484539-2037997284.1554944507}{please get in touch}.
+Dash is an open source package, released under the permissive MIT license. Plotly develops Dash and offers a \href{https://plotly.com/dash/}{platform for easily deploying Dash apps in an enterprise environment}. If you're interested, \href{https://plotly.com/get-pricing/}{please get in touch}.
 }
 \seealso{
 Useful links:
 \itemize{
-  \item \url{http://dashr.plotly.com}
+  \item \url{https://dash.plotly.com/r/}
   \item \url{https://github.com/plotly/dashR}
   \item Report bugs at \url{https://github.com/plotly/dashR/issues}
 }
diff --git a/package-lock.json b/package-lock.json
index c723bed9..de3b1254 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,12 +21,12 @@
         "fs-extra": "^9.0.1",
         "gulp": "^4.0.2",
         "gulp-concat": "2.6.1",
-        "gulp-footer": "^2.0.2",
         "gulp-print": "^5.0.2",
         "gulp-rename": "^2.0.0",
         "gulp-replace": "^1.0.0",
         "mkdirp": "^0.5.4",
         "prettier": "^1.14.2",
+        "set-value": ">=4.0.1",
         "shelljs": "0.8.4"
       }
     },
@@ -874,6 +874,33 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/cache-base/node_modules/extend-shallow": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+      "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+      "dev": true,
+      "dependencies": {
+        "is-extendable": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/cache-base/node_modules/set-value": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+      "dev": true,
+      "dependencies": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/call-bind": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -2870,19 +2897,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/gulp-footer": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/gulp-footer/-/gulp-footer-2.0.2.tgz",
-      "integrity": "sha512-HsG5VOgKHFRqZXnHGI6oGhPDg70p9pobM+dYOnjBZVLMQUHzLG6bfaPNRJ7XG707E+vWO3TfN0CND9UrYhk94g==",
-      "dev": true,
-      "dependencies": {
-        "lodash._reescape": "^3.0.0",
-        "lodash._reevaluate": "^3.0.0",
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.template": "^3.6.2",
-        "map-stream": "0.0.7"
-      }
-    },
     "node_modules/gulp-print": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/gulp-print/-/gulp-print-5.0.2.tgz",
@@ -3507,6 +3521,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-primitive": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
+      "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-regex": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -3880,125 +3903,6 @@
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
       "dev": true
     },
-    "node_modules/lodash._basecopy": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
-      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
-      "dev": true
-    },
-    "node_modules/lodash._basetostring": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
-      "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=",
-      "dev": true
-    },
-    "node_modules/lodash._basevalues": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
-      "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=",
-      "dev": true
-    },
-    "node_modules/lodash._getnative": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
-      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
-      "dev": true
-    },
-    "node_modules/lodash._isiterateecall": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
-      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
-      "dev": true
-    },
-    "node_modules/lodash._reescape": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
-      "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=",
-      "dev": true
-    },
-    "node_modules/lodash._reevaluate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
-      "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=",
-      "dev": true
-    },
-    "node_modules/lodash._reinterpolate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
-      "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
-      "dev": true
-    },
-    "node_modules/lodash._root": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
-      "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
-      "dev": true
-    },
-    "node_modules/lodash.escape": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
-      "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=",
-      "dev": true,
-      "dependencies": {
-        "lodash._root": "^3.0.0"
-      }
-    },
-    "node_modules/lodash.isarguments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
-      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
-      "dev": true
-    },
-    "node_modules/lodash.isarray": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
-      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
-      "dev": true
-    },
-    "node_modules/lodash.keys": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
-      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
-      "dev": true,
-      "dependencies": {
-        "lodash._getnative": "^3.0.0",
-        "lodash.isarguments": "^3.0.0",
-        "lodash.isarray": "^3.0.0"
-      }
-    },
-    "node_modules/lodash.restparam": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
-      "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
-      "dev": true
-    },
-    "node_modules/lodash.template": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
-      "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=",
-      "dev": true,
-      "dependencies": {
-        "lodash._basecopy": "^3.0.0",
-        "lodash._basetostring": "^3.0.0",
-        "lodash._basevalues": "^3.0.0",
-        "lodash._isiterateecall": "^3.0.0",
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.escape": "^3.0.0",
-        "lodash.keys": "^3.0.0",
-        "lodash.restparam": "^3.0.0",
-        "lodash.templatesettings": "^3.0.0"
-      }
-    },
-    "node_modules/lodash.templatesettings": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
-      "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=",
-      "dev": true,
-      "dependencies": {
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.escape": "^3.0.0"
-      }
-    },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5608,30 +5512,21 @@
       "dev": true
     },
     "node_modules/set-value": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
-      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
-      "dev": true,
-      "dependencies": {
-        "extend-shallow": "^2.0.1",
-        "is-extendable": "^0.1.1",
-        "is-plain-object": "^2.0.3",
-        "split-string": "^3.0.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/set-value/node_modules/extend-shallow": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-      "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz",
+      "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==",
       "dev": true,
+      "funding": [
+        "https://github.com/sponsors/jonschlinkert",
+        "https://paypal.me/jonathanschlinkert",
+        "https://jonschlinkert.dev/sponsor"
+      ],
       "dependencies": {
-        "is-extendable": "^0.1.0"
+        "is-plain-object": "^2.0.4",
+        "is-primitive": "^3.0.1"
       },
       "engines": {
-        "node": ">=0.10.0"
+        "node": ">=11.0"
       }
     },
     "node_modules/shebang-command": {
@@ -6410,6 +6305,33 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/union-value/node_modules/extend-shallow": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+      "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+      "dev": true,
+      "dependencies": {
+        "is-extendable": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/union-value/node_modules/set-value": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+      "dev": true,
+      "dependencies": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/unique-stream": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
@@ -7431,6 +7353,29 @@
         "to-object-path": "^0.3.0",
         "union-value": "^1.0.0",
         "unset-value": "^1.0.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+          "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.3",
+            "split-string": "^3.0.1"
+          }
+        }
       }
     },
     "call-bind": {
@@ -9121,19 +9066,6 @@
         "vinyl": "^2.0.0"
       }
     },
-    "gulp-footer": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/gulp-footer/-/gulp-footer-2.0.2.tgz",
-      "integrity": "sha512-HsG5VOgKHFRqZXnHGI6oGhPDg70p9pobM+dYOnjBZVLMQUHzLG6bfaPNRJ7XG707E+vWO3TfN0CND9UrYhk94g==",
-      "dev": true,
-      "requires": {
-        "lodash._reescape": "^3.0.0",
-        "lodash._reevaluate": "^3.0.0",
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.template": "^3.6.2",
-        "map-stream": "0.0.7"
-      }
-    },
     "gulp-print": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/gulp-print/-/gulp-print-5.0.2.tgz",
@@ -9594,6 +9526,12 @@
         "isobject": "^3.0.1"
       }
     },
+    "is-primitive": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
+      "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==",
+      "dev": true
+    },
     "is-regex": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -9884,125 +9822,6 @@
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
       "dev": true
     },
-    "lodash._basecopy": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
-      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
-      "dev": true
-    },
-    "lodash._basetostring": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
-      "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=",
-      "dev": true
-    },
-    "lodash._basevalues": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
-      "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=",
-      "dev": true
-    },
-    "lodash._getnative": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
-      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
-      "dev": true
-    },
-    "lodash._isiterateecall": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
-      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
-      "dev": true
-    },
-    "lodash._reescape": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
-      "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=",
-      "dev": true
-    },
-    "lodash._reevaluate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
-      "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=",
-      "dev": true
-    },
-    "lodash._reinterpolate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
-      "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
-      "dev": true
-    },
-    "lodash._root": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
-      "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
-      "dev": true
-    },
-    "lodash.escape": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
-      "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=",
-      "dev": true,
-      "requires": {
-        "lodash._root": "^3.0.0"
-      }
-    },
-    "lodash.isarguments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
-      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
-      "dev": true
-    },
-    "lodash.isarray": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
-      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
-      "dev": true
-    },
-    "lodash.keys": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
-      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
-      "dev": true,
-      "requires": {
-        "lodash._getnative": "^3.0.0",
-        "lodash.isarguments": "^3.0.0",
-        "lodash.isarray": "^3.0.0"
-      }
-    },
-    "lodash.restparam": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
-      "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
-      "dev": true
-    },
-    "lodash.template": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
-      "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=",
-      "dev": true,
-      "requires": {
-        "lodash._basecopy": "^3.0.0",
-        "lodash._basetostring": "^3.0.0",
-        "lodash._basevalues": "^3.0.0",
-        "lodash._isiterateecall": "^3.0.0",
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.escape": "^3.0.0",
-        "lodash.keys": "^3.0.0",
-        "lodash.restparam": "^3.0.0",
-        "lodash.templatesettings": "^3.0.0"
-      }
-    },
-    "lodash.templatesettings": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
-      "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=",
-      "dev": true,
-      "requires": {
-        "lodash._reinterpolate": "^3.0.0",
-        "lodash.escape": "^3.0.0"
-      }
-    },
     "loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11290,26 +11109,13 @@
       "dev": true
     },
     "set-value": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
-      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz",
+      "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==",
       "dev": true,
       "requires": {
-        "extend-shallow": "^2.0.1",
-        "is-extendable": "^0.1.1",
-        "is-plain-object": "^2.0.3",
-        "split-string": "^3.0.1"
-      },
-      "dependencies": {
-        "extend-shallow": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-          "dev": true,
-          "requires": {
-            "is-extendable": "^0.1.0"
-          }
-        }
+        "is-plain-object": "^2.0.4",
+        "is-primitive": "^3.0.1"
       }
     },
     "shebang-command": {
@@ -11955,6 +11761,29 @@
         "get-value": "^2.0.6",
         "is-extendable": "^0.1.1",
         "set-value": "^2.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+          "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.3",
+            "split-string": "^3.0.1"
+          }
+        }
       }
     },
     "unique-stream": {
diff --git a/package.json b/package.json
index 41a0cc18..5a8a6fb9 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,6 @@
     "eslint-plugin-react": "^7.11.1",
     "fs-extra": "^9.0.1",
     "gulp": "^4.0.2",
-    "gulp-footer": "^2.0.2",
     "gulp-concat":"2.6.1",
     "gulp-print": "^5.0.2",
     "gulp-replace": "^1.0.0",
@@ -37,6 +36,7 @@
     "fancy-log": "^1.3.3",
     "mkdirp": "^0.5.4",
     "prettier": "^1.14.2",
+    "set-value": ">=4.0.1",
     "shelljs": "0.8.4"
   },
   "dependencies": {
diff --git a/tests/testthat/test-callback.R b/tests/testthat/test-callback.R
index 75730701..3d0bc455 100644
--- a/tests/testthat/test-callback.R
+++ b/tests/testthat/test-callback.R
@@ -8,7 +8,8 @@ test_that("Callback outputs can be provided with or without output function", {
         dccInput(id='input-1-state', type='text', value='Montreal'),
         dccInput(id='input-2-state', type='text', value='Canada'),
         html$button(id='submit-button', n_clicks=0, children='Submit'),
-        html$div(id='output-state')
+        html$div(id='output-state'),
+        html$div(id='output-state2')
       )
     )
   )
@@ -24,7 +25,7 @@ test_that("Callback outputs can be provided with or without output function", {
   )
 
   expect_silent(
-    app$callback(output(id = 'output-state', property = 'children'),
+    app$callback(output(id = 'output-state2', property = 'children'),
                  list(input(id = 'submit-button', property = 'n_clicks'),
                       state(id = 'input-1-state', property = 'value'),
                       state(id = 'input-2-state', property = 'value')),