This is due to the way the inputs are used by shiny. In the javascript it has an option with a 'debounce' of 250ms which explains why it only updates after you stop typing for a quarter of a second.
You can override this but it seems to involve writing a replacement for textInput. The crucial bit is the getRatePolicy function in the javascript.

library(shiny)
library(shinyCustom)
textinput_script <- "
<script>
var customTextInputBinding = $.extend({}, Shiny.inputBindings.bindingNames['shiny.textInput'].binding, {
find: function(scope) {
return $(scope).find('input.customTextInput');
},
subscribe: function(el, callback) {
$(el).on('keyup.customTextInputBinding input.customTextInputBinding', function(event) {
callback();
});
$(el).on('focusout.customTextInputBinding', function(event) { // on losing focus
callback();
});
},
unsubscribe: function(el) {
$(el).off('.customTextInputBinding');
},
getRatePolicy: function() {
return {
policy: 'direct'
};
}
});
Shiny.inputBindings.register(customTextInputBinding, 'shiny.customTextInput');
</script>
"
ui <- fluidPage(sidebarLayout(
sidebarPanel(
HTML(textinput_script),
customTextInput("text", label = NULL)
),
mainPanel(textOutput("textout"))
))
server <- function(input, output, session) {
output$textout <- renderText({
input$text
})
}
shinyApp(ui, server)
This is cannibalized from here, and the actual guide for doing this in general is here.