Overview
useLoaderData() connects React Router’s useLoaderData()
hook to your UI. It calls the hook and injects the result into a
component — either as text content inside a plain HTML element, or as a
structured JavaScript value into any prop of a
shiny.react-based component.
Use it inside a Route that has a loader
function.
Arguments
| Argument | Required | Description |
|---|---|---|
into |
yes | The component that will receive the loader data. Can be a plain
htmltools tag (e.g. tags$h3()) or a
shiny.react-based component. |
as |
no | The prop name to inject data into. Defaults to
"children". |
selector |
no | A key to extract from the loader data object. If NULL
(default), the entire loader data is passed. |
Rendering text in HTML elements
Pass any htmltools tag as into to render
loader data as text. This is the simplest use case — no extra
configuration needed:
# Render a single field as text
useLoaderData(tags$h3(), selector = "name") # renders the "name" field
useLoaderData(tags$pre()) # renders full JSON stringFull example with dynamic route parameters:
library(reactRouter)
library(htmltools)
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
people_json <- jsonlite::toJSON(dplyr::starwars, dataframe = "rows", auto_unbox = TRUE)
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = tags$div(
NavLink(to = "/", "Home"), " | ",
NavLink(to = "/people/1", "Luke"), " | ",
NavLink(to = "/people/2", "C-3PO"), " | ",
NavLink(to = "/people/3", "R2-D2"),
tags$hr(),
Outlet()
),
Route(index = TRUE, element = p("Select a person.")),
Route(
path = "people/:id",
loader = JS(sprintf(
"({ params }) => {
const db = %s;
const person = db[params.id - 1];
if (!person) throw new Response('Not found', { status: 404 });
return person;
}",
people_json
)),
element = div(
useLoaderData(into = tags$h3(), selector = "name"),
useLoaderData(into = tags$p(), selector = "gender")
),
errorElement = div(tags$p("Not found."), NavLink(to = "/", "Back"))
)
)
)
)
#> Warning: The `reloadDocument` argument of `NavLink()` default is now FALSE as of
#> reactRouter 0.2.0.
#> ℹ The default of `reloadDocument` was TRUE in version 0.1.1. It is now FALSE.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.Injecting structured data into shiny.react components
Pass a shiny.react-based component as into
and specify the target prop with as. The loader data (or a
selector field from it) is injected as the raw JavaScript
value — not converted to a string.
This is needed whenever a component expects an actual array or object
for a prop, for example rows in a data grid or
options in an autocomplete.
library(reactRouter)
library(muiDataGrid)
library(htmltools)
loader <- JS("async () => {
const res = await fetch('https://swapi.info/api/people');
return (await res.json()).map((p, i) => ({
id: i, name: p.name, height: p.height
}));
}")
RouterProvider(
router = createHashRouter(
Route(
path = "/",
loader = loader,
element = div(
style = "height: 400px;",
useLoaderData(
muiDataGrid::DataGrid(
columns = list(
list(field = "name", headerName = "Name", flex = 1),
list(field = "height", headerName = "Height", width = 100)
)
),
as = "rows"
)
)
)
)
)When the loader returns an object with multiple fields, use
selector to extract the relevant one:
# -- JS loader: fetches Star Wars characters from SWAPI, filters by ?name= param ----
loader_people <- JS(
"
async ({ request }) => {
const filter = new URL(request.url).searchParams.getAll('name');
const data = await fetch('https://swapi.info/api/people').then(r => r.json());
return {
names: data.map(p => p.name).sort(),
people: filter.length ? data.filter(p => filter.includes(p.name)) : data
};
}
"
)
# -- UI --------------------------------------------------------------------
RouterProvider(
router = createHashRouter(
Route(
path = "/",
loader = loader_people,
element = div(
h2("Star Wars Characters"),
p(
"Select characters to filter the table, or clear the selection to show all."
),
useLoaderData(
into = muiMaterial::Autocomplete(
onChange = JS(
"(event, value) => { window.location.hash = value.length ? '/?' + value.map(v => 'name=' + encodeURIComponent(v)).join('&') : '/'; }"
),
renderInput = JS(
"(params) => React.createElement(window.jsmodule['@mui/material'].TextField, {...params, label: 'Name'})"
),
multiple = TRUE,
sx = list(width = 300, marginBottom = 2)
),
as = "options",
selector = "names"
),
useLoaderData(
into = muiDataGrid::DataGrid(
columns = list(
list(field = 'name', headerName = 'Name', flex = 1),
list(field = 'height', headerName = 'Height', width = 100),
list(field = 'mass', headerName = 'Mass', width = 100),
list(field = 'gender', headerName = 'Gender', width = 120),
list(field = 'birth_year', headerName = 'Birth Year', width = 120)
),
getRowId = JS("function(row) { return(row.name) }"),
initialState = list(
pagination = list(paginationModel = list(pageSize = 5))
),
showToolbar = TRUE
),
as = "rows",
selector = "people"
)
)
)
)
)Designing your loader
The React Router community recommends that each loader returns
exactly the shape the consuming component needs. This
avoids the need for selector:
When a single route renders multiple components with different data needs, consider splitting into nested routes — each with its own loader:
library(reactRouter)
library(muiDataGrid)
library(htmltools)
stats_loader <- JS("async () => {
const res = await fetch('https://swapi.info/api/people');
const people = await res.json();
return { summary: people.length + ' characters found' };
}")
people_loader <- JS("async () => {
const res = await fetch('https://swapi.info/api/people');
return (await res.json()).map((p, i) => ({
id: i, name: p.name, height: p.height
}));
}")
RouterProvider(
router = createHashRouter(
Route(
path = "/",
loader = stats_loader,
element = div(
useLoaderData(tags$h2(), selector = "summary"),
Outlet()
),
Route(
index = TRUE,
loader = people_loader,
element = div(
style = "height: 400px;",
useLoaderData(
into = muiDataGrid::DataGrid(
columns = list(
list(field = "name", "headerName" = "Name", flex = 1),
list(field = "height", "headerName" = "Height", width = 100)
)
),
as = "rows")
)
)
)
)
)Works with any shiny.react component
useLoaderData() works with any component built on
shiny.react — it simply injects a prop value via
React.cloneElement(). Examples of compatible packages:
-
muiMaterial— MUI Material UI components -
muiDataGrid— MUI Data Grid -
muiCharts— MUI Charts -
muiTreeView— MUI Tree View -
shiny.fluent— Microsoft Fluent UI components -
shiny.blueprint— Palantir Blueprint UI components
Standard htmltools tags (tags$div,
tags$h3, etc.) only support text injection (the default
as = "children" with no structured data).
muiCharts::BarChart — inject loader data as
dataset:
library(reactRouter)
library(muiCharts)
library(htmltools)
loader <- JS("async () => {
const res = await fetch('https://swapi.info/api/people');
const people = await res.json();
return people.slice(0, 10).map(p => ({
name: p.name,
height: Number(p.height) || 0,
mass: Number(p.mass) || 0
}));
}")
RouterProvider(
router = createHashRouter(
Route(
path = "/",
loader = loader,
element = useLoaderData(
muiCharts::BarChart(
series = list(
list(dataKey = "height", label = "Height (cm)"),
list(dataKey = "mass", label = "Mass (kg)")
),
xAxis = list(list(scaleType = "band", dataKey = "name")),
height = 400
),
as = "dataset"
)
)
)
)Injecting into multiple props
To inject loader data into more than one prop of the same component,
nest useLoaderData() calls:
library(reactRouter)
library(muiDataGrid)
library(htmltools)
loader <- JS("async () => {
const res = await fetch('https://swapi.info/api/people');
const people = await res.json();
return {
columnDefs: [
{ field: 'name', headerName: 'Name', flex: 1 },
{ field: 'height', headerName: 'Height', width: 100 }
],
people: people.map((p, i) => ({ id: i, name: p.name, height: p.height }))
};
}")
RouterProvider(
router = createHashRouter(
Route(
path = "/",
loader = loader,
element = div(
style = "height: 400px;",
useLoaderData(
useLoaderData(
muiDataGrid::DataGrid(),
as = "rows",
selector = "people"
),
as = "columns",
selector = "columnDefs"
)
)
)
)
)Each call extracts its own selector from the same
useLoaderData() result and injects it into the specified
prop.
Requirements
useLoaderData() requires a data router
(createHashRouter, createBrowserRouter, or
createMemoryRouter) because only data routers support
loader functions. It will not work with the component-based
routers (HashRouter, MemoryRouter).
The child component’s JavaScript bundle must be loaded independently.
For example, muiDataGrid::DataGrid() automatically attaches
its own HTML dependency — reactRouter does not bundle
it.
