Suggestion Box
Spot an error or have suggestions for improvement on these notes? Let us know!
Using here::here() for File Paths in Functions
When writing and testing your R scripts in the main project directory, your working directory is automatically the project root (for example, psy1903/web/npt_project).
But when you render a Quarto report stored inside a subfolder like reports/, the working directory changes.
This difference often breaks file paths, especially when functions load or save data files.
This short lesson explains how to fix that problem using the here package and its function here::here().
It builds on what you learned about R Projects, relative paths, and reproducible code organization.
1 · Recap: Project Roots and Relative Paths
When you open an .Rproj file, RStudio automatically sets your working directory to that project’s root folder.
All paths are interpreted relative to that root.
Example structure for npt_project:
npt_project/
├── npt_project.Rproj
├── data/
│ ├── raw/
│ │ └── npt-experiment-2025-11-05-10-33-05.csv
│ │ ...
│ └── cleaned/
│ └── participants/
│ │ └── npt-experiment-2025-11-05-10-33-05_processed.csv
│ │ ...
│ └── study_level.csv
│ └── study_level.rds
├── scripts/
│ └── process_participant.R
│ └── score_questionnaire.R
│ └── summarize_behavior.R
└── reports/
└── npt_import_files/
└── npt_import.html
└── npt_import.qmd
- In scripts run from the root, "data/raw/file.csv" works. This applies when you are testing code in the console or test new functions in R Scripts before moving to Quarto
- In Quarto documents inside
reports/, you would have to update all paths to "../data/raw/file.csv", which is easy to forget.
That is where here::here() comes in.
2 · What here::here() Does
The here package automatically finds your project’s root directory (the folder containing the .Rproj file) and constructs file paths relative to it.
library(here)
here("data", "raw", "npt-experiment-2025-11-05-10-33-05.csv")
# "psy1903/web/npt_project/data/raw/npt-experiment-2025-11-05-10-33-05.csv"
No matter whether the code runs from scripts/, reports/, or interactively in RStudio,
here() always starts at the project root. This means you never need setwd(), remember whether or not to use ../, or hardcode your paths.
Calling here::here() tells R exactly which package the function comes from, without needing to load it first with library(here).
This makes your code more explicit, avoids naming conflicts across packages, and keeps your functions self-contained.
In other words:
here::here()always works as long as theherepackage is installed.here()alone only works after you have loaded the package withlibrary(here)orrequire(here)(and you would need to do that each time you start R or render a Quarto report).
3 · Example: Updating process_participant()
Below is the process_participant() function we wrote for the NPT Project with path names replaced with here::here.
This function loads a single participant’s raw data file, processes it, and saves the cleaned result.
Notice how here::here() keeps file paths consistent whether the function is sourced or run from a Quarto report, for example:
## Read the raw CSV
participant_data <- read.csv(
here::here("data", "raw", file_name),
stringsAsFactors = FALSE
)
Full process_participant.R script with here::here() replacements:
process_participant <- function(file_name) {
#### Load File and Extract ID ------------------------------------------------
## Derive a subject id from the filename (no extension)
subject_id <- sub("\\.csv$", "", basename(file_path))
## Read the raw CSV
participant_data <- read.csv(
here::here("data", "raw", file_name),
stringsAsFactors = FALSE
)
#### Questionnaire score -----------------------------------------------------
## Score questionnaire with our defaults (reverse 2,4,7 on 0–4 scale)
tef10_score <- score_questionnaire(
json_string = participant_data[participant_data$trialType == "questionnaire", "response"],
reverse = c(2, 4, 7),
scale_min = 0L,
scale_max = 4L
)
#### Behavioral summary ------------------------------------------------------
## Filter and summarize behavioral data (250–900 ms)
behavior <- summarize_behavior(participant_data, rt_min = 250, rt_max = 900)
#### Save participant summary ------------------------------------------------
## Ensure output directory is created
dir.create(
here::here("data", "cleaned", "participants"),
recursive = TRUE,
showWarnings = FALSE
)
## Combine into a single-row participant summary
df_clean <- data.frame(
subject_id = subject_id,
tef10_score = tef10_score,
behavior = behavior
)
## Save summary CSV to cleaned/participants
write.csv(
df_clean,
here::here("data", "cleaned", paste0(subject_id, "_processed.csv")),
row.names = FALSE
)
#### Return output -----------------------------------------------------------
stopifnot(nrow(df_clean) == 1) # one row per participant
return(df_clean)
}
A few notes:
- We updated the
process_participantfunction to now use just thefile_nameas an input argument instead of the fullfile_path, because that is now handled byhere:here() - This means we need to update where we build our file_list in our Quarto report to be
full.names = FALSEinstead ofTRUE:
#### 1) Find files -------------------------------------------------------------
```{r}
# Prefer a pattern to avoid accidentally pulling other CSVs
file_list <- list.files(
here::here("data", "raw"),
pattern = "^npt-experiment-.*\\.csv$",
full.names = FALSE
)
```
Key points:
- All file locations are built with
here::here(), so they work from anywhere. - No
setwd()or../paths are needed. - The function now runs identically when sourced and tested in RStudio (interactively) or when called inside a Quarto report chunk.
Testing It Interactively
From the Console or an R Script (with the .Rproj open):
source("scripts/process_participant.R")
processed <- process_participant("npt-experiment-2025-11-05-10-08-45.csv")
head(processed)
Using It in a Quarto Report
Inside reports/npt_project.qmd:
```{r}
source(here::here("scripts", "process_participant.R"))
processed <- process_participant("npt-experiment-2025-11-05-10-08-45.csv")
```
Because every path inside the function starts at the project root,
the code runs the same way when rendered.
4 · Applying This Pattern to Other Scripts
You can use the same here::here() pattern anywhere a path appears in your other NPT functions:
| Script | Where to Add here::here() |
Example |
|---|---|---|
process_participant.R |
As shown above, handles both loading and saving participant data. | read.csv(here::here("data", "raw", file_name))write.csv(here::here("data", "cleaned", "participants", paste0(subject_id, ".csv"))) |
| Quarto Report | When sourcing scripts or loading data within a .qmd file. |
source(here::here("scripts", "process_participant.R")) |
score_questionnaire.R |
If you wanted to extend this function to read or write data files, use here::here() for any file paths. |
jsonlite::read_json(here::here("data", "raw", "questionnaire.json")) |
summarize_behavior.R |
If you wanted to add code that writes or saves summary tables or plots, use here::here() for those output paths. |
write.csv(summary, here::here("output", "tables", "behavior_summary.csv")) |
By including here::here() inside your functions, each script becomes self-contained and portable,
and your Quarto reports can source them directly without path edits.
5 · Summary
| Concept | Without here::here() |
With here::here() |
|---|---|---|
| Quarto report cannot find files | Paths break ("../data/...") | Always resolves from project root |
| Testing vs. rendering behavior | Different working directories | Identical behavior everywhere |
Need for setwd() |
Often used, brittle | Never needed |
| Reproducibility and portability | Dependent on computer setup | Fully reproducible across machines |
Take-home message:
Use here::here() for every file path in your functions and scripts.
It keeps your workflow reproducible, your functions self-contained, and your Quarto reports consistent.