Who can deny that life is not fair, nor easy, just a messy affair where we muddle along with glimpses of significant relationships and futile searches of meaning and purpose? Who can justify that lies are abundant and spreading, news are mainly propaganda driven by political and economic narratives, authorities are corrupt, history is being twisted, trust is all gone, and we all know that something needs to be done? Who can deny that yesterday is gone, tomorrow is not promised, and we only have today?
Who can rebuff that destiny is a bitch, the world is getting crazy, money is king and sometimes even god, and life is but a dream, a worn out ship on troubled waters, and things are only getting harder? Who can deny that I love you Bew like crazy, my sweet angel? Desperate verses IV, M. Anawim, #justtothepoint.
We’re creating a custom React hook called useDiary that manages everything related to diary entries in our app. Think of a hook as a reusable piece of logic that can be shared across different components.
Imagine a text editor where you write journal entries. This hook handles:
The Problem It Solves. Think about what happens when you type:
Debouncing means: Wait until the user stops doing something, then act.
Here’s how it works with typing:
You type: H → e → l → l → o → (stop typing)
↓ ↓ ↓ ↓ ↓
Timer: Start Reset Reset Reset Reset
(2s) (new 2s)(new 2s)(new 2s)
↓
After 2 seconds of silence: SAVE!
Two seconds is a sweet spot:
// The Timer Setup
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Let’s break this down piece by piece:
// The Effect Hook
useEffect(() => {
// 1. Check if we need to save.
// isDirty means "has the user changed anything?"
// If the content hasn't changed, don't bother setting a timer
// This prevents unnecessary timers when nothing has changed
if (!isDirty) return;
// 2. Clear any existing timer. Cancel any pending save.
// Before starting a new timer, cancel the old one.
// This is the heart of debouncing.
// If you typed "H" and then "e" 1 second later, the "H" timer gets cancelled.
if (timerRef.current) clearTimeout(timerRef.current);
// 3. Start a new timer.
// Set a timer that will fire in 2 seconds.
// When it fires, it calls saveEntry() which saves our diary entry.
// The await means "wait for the save to complete".
timerRef.current = setTimeout(async () => {
await saveEntry();
}, 2000);
// Whenever an effect is about to run again, React first runs the cleanup from the previous run.
// And when the component unmounts, React runs the cleanup one final time.
// 4. Cleanup runs in two situations
// 1️⃣ Before the effect runs again. Dependencies:
// [content (you type something), isDirty (we're tracking whether it's dirty)].
// Imagine the user types: First keystroke → effect runs → timer starts.
// Second keystroke → dependencies changed → cleanup runs → timer cleared
// Effect runs again → new timer starts. This is the debounce cycle.
// Notice: You stop typing → 2 seconds pass → timer fires → entry saves!
// 2️⃣ When the component unmount. If the user navigates away, closes the page, or the component disappears.
// React runs the cleanup one last time. The timer is cleared.
// No “orphan” or "zombie" timers save runs after the component is gone
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [content, isDirty]); // re-run every time content changes
useRef is used for values that:
useState is used for values that:
The timer ID is internal bookkeeping. When it changes (from one timer to another), we don’t need the UI to update. Therefore, useRef is perfect.
# Python: threading.Timer with cancel/restart
class DiaryEditor:
def __init__(self):
# Similar to useRef
self._save_timer: threading.Timer | None = None
def on_text_changed(self, content):
# If there's a pending save, cancel it
if self._save_timer:
self._save_timer.cancel()
# Schedule a new save in 2 seconds
self._save_timer = threading.Timer(2.0, self._auto_save)
self._save_timer.start()
def _auto_save(self):
self.save_to_file(self.content)
const [isDirty, setIsDirty] = useState(false);
const [status, setStatus] = useState('Ready');
Possible status values:
User types → status changes to 'Saving...' → 2 seconds pass → status changes to 'Saved ✅'
This gives the user confidence that their work is being saved.
Think of it as a restaurant:
The hook puts all these pieces together into one manageable system.
src/hooks/useDiary.tsThis is a custom React Hook called useDiary that manages all the data and operations for a diary/journal application. Think of it like a dedicated butler for your diary feature - it handles loading entries, saving them, tracking what’s changed, and automatically saving your work.
Think of it as you were building a diary app. You need to:
Any React component can use it by simply writing:
const diary = useDiary();
// src/hooks/useDiary.ts
//
// DIARY HOOK
// ───────────
// Manages all diary state and file I/O for the Diary tab.
//
// STATE:
// What is "state"? In React, "state" is like the app's memory.
// It's data that can change over time, and when it changes, React automatically updates what you see on screen.
// entries: An array (list) of diary entry summaries (sorted newest-first).
// "Newest-first" means if you wrote an entry yesterday and one today, today's shows up first.
// currentDat: Which entry are you currently looking at?
// This stores its date (the ISO date). null means no entry is open.
// content: What's actually written in the text editor right now.
// loading: A flag that says "I'm busy fetching data." Your UI can show a spinner when this is true.
// saving: Similar to loading, but specifically for save operations (a save is in progress).
// isDirty: A very useful concept! "Dirty" means "has unsaved changes."
// If you type "Hello" and haven't saved yet, isDirty becomes true.
// saveStatus: A more detailed version of what's happening with saving. It can be:
// "idle" - nothing happening; "saving" - currently writing to disk
// "saved" - just completed saving; "error" - something went wrong
//
// AUTO-SAVE:
// Watches `content` for changes. If content changes and isDirty is true,
// starts a 2-second debounce timer. When the timer fires, calls saveEntry().
// If content changes again before 2 seconds, the timer resets.
// You type: "H" → Timer starts: "Save in 2 seconds"
// You type: "He" (0.5 seconds later) → Old timer cancelled, new timer: "Save in 2 seconds"
// You type: "Hel" (0.3 seconds later) → Old timer cancelled, new timer: "Save in 2 seconds"
// You stop typing... → 2 seconds pass → Content "Hel" is saved!
// This is called **debouncing**.
// It prevents saving every single keystroke (which would be wasteful) and only saves when you pause.
//
// USAGE:
// const diary = useDiary();
// // mount: entries are loaded automatically
// diary.newEntry(); // create/open today's entry
// diary.setContent("Hello"); // user typing — triggers auto-save timer
// diary.saveEntry(); // manual save (bypasses debounce)
// diary.openEntry("2026-05-14") // open a specific past entry
// diary.deleteEntry("2026-05-13") // delete an old entry
// These are special React tools:
// useState: Creates a "state variable" that React watches for changes
// useEffect: Runs code at specific times (when component mounts, when certain values change)
// useCallback: Creates a function that doesn't get recreated unnecessarily
// useRef: Creates a "reference" - like a box that can hold a value without triggering re-renders
import { useState, useEffect, useCallback, useRef } from "react";
// These are functions that handle actual file operations (reading/writing to disk).
import {
listEntries,
readEntry,
writeEntry,
deleteEntry as deleteEntryFile,
} from "../services/diaryService";
import { todayISO, formatDate } from "../utils/dateUtils";
import type { DiaryEntrySummary, ISODate } from "../db/types";
// ── Auto-save delay ───────────────────────────────────────────────────────────
const AUTO_SAVE_DELAY_MS = 2000; // 2 seconds after last keystroke
// ── Save status type ──────────────────────────────────────────────────────────
// This creates a custom type. It says "SaveStatus can only be one of these four specific strings."
// TypeScript will prevent you from accidentally setting it to anything else.
export type SaveStatus = "idle" | "saving" | "saved" | "error";
// An interface is like a contract. It says
// "Any object claiming to be a UseDiaryResult must have all these properties with these specific types."
// This is the "shape" of what useDiary() returns.
// ── Return type ───────────────────────────────────────────────────────────────
export interface UseDiaryResult {
/** Lightweight list of all entries, newest first */
entries: DiaryEntrySummary[];
/** ISO date of the entry currently open in the editor (null if none) */
currentDate: ISODate | null;
/** Current editor text (controlled — update via setContent) */
content: string;
/** true while loading the entry list or opening an entry */
loading: boolean;
/** true while a manual save is in progress */
saving: boolean;
/** true when content differs from the last saved version */
isDirty: boolean;
/** UI-friendly save status string */
saveStatus: SaveStatus;
/** Update the editor content (triggers auto-save timer) */
setContent: (text: string) => void;
/** Open a specific entry by date (loads content from disk) */
openEntry: (date: ISODate) => Promise<void>;
/** Create or open today's entry, pre-filling a template if empty */
newEntry: () => Promise<void>;
/** Save immediately (bypasses the 2-second debounce) */
saveEntry: () => Promise<void>;
/** Delete an entry and refresh the list */
deleteEntry: (date: ISODate) => Promise<void>;
/** Reload the entry list (e.g. after navigating back to the tab) */
refreshList: () => Promise<void>;
}
// ── Daily template ────────────────────────────────────────────────────────────
/**
* When you create a new diary entry, instead of a blank page,
* you get a pre-made Markdown template with sections for:
* Gratitude (what you're thankful for)
* Reflection (thoughts from prayer)
* Intention (what you'll do tomorrow)
*/
function buildTemplate(date: ISODate): string {
const formatted = formatDate(date, "long");
return (
`# ${formatted}\n\n` +
`## Gratitude\n\n` +
`Today I am grateful for...\n\n` +
`## Reflection\n\n` +
`During prayer today I noticed...\n\n` +
`## Intention\n\n` +
`Tomorrow I will...\n`
);
}
// ══════════════════════════════════════════════════════════════════════════════
// THE HOOK
// ══════════════════════════════════════════════════════════════════════════════
// State Declarations
export function useDiary(): UseDiaryResult {
// ── State ─────────────────────────────────────────────────────────────────
// useState returns an array with two things:
// (1) The current value (entries); (2) A function to update it (setEntries)
// The <DiaryEntrySummary[]> tells TypeScript "this will hold an array of DiaryEntrySummary objects."
// The [] at the end is the initial value - an empty array.
const [entries, setEntries] = useState<DiaryEntrySummary[]>([]);
const [currentDate, setCurrentDate] = useState<ISODate | null>(null);
const [content, _setContent] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const [isDirty, setIsDirty] = useState<boolean>(false);
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
What’s a ref? Imagine you have a box. You can put something in it and change what’s inside whenever you want, but unlike state, changing a ref doesn’t make React re-render the component. Refs are perfect for tracking things that shouldn’t cause visual updates.
// ── Refs (The "Boxes", do NOT trigger re-renders) ─────────────────────────────────────
// savedContentRef: Remember the last content that was successfully saved.
// This helps determine isDirty (if current content differs from saved content, the document is "dirty").
// Used to compute isDirty: isDirty = (content !== savedContentRef.current)
const savedContentRef = useRef<string>("");
// autoSaveTimerRef: Stores the ID of the auto-save timer (debounce timer ID).
// We need this to cancel the timer when the user keeps typing.
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// mountedRef: Tracks whether the component is still on screen (mounted).
// This prevents a very specific bug: trying to update state after the component is gone (which causes React errors).
const mountedRef = useRef<boolean>(true);
// React always follows this lifecycle: (1) Mount → component appears on screen
// (2) Render updates → state changes cause re-renders; (3) Unmount → component is removed from the screen
// An effect with [] runs exactly once after the first render, and its cleanup runs exactly once when the component unmounts.
useEffect(() => {
// Step 1 — Component mounts. React renders your component for the first time.
// Then it runs the effect: mountedRef.current = true; The component is alive.
mountedRef.current = true;
// Step 3 — Component unmounts
// When React removes the component from the screen, it runs the cleanup:
// This marks: “The component is gone.”
return () => { mountedRef.current = false; };
// Step 2 — Component stays mounted.
// Nothing else happens with this effect. Because the dependency array is [].
}, []);
// ── The setContent Function ────────────────────────────
// Notice it's wrapped in useCallback with an empty dependency array [].
// This means "create this function once and never recreate it."
// Every time the editor text changes, we:
const setContent = useCallback((text: string) => {
// 1. Updates what's displayed (_setContent(text)).
// It triggers a re-render, user sees their typing right away.
_setContent(text);
// 2. Checks if this new text is different from what was saved.
// Recalculate isDirty (are there unsaved changes?)
const dirty = text !== savedContentRef.current;
// 3. If it's different, marks the document as "dirty"
setIsDirty(dirty);
// 4. Resets the save status (because we haven't saved this new content yet).
if (dirty) setSaveStatus("idle");
}, []);
// ── Auto-save effect ──────────────────────────────────────────────────────
// 1. When does this run? Every time content, isDirty, or currentDate changes.
useEffect(() => {
// 2. Guard clause: If the document isn't dirty or no entry is open, do nothing.
if (!isDirty || !currentDate) return; // nothing to save
// 3. Cancel existing timer:
// If you typed something and there's already a timer waiting, cancel it.
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
}
// 4. Start new timer: Schedule performSave() to run after 2 seconds.
autoSaveTimerRef.current = setTimeout(async () => {
if (!mountedRef.current) return; // component unmounted — skip
await performSave();
}, AUTO_SAVE_DELAY_MS);
// React ALWAYS runs the cleanup before the effect re‑runs, and once more when the component unmounts.
// 1️⃣ User types → effect runs. content changes → effect runs. You clear any previous timer. You start a new timer.
// 2️⃣ User types again → cleanup runs FIRST
// Cleanup function: If the effect runs again before the timer fires (because you typed more),
// React calls this cleanup first, which cancels the old timer. This is the magic of debouncing!
// This is what implements "reset the timer on each keystroke".
return () => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
}
};
}, [content, isDirty, currentDate]);
// NOTE: performSave is not in the deps array because it is defined below
// using useCallback. Including it would cause an infinite loop.
// ── Core save function ────────────────────────────────────────────────────
// `performSave` contains the actual save logic. It is intentionally separated
// from the public `saveEntry` so both the auto‑save timer and the manual
// “Save” button can call the exact same code path.
async function performSave(): Promise<void> {
// 1. If the component has been unmounted, abort immediately.
// This prevents state updates on an unmounted component.
if (!mountedRef.current) return;
// 2. Determine which entry we are saving.
// If no date is selected, there is nothing to save.
const dateToSave = currentDate;
if (!dateToSave) return;
// 3. Mark the UI as "saving".
// This allows the UI to show a spinner or status indicator.
setSaving(true);
setSaveStatus("saving");
try {
// 4. Write the file to storage.
//
// autoSaveTimerRef.current = setTimeout(async () => {
// await performSave();
// }, 2000);
// the arrow function () => { await performSave(); } captures all variables it uses from the surrounding scope:
// performSave, content, ... This “captured environment” is the closure.
// Closures do NOT freeze values. They reference variables.
// IMPORTANT: `content` is read directly from React state.
// Because the auto‑save timer fires *later*, the closure captures the
// *current* value of `content` at the moment the timer executes.
await writeEntry(dateToSave, content);
// If the component is unmounted during the async write, stop here.
if (!mountedRef.current) return;
// 5. Mark the new content as the "last saved" version.
// This makes isDirty → false, because the saved version now matches
// what the user sees.
savedContentRef.current = content;
setIsDirty(false);
setSaveStatus("saved");
// 6. Refresh the entry list.
// Word count, preview text, or ordering may have changed.
const updated = await listEntries();
if (mountedRef.current) setEntries(updated);
// 7. After a brief delay, fade the status back to "idle".
// This gives the user a momentary “Saved” confirmation.
setTimeout(() => {
if (mountedRef.current) setSaveStatus("idle");
}, 2000);
} catch (e) {
// Any failure during the save process is caught here.
// We avoid updating state if the component unmounted mid‑error.
if (!mountedRef.current) return;
console.error("[useDiary] Auto-save failed:", e);
setSaveStatus("error");
} finally {
// Always clear the "saving" flag, success or failure.
// Again, only update state if the component is still mounted.
if (mountedRef.current) setSaving(false);
}
}
// ── Load entry list on mount ──────────────────────────────────────────────
// "On mount" means "do this when the component first appears."
// This loads all diary entries from disk.
// It's wrapped in useCallback with empty dependencies so it's created once and never changes.
// On every render, React reuses the same function reference.
const refreshList = useCallback(async () => {
if (!mountedRef.current) return;
// When mountedRef.current is false, it means the component has been removed from the screen.
// If that happened, there's no point updating state for a component that no longer exists.
// The return statement exits the function immediately.
setLoading(true);
// Update the UI to show a loading spinner or indicator.
try {
// The function listEntries() reads files from disk - this takes time.
// await says "pause here until this finishes, but let other things happen in the app
// (like animations) while we wait."
// listEntries scans a folder for diary files, reads their metadata, and returns them as a sorted array.
const list = await listEntries();
// Why check mounted again? While we were waiting for listEntries() to finish (the await),
// the user might have navigated away from the diary tab. If the component unmounted while we were waiting,
// updating setEntries would cause a React error. This check prevents that.
// setEntries(list) updates the entries state with the fresh list from disk.
if (mountedRef.current) setEntries(list);
} catch (e) {
console.error("[useDiary] listEntries failed:", e);
} finally {
// Why set loading to false here? Because whether we got the entries or failed, the loading process is done.
// The spinner should stop spinning.
if (mountedRef.current) setLoading(false);
}
}, []);
// What is useEffect? This is React's way of saying "run this code when certain things change."
// It's for "side effects" - things that affect the outside world (like fetching data).
// Since refreshList never changes (thanks to useCallback with []),
// this effect runs exactly once: when the component first appears (or mounts). 🧭
useEffect(() => {
refreshList();
}, [refreshList]);
What happens in simple terms:
setLoading(true);const list = await listEntries();if (mountedRef.current) setEntries(list);openEntry opens an existing diary entry to view or edit. When you click on an entry in the list, this is what happens behind the scenes.
// ── openEntry ─────────────────────────────────────────────────────────────
// The function takes one parameter: date of type ISODate.
// An ISODate is a string like "2026-08-06" - a standardized date format.
const openEntry = useCallback(async (date: ISODate) => {
if (!mountedRef.current) return;
// Protecting Unsaved Work. If there are unsaved changes to the current entry, save them first
// before switching — so no work is lost.
if (isDirty && currentDate) {
await performSave();
}
// It tells the UI to show a spinner while we are working.
setLoading(true);
try {
// Then we call readEntry(date) which reads the file from disk.
// For example, it might read a file named "2026-08-06.md" and return its contents as a string.
const text = await readEntry(date);
if (!mountedRef.current) return;
setCurrentDate(date);
// Remember earlier, the hook created a custom setContent wrapper function
// that marks the document as dirty whenever content changes.
// That's perfect when the user is typing, but when we're loading a file from disk,
// we don't want to mark it as dirty. The loaded content IS the saved content.
// We have stored the original setter as _setContent (note the underscore prefix
// - a common convention meaning "internal" or "original"). This bypasses the dirty-tracking logic.
_setContent(text);
// Then, manually:
// Updates savedContentRef.current to match content.
savedContentRef.current = text;
// Explicitly sets isDirty to false (this is a clean, just-loaded document).
setIsDirty(false);
// Sets saveStatus to "idle" (no save in progress).
setSaveStatus("idle");
} catch (e) {
console.error(`[useDiary] openEntry(${date}) failed:`, e);
} finally {
if (mountedRef.current) setLoading(false);
}
// Dependencies. Unlike refreshList, this function depends on two things:
// isDirty - whether the current content has unsaved changes.
// currentDate - which entry is currently open.
// If either of these changes, useCallback will create a new version of this function.
// Why? Because the function reads these values, and it needs the latest versions.
// If it used old values, it might try to save the wrong thing.
}, [isDirty, currentDate]);
// ── newEntry ──────────────────────────────────────────────────────────────
// This creates or opens today's diary entry. It's called when the user clicks "New Entry" or "Today" button.
const newEntry = useCallback(async () => {
// 1. Getting Today's Date
// todayISO() is a utility function from dateUtils.
// It returns today's date as an ISO string like "2026-08-06".
const today = todayISO();
// 2. Protecting Unsaved Work
// If you've been editing yesterday's entry and click "New Entry," your changes get saved first.
if (isDirty && currentDate) {
await performSave();
}
// 3. First, we try to read today's entry from disk.
// It might already exist (if you opened it earlier today) or return an empty string (if it's never been created).
const existing = await readEntry(today);
if (!mountedRef.current) return;
setCurrentDate(today);
if (existing) {
// 3.1 Today's entry already has content — open it as-is
// Load it exactly like openEntry does. Use _setContent to bypass dirty tracking.
// Set savedContentRef to match. Mark as not dirty. This is exactly what you want
// - you're returning to an entry you already started.
_setContent(existing);
savedContentRef.current = existing;
setIsDirty(false);
} else {
// 3.2 Blank (or brand) new entry — pre-fill with the template
// A. buildTemplate(today) creates a pre-formatted Markdown template with sections for
// Gratitude, Reflection, and Intention, headed with today's date in a nice format.
const template = buildTemplate(today);
// B. _setContent(template) puts the template in the editor.
_setContent(template);
// C. savedContentRef.current = "" is set to empty string!
// Why? Because the template has NEVER been saved to disk.
// An empty string represents "nothing saved yet"
savedContentRef.current = "";
// D. It marks it as dirty immediately - the template content has never been written to disk!
setIsDirty(true);
}
// 4. Whether loading existing or creating new, reset the save status to idle.
setSaveStatus("idle");
// Dependencies: it needs current values of these to protect unsaved work.
}, [isDirty, currentDate]);
// ── saveEntry (manual) ────────────────────────────────────────────────────
// This is the manual save - when the user clicks a "Save" button in the toolbar.
// It bypasses the 2-second auto-save debounce and saves immediately.
const saveEntry = useCallback(async () => {
// Cancel any pending auto-save timer (avoid double-save)
if (autoSaveTimerRef.current) {
// clearTimeout tells JavaScript "forget about that timer I set earlier."
clearTimeout(autoSaveTimerRef.current);
// Then, we set the ref to null to indicate "there is no pending timer."
autoSaveTimerRef.current = null;
}
// Call the core save function.
// Since this is manual (user explicitly asked to save), we **await** it - we want to be sure it completes.
await performSave();
// This function has NO dependencies.
// Because it delegates to performSave(), which reads the latest state values internally.
}, []);
// ── deleteEntry ───────────────────────────────────────────────────────────
// Deletes a diary entry file from disk and cleans up the UI accordingly.
const deleteEntry = useCallback(async (date: ISODate) => {
// 1. deleteEntryFile is the service function that removes the file from disk.
await deleteEntryFile(date);
if (!mountedRef.current) return;
// 2. Handling the Currently Open Entry
// What if you delete the entry you're looking at (currentDate)?
if (date === currentDate) {
// A. Set currentDate to null (no entry is open).
setCurrentDate(null);
// B. Clear the editor content to empty string.
_setContent("");
// C. Reset savedContentRef (no content means nothing saved).
savedContentRef.current = "";
// D. Mark as not dirty (no content means no unsaved changes).
setIsDirty(false);
// E. Reset save status
setSaveStatus("idle");
// This effectively returns the editor to its blank, initial state.
// The user sees an empty editor with no entry selected.
}
// 3. Refresh the list
// After deleting, the entry list is out of date (it still shows the deleted entry).
// So we reload the list from disk and update the state.
// This happens regardless of whether the deleted entry was currently open.
const updated = await listEntries();
if (mountedRef.current) setEntries(updated);
// The function needs to know currentDate to handle the case
// where the deleted entry is the one currently being viewed.
}, [currentDate]);
All these functions are building blocks that create a complete diary editing experience:
refreshList loads all entriesopenEntry saves current work (if any), loads the selected entrynewEntry saves current work, then either opens today’s existing entry or creates a fresh template.saveEntry saves immediately, cancelling any pending auto-save.deleteEntry removes the file and cleans up the editor if needed.Every function follows the same pattern:
// ── Return ────────────────────────────────────────────────────────────────
return {
entries,
currentDate,
content,
loading,
saving,
isDirty,
saveStatus,
setContent,
openEntry,
newEntry,
saveEntry,
deleteEntry,
refreshList,
};
}
app/(tabs)/diary.tsx with the hook test screenThis is a React Native screen component that serves as a test harness for the useDiary hook we explored earlier. Think of it as a workbench where a carpenter tests all their new tools before building the final furniture. The developer wants to verify that every function of the hook works correctly before building the beautiful, polished diary interface.
// app/(tabs)/diary.tsx
//
// DIARY TAB — Test screen
//
// Tests the useDiary hook:
// 1. Entry list loads on mount
// 2. "New entry" opens today's entry with a template
// 3. Typing into the TextInput updates `content` and sets isDirty=true
// 4. Stopping for 2 seconds triggers auto-save (saveStatus="saved")
// 5. Restarting the app and tapping the entry shows the saved content
//
// Will be replaced with the full styled editor UI later on.
// This imports the React library itself. You always need this in any file that uses JSX
import React from "react";
// React Native Components
// View: The most basic container. Like a DIV in web development. It's an invisible box that can hold other components.
// Text: Displays text on screen. In React Native, you can't just write text directly - it must be wrapped in a Text component.
// TextInput: A field where users can type. This is the editor area where diary entries are written.
// ScrollView: A container that scrolls when its content is taller than the screen. Used for the entry list sidebar.
// StyleSheet: A way to define styles (colors, sizes, layouts) in a structured, optimized way.
// TouchableOpacity: Makes anything tappable. When pressed,
// it briefly becomes slightly transparent, giving visual feedback to the user. Used for buttons and list items.
// ActivityIndicator: A spinning loading animation. Shows the user that something is happening in the background.
import {
View, Text, TextInput, ScrollView, StyleSheet,
TouchableOpacity, ActivityIndicator,
} from "react-native";
// The Custom Hook
// This imports the hook we studied earlier.
// The path ../../ means "go up two folders from the current file, then into src/hooks."
// The hook returns an object with all the diary management functions and state.
import { useDiary } from "../../src/hooks/useDiary";
// Type Import. It tells TypeScript this is only a type import - it won't generate any actual JavaScript code.
// DiaryEntrySummary is the shape of each entry in the list (it has date, wordCount, preview, etc.).
// This import is needed for TypeScript to understand what properties exist on entry objects.
import type { DiaryEntrySummary } from "../../src/db/types";
// The Component Definition
// This defines a React component called DiaryScreen.
// The export default means this is the main thing this file provides to other files.
// The function keyword creates a component - in React, components are just functions that return UI elements
export default function DiaryScreen() {
// Initializing the Hook
// When this component first renders, useDiary():
// 1. Sets up all the state variables (entries, currentDate, content, loading, saving, isDirty, saveStatus)
// 2. Creates the auto-save timer infrastructure
// 3. Immediately starts loading the entry list from disk
// 4. Returns an object with all the functions and state we need
// The variable diary now holds everything. It's like having a fully trained diary management assistant ready to help.
// To access the entry list, you use diary.entries. To open an entry, call diary.openEntry(date).
// To save, call diary.saveEntry(). Everything is neatly packaged together
const diary = useDiary();
// The Status Configuration Object
// This is a lookup table - a way to map one value to another without using if/else statements.
// The type annotation Record means:
// "This object has string keys, and each value is an object with a label and a color."
// TypeScript ensures every key has the correct shape.
const statusConfig: Record<string, { label: string; color: string }> = {
// Why use emojis? In a test screen, visual clarity matters more than polish.
// Emojis are universally understood and require no custom icons or images.
// What each status means visually
// idle: Nothing to show. The label is empty, the color is transparent (invisible).
// This is the normal resting state.
idle: { label: "", color: "transparent" },
// saving: A floppy disk emoji and "Saving…" in blue.
// The user knows their work is being written to disk.
saving: { label: "💾 Saving…", color: "#5b8fd4" },
// saved: A checkmark and "Saved" in green. Brief confirmation that the save completed.
saved: { label: "✅ Saved", color: "#86efac" },
// error: An X and "Error" in red. Something went wrong with saving.
error: { label: "❌ Error", color: "#f87171" },
};
// Looking Up the Current Status
// This uses bracket notation to look up the current save status in the configuration object.
// If diary.saveStatus is "saved", then status becomes { label: "✅ Saved", color: "#86efac" }.
// If it's "idle", status gets { label: "", color: "transparent" }.
// This computed status variable is then used in the toolbar to display the appropriate label and color.
// It's much cleaner than writing multiple if/else checks everywhere.
const status = statusConfig[diary.saveStatus];
return (
// The Overall Layout Structure
<View style={s.root}>
The screen is split into two main areas:
The s.root style uses flexbox to arrange these side by side (root: { flex: 1, flexDirection: "row", backgroundColor: "#1a1f2e" },). The curly braces {s.root} mean “use the style defined in the StyleSheet at the bottom of the file.” This keeps styles organized in one place.
React Native uses a flexbox layout system. By default, items stack vertically. To put them side by side, you use flexDirection: ‘row’ on the parent container.
flexDirection: “row” tells React Native: “Lay out children horizontally, from left to right” Everything inside <View style={s.root}> becomes a row: first child → left; second child → right
The Sidebar (Left Panel)
{/* LEFT: entry list ──────────────────────────────────────────────────── */}
<View style={s.sidebar}>
// Sidebar Header
// The sidebar has a header showing the word "Diary" and a count of total entries.
// The diary.entries.length dynamically updates whenever entries are loaded or deleted.
// If you have 5 entries, it shows "5". If you delete one, it becomes "4" automatically
// because React re-renders when state changes.
<View style={s.sidebarHeader}>
<Text style={s.sidebarLabel}>Diary</Text>
<Text style={s.sidebarCount}>{diary.entries.length}</Text>
</View>
// Loading State vs. Entry List
// This is a conditional render using the ternary operator (condition ? trueResult : falseResult).
{diary.loading ? (
// When diary.loading is true: The app is still reading files from disk.
// Instead of showing an empty list (which might confuse the user), it shows a spinning ActivityIndicator.
// The gold color #c9a84c matches the app's prayer diary theme.
// The size="small" keeps it compact in the sidebar.
<View style={s.sidebarLoading}>
<ActivityIndicator size="small" color="#c9a84c" />
</View>
) : (
// When diary.loading is false: The entries have loaded (or failed silently),
// so we show the actual list wrapped in a ScrollView. The showsVerticalScrollIndicator={false}
// hides the scrollbar for a cleaner look - users can still scroll with touch gestures.
<ScrollView showsVerticalScrollIndicator={false}>
// Each Entry Row
// The .map() function transforms an array into UI elements.
// For each entry in diary.entries, it creates a TouchableOpacity (a tappable row).
{diary.entries.map((entry: DiaryEntrySummary) => (
<TouchableOpacity
// The key prop: React requires a unique identifier for each item in a list.
// This helps React efficiently update the list when items are added or removed.
// The entry's date is perfect because it's unique - you can't have two entries for the same date.
key={entry.date}
// the style array: This is a powerful React Native pattern.
// You can pass an array of styles, and they merge from left to right. So:
style={[
s.entryRow, // Every row gets the base s.entryRow style
// If this entry's date matches diary.currentDate (it's the currently selected entry),
// it ALSO gets s.entryRowSelected style
entry.date === diary.currentDate && s.entryRowSelected,
]}
// The onPress handler: An arrow function that calls diary.openEntry(entry.date).
// The arrow function is necessary because we need to pass the entry's date.
onPress={() => diary.openEntry(entry.date)}
>
// Entry Row Contents. Each row shows three pieces of information:
// 1. The date: Conditionally styled - highlighted when selected.
<Text style={[
s.entryDate,
entry.date === diary.currentDate && s.entryDateSelected,
]}>
{entry.date}
</Text>
// 2. Word count: Shows how long the entry is, e.g., "247 words"
<Text style={s.entryMeta}>
{entry.wordCount} words
</Text>
// 3. Preview text: The first few lines of the entry content, limited to 2 lines with numberOfLines={2}.
// This lets users see what each entry is about without opening it.
<Text style={s.entryPreview} numberOfLines={2}>
{entry.preview}
</Text>
</TouchableOpacity>
))}
// Empty State. Show the empty‑state message only when there are zero entries and we’re not loading.
// This uses the logical AND operator (&&) for conditional rendering. React only renders "Tap New Entry to start" if
// diary.entries.length === 0 (no entries exist)
// !diary.loading (we're not currently loading - avoids showing "no entries" during the initial load)
// React evaluates the expression left‑to‑right: If both are true, React renders the <Text> block.
{diary.entries.length === 0 && !diary.loading && (
<Text style={s.emptyText}>
No entries yet.{"\n"}Tap New Entry to start.
</Text>
)}
</ScrollVie>
)}
// New Entry Button
// This button sits at the bottom of the sidebar. When pressed, it calls diary.newEntry(), which we learned earlier:
// (*) Saves any unsaved work
// (*) Opens today's entry (or creates a new template if it doesn't exist)
<TouchableOpacity style={s.newBtn} onPress={diary.newEntry}>
<Text style={s.newBtnText}>+ New Entry</Text>
</TouchableOpacity>
</View>
{/* The Main Area. RIGHT: editor ─────────────────────────────────────────────────────── */}
<View style={s.main}>
// The main area has two completely different states:
// 1. An entry is open (diary.currentDate is not null): Shows the editor, toolbar, and debug panel
// This is another conditional render using the ternary operator
{diary.currentDate ? (
<>
// When an Entry IS Open
{/* Toolbar */}
// The toolbar shows information and actions related to the current entry.
<View style={s.toolbar}>
// It starts with the date of the open entry, displayed prominently.
<Text style={s.toolbarDate}>{diary.currentDate}</Text>
// Unsaved Indicator
// This uses the && pattern again. When diary.isDirty is true (there are unsaved changes),
// a small badge appears showing "● unsaved." The bullet character (●) draws attention.
// This visual indicator helps users know when they need to save.
{diary.isDirty && (
<Text style={s.dirtyBadge}>● unsaved</Text>
)}
// Save Status Display
// Remember the status variable we computed earlier? Here's where it's used.
// If status.label has a value (meaning we're not in "idle" state), we display it with the appropriate color.
// The style uses an array with both a predefined style and an inline style.
// The { color: status.color } part overrides the color dynamically based on the current status.
// When idle, status.label is empty string, which is "falsy" in JavaScript, so nothing renders.
{status.label ? (
<Text style={[s.saveStatus, { color: status.color }]}>
{status.label}
</Text>
) : null}
// Save Button
<TouchableOpacity
// Conditional styling: When !diary.isDirty (the document is clean), the button gets a "disabled" appearance.
// This provides visual feedback that there's nothing to save.
style={[s.saveBtn, !diary.isDirty && s.saveBtnDisabled]}
onPress={diary.saveEntry}
// The disabled prop: The button is disabled (doesn't respond to taps) when
// (*) !diary.isDirty - nothing to save
// OR (*) diary.saving - a save is already in progress
disabled={!diary.isDirty || diary.saving}
>
<Text style={s.saveBtnText}>Save</Text>
</TouchableOpacity>
// Delete Button
<TouchableOpacity
style={s.deleteBtn}
// The ! after diary.currentDate is TypeScript's non-null assertion operator.
// It tells TypeScript "I know this might be null according to the type, but I guarantee it's not null here."
// The arrow function () => diary.deleteEntry(diary.currentDate!) is needed to pass the current date as an argument.
onPress={() => diary.deleteEntry(diary.currentDate!)}
>
<Text style={s.deleteBtnText}>Delete</Text>
</TouchableOpacity>
</View>
{/* Auto-save hint */}
// This provides helpful information to the user.
// When there are unsaved changes, it appends "Unsaved changes." as a warning. When clean, no extra message.
// This educates the user about the auto-save feature while also serving as an additional dirty state indicator.
<Text style={s.autoSaveHint}>
Auto-saves 2 seconds after you stop typing.
{diary.isDirty ? " Unsaved changes." : ""}
</Text>
{/* The Text Editor */}
// This is the heart of the diary - where users write.
<TextInput
// style: Applies the editor styling.
style={s.editor}
// This makes the TextInput a controlled component. The text displayed is always exactly diary.content.
// The TextInput doesn't maintain its own internal text state - React controls it completely.
// This is why we needed the setContent wrapper function in the hook.
value={diary.content}
// Every time the user types a character, this function is called with the new full text.
// This triggers the chain: update content → check if dirty → reset auto-save timer.
// Without this, the editor wouldn't respond to typing.
onChangeText={diary.setContent}
// Allows multiple lines of text.
multiline={true}
// On Android, multiline TextInputs sometimes center text vertically.
// This forces the text to start at the top, like a normal document.
textAlignVertical="top"
// The gray hint text shown when the editor is empty.
// It disappears as soon as the user starts typing. This gives users a clear indication of what this space is for.
placeholder="Write your prayer diary entry here..."
// Controls the color of the placeholder text. #4b5563 is a medium gray that's visible but not distracting.
placeholderTextColor="#4b5563"
// Enables the keyboard's autocorrect feature. For a diary, this is helpful.
autoCorrect={true}
// Automatically capitalizes the first letter of each sentence. A nice touch for a writing app.
autoCapitalize="sentences"
/>
{/* Debug panel */}
// This is a test screen, so it has a debug panel.
// This shows the raw state values in real-time, which is invaluable for testing.
<View style={s.debugPanel}>
<Text style={s.debugTitle}>useDiary() state</Text>
// The ?? operator (nullish coalescing): diary.currentDate ?? "null" means "if currentDate is null or undefined,
// display the string 'null' instead." This prevents React from trying to render an actual null value,
// which would cause an error.
// The developer could verify that currentDate changes when opening different entries
<Text style={s.debugRow}>
currentDate: {diary.currentDate ?? "null"}
</Text>
// String(diary.isDirty): Converts the boolean true/false to the strings "true"/"false" for display.
// React can't render boolean values directly.
// The developer could verify that isDirty becomes true after typing and false after saving
<Text style={s.debugRow}>
isDirty: {String(diary.isDirty)}
</Text>
// The developer could verify that saveStatus cycles through idle → saving → saved → idle
<Text style={s.debugRow}>
saveStatus: {diary.saveStatus}
</Text>
<Text style={s.debugRow}>
content.length: {diary.content.length} chars
</Text>
<Text style={s.debugRow}>
entries.length: {diary.entries.length}
</Text>
</View>
>
) : (
// Recall. The main area has two completely different states.
// An entry is open AND No entry is open (diary.currentDate is null): Shows a welcome screen with instructions.
// The Welcome Screen. This is the empty state - what users see before they've opened any entry.
// Good empty states are crucial for user experience because they guide first-time users.
<View style={s.noEntry}>
// The notebook emoji (📔) gives a friendly, visual cue that this is a writing space.
<Text style={s.noEntryIcon}>📔</Text>
// The title "No entry open" clearly states the current state.
<Text style={s.noEntryTitle}>No entry open</Text>
// The instructional text tells the user exactly what to do: select from the list or create new.
<Text style={s.noEntryText}>
Tap an entry in the list or create a new one.
</Text>
// The prominent button gives a clear call to action. Notice it also calls diary.newEntry
// - the same function as the sidebar button. Duplicate entry points reduce friction for new users.
<TouchableOpacity style={s.noEntryBtn} onPress={diary.newEntry}>
<Text style={s.noEntryBtnText}>+ New Entry for Today</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
);
}
const s = StyleSheet.create({
root: { flex: 1, flexDirection: "row", backgroundColor: "#1a1f2e" },
// Sidebar
sidebar: { width: 160, backgroundColor: "#1e2338",
borderRightWidth: 1, borderRightColor: "#2d3550" },
sidebarHeader: { flexDirection: "row", justifyContent: "space-between",
alignItems: "center", paddingHorizontal: 12,
paddingVertical: 10, borderBottomWidth: 1,
borderBottomColor: "#2d3550" },
sidebarLabel: { fontSize: 10, fontWeight: "700", color: "#6b7280",
textTransform: "uppercase", letterSpacing: 0.8 },
sidebarCount: { fontSize: 10, color: "#5b8fd4", backgroundColor: "#1a2a3a",
paddingHorizontal: 5, paddingVertical: 1,
borderRadius: 8, overflow: "hidden" },
sidebarLoading: { padding: 20, alignItems: "center" },
entryRow: { paddingHorizontal: 12, paddingVertical: 10,
borderBottomWidth: 1, borderBottomColor: "#1a1f2e" },
entryRowSelected: { backgroundColor: "#222840",
borderLeftWidth: 2, borderLeftColor: "#c9a84c" },
entryDate: { fontSize: 12, color: "#9ca3af", fontFamily: "monospace",
marginBottom: 2 },
entryDateSelected:{ color: "#c9a84c", fontWeight: "700" },
entryMeta: { fontSize: 10, color: "#4b5563", marginBottom: 2 },
entryPreview: { fontSize: 11, color: "#6b7280", lineHeight: 15 },
emptyText: { padding: 16, fontSize: 12, color: "#4b5563",
textAlign: "center", lineHeight: 18 },
newBtn: { padding: 12, borderTopWidth: 1, borderTopColor: "#2d3550",
alignItems: "center" },
newBtnText: { fontSize: 12, color: "#c9a84c", fontWeight: "600" },
// Main editor area
main: { flex: 1, flexDirection: "column" },
toolbar: { flexDirection: "row", alignItems: "center",
paddingHorizontal: 14, paddingVertical: 8,
backgroundColor: "#222840", borderBottomWidth: 1,
borderBottomColor: "#2d3550", gap: 8, flexWrap: "wrap" },
toolbarDate: { fontSize: 12, color: "#c9a84c", fontFamily: "monospace",
fontWeight: "700" },
dirtyBadge: { fontSize: 11, color: "#f59e0b" },
saveStatus: { fontSize: 11, fontWeight: "600", marginLeft: "auto" as any },
saveBtn: { backgroundColor: "#5b8fd4", borderRadius: 6,
paddingHorizontal: 12, paddingVertical: 5 },
saveBtnDisabled: { backgroundColor: "#2d3550" },
saveBtnText: { fontSize: 12, color: "#fff", fontWeight: "600" },
deleteBtn: { backgroundColor: "#2d1a1a", borderRadius: 6,
paddingHorizontal: 10, paddingVertical: 5,
borderWidth: 1, borderColor: "#4a2d2d" },
deleteBtnText: { fontSize: 12, color: "#f87171" },
autoSaveHint: { fontSize: 10, color: "#4b5563", paddingHorizontal: 14,
paddingVertical: 5, fontStyle: "italic" },
editor: { flex: 1, padding: 16, color: "#dde4f0",
fontSize: 15, lineHeight: 24,
fontFamily: "monospace",
textAlignVertical: "top" },
debugPanel: { backgroundColor: "#1a1f2e", padding: 12,
borderTopWidth: 1, borderTopColor: "#2d3550" },
debugTitle: { fontSize: 10, color: "#6b7280", fontWeight: "700",
textTransform: "uppercase", marginBottom: 4 },
debugRow: { fontSize: 10, color: "#4b5563", fontFamily: "monospace",
lineHeight: 15 },
// No entry state
noEntry: { flex: 1, alignItems: "center", justifyContent: "center",
padding: 32, gap: 12 },
noEntryIcon: { fontSize: 48 },
noEntryTitle: { fontSize: 18, fontWeight: "bold", color: "#9ca3af" },
noEntryText: { fontSize: 13, color: "#6b7280", textAlign: "center",
lineHeight: 20 },
noEntryBtn: { backgroundColor: "#5b8fd4", borderRadius: 8,
paddingVertical: 12, paddingHorizontal: 24 },
noEntryBtnText: { color: "#fff", fontSize: 14, fontWeight: "700" },
});
This screen is a complete demonstration of the useDiary hook in action:
useDiary() initializes, refreshList() loads entries from diskdiary.newEntry() runs, today’s template appears in the editoronChangeText calls diary.setContent(), which updates content and marks it as dirtydiary.openEntry() saves current work, loads the selected entrydiary.deleteEntry() removes the file, refreshes the list, clears editor if needednpx expo start --clear| Step | Action | Expected |
|---|---|---|
| 1 | Open Diary tab | “No entry open” + empty list |
| 2 | Tap + New Entry |
Editor fills with today’s template |
| 3 | Wait 2 seconds | “✅ Saved” status appears |
| 4 | Left list | Shows today’s date + word count |
| 5 | Type more text | ● unsaved badge + isDirty: true in debug panel |
| 6 | Stop typing, wait 2s | “💾 Saving…” → “✅ Saved” |
● unsaved badge+ New Entry — blank template appears