Skip to content

Conversation

@MarcelOlsen
Copy link
Owner

@MarcelOlsen MarcelOlsen commented Oct 29, 2025

Fiber Architecture Implementation

Overview

This PR replaces MiniReact's recursive rendering system with a complete fiber-based architecture. The fiber architecture is how modern React works internally, and it gives us a solid foundation for future features like concurrent mode and Suspense while improving performance and maintainability.

Why Fiber?

The old recursive approach had some fundamental limitations that were becoming problematic:

Stack depth issues - Deep component trees could blow the stack. With recursion, there's no way around this except limiting tree depth.

Non-interruptible rendering - Once rendering started, it had to complete. This blocked the main thread and could make UIs feel sluggish.

No concurrent mode support - You can't pause or prioritize recursive work. Updates were all-or-nothing.

Limited error recovery - Error boundaries are hard to implement properly when you're deep in a recursive call stack.

Performance bottlenecks - Re-rendering entire subtrees even when only small parts changed, because there wasn't a good way to track what actually needed updating.

Fiber solves all of these by breaking rendering into small units of work that can be paused, resumed, and prioritized.

What's a Fiber?

A fiber is just a JavaScript object that represents a component instance and tracks its work. Instead of using the call stack, we build an explicit tree of fiber objects linked by pointers. This lets us traverse the tree iteratively and maintain our own work queue.

The key insight is maintaining two trees - the current tree (what's on screen) and the work-in-progress tree (what we're building). This double-buffering pattern means we can work on updates without disrupting the current UI, then swap the trees atomically when we're done.

Key Features

Core Architecture

Double-buffering - Current and work-in-progress trees linked through the alternate pointer. Enables safe, interruptible updates.

Incremental rendering - Work processed unit-by-unit using iterative tree traversal. No recursion means no stack depth issues.

Effect lists - Instead of walking the entire tree during commit, we maintain a linked list of only the fibers that have changes. This makes commit phase O(changes) instead of O(total nodes).

Work loop - Clean separation between render phase (building the tree) and commit phase (applying changes to DOM).

Reconciliation Engine

O(n) diffing - Key-based reconciliation keeps things fast. We make smart assumptions (same type = reuse, different type = replace) to avoid O(n³) tree comparison.

Mixed keyed/unkeyed handling - Properly handles edge cases when you mix keyed and unkeyed children in the same list.

Type-based reuse - When component types match, we reuse the fiber and just update it. When types differ, we know we need a full replacement.

Optimized paths - Special fast paths for common cases like single children or empty children.

Event System

Full delegation - All events delegated to the root container. Better performance and easier to manage.

Synthetic events - Normalized cross-browser event handling. Your event handlers get consistent objects regardless of browser.

Portal event bubbling - Events bubble through the React component tree, not the DOM tree. This is important for portals to work correctly.

Proper cleanup - Event listeners removed when components unmount. No memory leaks.

Hooks Integration

Fiber-based storage - Hooks stored directly on fiber objects. Each fiber maintains its own hook array.

Update queues - State updates are queued and batched. Multiple setState calls in the same render get processed together.

Effect scheduling - Effects run at the right time with proper cleanup handling.

Architecture

The fiber system is split into focused modules:

src/fiber/
├── types.ts              # Core data structures and type definitions
├── fiberRoot.ts          # Root management and container tracking
├── fiberCreation.ts      # Fiber creation with object pooling
├── workLoop.ts           # Main rendering coordinator
├── beginWork.ts          # Fiber processing and component calls
├── completeWork.ts       # DOM node creation and effect lists
├── commitWork.ts         # DOM mutations and effect application
├── reconcileChildren.ts  # The actual diffing algorithm
├── domOperations.ts      # Low-level DOM utilities
├── fiberHooks.ts         # Hook state and update queues
└── fiberFlags.ts         # Effect tags (placement, update, deletion)

Each module has a single clear responsibility. The work loop coordinates everything, begin work processes components, reconciliation diffs children, complete work builds DOM nodes, and commit work applies changes.

Documentation

Added comprehensive docs that explain how everything works:

Fiber Architecture (docs/01-fiber-architecture.md) - The core concepts, double-buffering pattern, and tree structure.

Work Loop (docs/02-work-loop.md) - How rendering is coordinated, the traversal algorithm, and where work happens.

Reconciliation (docs/03-reconciliation.md) - The diffing algorithm, key-based matching, and how we handle updates efficiently.

Event System (docs/04-event-system.md) - Event delegation, synthetic events, and how portal bubbling works.

Hooks System (docs/05-hooks-system.md) - Hook storage on fibers, update queues, and effect scheduling.

Overview (docs/README.md) - Navigation guide and getting started.

Each doc includes practical examples, implementation notes, and thoughts on future enhancements. The goal is to make the codebase approachable for contributors.

Example App Rewrite

The example app got a complete overhaul with modern tooling:

ElysiaJS server - Type-safe server with proper MIME types. Fast and lightweight.

Full TypeScript - Everything properly typed, no any types anywhere in the app code.

Comprehensive demos showing all MiniReact features:

  • Counter using useState
  • Todo list with complex state management
  • Timer demonstrating useEffect with cleanup
  • Portal showing event bubbling across DOM boundaries
  • Memoization preventing unnecessary re-renders
  • useCallback and useMemo for performance
  • Theme switching with useContext

The app is clean, well-documented, and shows best practices for using MiniReact.

Technical Details

TypeScript

Fixed all type errors properly without resorting to any:

  • Proper union types for fiber stateNode: Node | FiberRoot | PortalContainer | null
  • Added explanatory @ts-expect-error comments only where our types intentionally differ from React
  • Used @ts-nocheck in test files that deliberately use incomplete props for testing
  • Zero any types in source code (tests use it only for edge case testing)

Linting

  • Added Biome rules for test files (allow noNonNullAssertion since tests often assert non-null)
  • Configured ignore patterns for built assets
  • All source files pass linting
  • Consistent formatting throughout

Testing

All existing tests pass plus comprehensive new fiber tests:

Integration tests - Full render cycles, updates, unmounting, effects
Unit tests - Individual fiber operations in isolation
Edge cases - Mixed keyed/unkeyed children, portal events, synthetic event properties
Reconciliation tests - Key matching, element moves, insertions, deletions
Hook tests - State updates, effect cleanup, batching behavior

Test coverage maintained at 100% with 484 passing tests.

Performance

The fiber architecture improves performance in several ways:

Efficient updates - Only changed nodes are processed during commit. If 5 nodes changed in a 10,000 node tree, we process 5 nodes.

No recursion - Iterative traversal means no stack depth limits. Render trees as deep as you want.

Fiber pooling - The alternate pointer lets us reuse fiber objects between renders instead of allocating new ones.

Effect lists - Building a linked list of changes during render means commit phase is O(changes) not O(nodes).

Early bailout - When props haven't changed and there's no local state update, we skip entire subtrees.

Rendering is still synchronous for now (predictable behavior, simpler implementation), but the architecture is ready for concurrent mode and time-slicing when we want to add them.

Breaking Changes

None for the public API. Everything that worked before still works:

render(<App />, container)
useState(initialState)
useEffect(effect, deps)
createContext(defaultValue)
createPortal(children, container)
memo(component)
// ... everything else

Internally, the old VDOMInstance system is gone, but that was already deprecated and not part of the public API.

Testing

To verify everything works:

# Run all tests
bun test

# Check types
bun run typecheck

# Check linting
bun run lint

# Try the example app
cd example
bun run dev
# Visit http://localhost:3000

All checks pass. 484 tests, zero type errors, zero lint errors.

Future Work

This implementation sets us up for some really interesting features:

Concurrent mode - Time-slice rendering so the UI stays responsive. High-priority updates (typing) can interrupt low-priority ones (data fetching).

Suspense - Async rendering with proper loading states. Components can "suspend" while waiting for data.

Error boundaries - Catch errors in component trees and show fallback UI. The fiber tree structure makes this straightforward.

Profiler - Measure render times and identify performance bottlenecks. Fiber gives us the hooks we need.

Server components - Better server-side rendering with streaming and selective hydration.

The hard part (the architecture) is done. Adding these features is mostly incremental work now.

Migration

No migration needed. This is a drop-in replacement for the old rendering system. Existing code works without any changes.

References

This implementation draws heavily from React's fiber architecture:

  • React Fiber Architecture by Andrew Clark
  • Inside Fiber: In-depth overview by Max Koretskyi
  • React source code (particularly the reconciler)

We've simplified some parts and left out others (no class components, no legacy context), but the core ideas are the same.

Checklist

  • All tests passing (484/484)
  • TypeScript clean (zero errors)
  • Linting clean (zero errors)
  • Documentation complete
  • Example app working
  • No breaking changes to public API
  • Performance maintained or improved
  • No any types in source code

Ready to merge.

Summary by CodeRabbit

  • Documentation

    • Added comprehensive guides on the fiber architecture, work loop, reconciliation, event system, and hooks system.
  • New Features

    • Introduces a Fiber-based renderer and new synchronous work/commit pipeline plus migrated Fiber-backed hooks.
    • Enhanced event system cleanup and portal delegation handling.
  • Example Application

    • Redesigned TypeScript showcase with Bun-based scripts, Elysia server, and a consolidated demo app.
  • Tests

    • Added extensive unit and integration tests covering fiber phases, reconciliation, work loop, and commit behavior.

…ine and comprehensive documentation

Replaces the old recursive rendering system with React-like fiber architecture for better performance and future concurrent mode support, includes full test suite, modernized ElysiaJS example app, and detailed design documentation for all core systems.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

This PR migrates the renderer from a VDOMInstance model to a Fiber-based architecture: introduces fiber types, work loop (begin/complete/commit), reconciliation, fiber-backed hooks/context, DOM ops, event adjustments, docs, example overhaul to Bun/Elysia, and extensive fiber tests.

Changes

Cohort / File(s) Summary
Core Fiber Types & Flags
src/fiber/types.ts, src/fiber/fiberFlags.ts
Adds full Fiber type system, Portal/FiberRoot types, effect tags, lane bitflags and helpers.
Fiber Creation & Root
src/fiber/fiberCreation.ts, src/fiber/fiberRoot.ts, src/fiber/index.ts
Implements createFiber, createWorkInProgress, cloneChildFibers, FiberRoot registry (WeakMap) and re-exports fiber APIs.
Work Loop & Scheduling
src/fiber/workLoop.ts, src/fiber/beginWork.ts, src/fiber/completeWork.ts, src/fiber/reconcileChildren.ts
Implements synchronous non-recursive work loop, beginWork dispatcher per fiber type, completeWork DOM prep, and child reconciliation (keyed/unkeyed diffing).
Commit & DOM Mutation
src/fiber/commitWork.ts, src/fiber/domOperations.ts
Implements commitRoot with mutation/layout phases, placement/update/deletion semantics, ref attach/detach, and DOM utilities (insertBefore, removeChild, set/update props, text updates).
Fiber Hooks Integration
src/fiber/fiberHooks.ts, src/hooks/fiberHooksImpl.ts, src/hooks/index.ts, src/hooks/types.ts, src/hooks/index.ts.backup
Introduces per-fiber hook cursor and UpdateQueue, dispatch/enqueue/process logic, fiber-based hook implementations and re-exports; backup preserves legacy approach.
Core API & Context Migration
src/core/index.ts, src/context/index.ts, src/MiniReact.ts
Replaces VDOMInstance rendering with FiberRoot scheduling, deprecates old find/get APIs (wrappers), moves context to fiber.contextValues and exposes eventSystem/TEXT_ELEMENT/FRAGMENT/PORTAL.
Fiber Utilities & Public Surface
src/fiber/* (re-exports)
New central fiber index exposes types, flags, creation, workLoop, reconcile, begin/complete/commit and hook accessors.
Event System
src/events/eventSystem.ts
Cleanup extended to remove listeners from portal containers and clear portal tracking.
JSX Runtime
src/jsx-dev-runtime.ts
Exposes named jsxDEV in addition to jsx/jsxs aliases.
Documentation
docs/* (docs/README.md, docs/01-fiber-architecture.md, docs/02-work-loop.md, docs/03-reconciliation.md, docs/04-event-system.md, docs/05-hooks-system.md)
Adds comprehensive design docs covering fiber architecture, work loop, reconciliation, event system, and hooks.
Example Project — Overhaul
example/package.json, example/tsconfig.json, example/biome.json, example/README.md, example/.gitignore
Migrates example to Bun/TypeScript/Elysia, updates scripts, tooling, tsconfig paths and linter/formatter settings.
Example App / Server / Templates
example/src/app.tsx, example/src/server.ts, example/src/templates/index.html
Adds TS showcase app (Counter, TodoList, PortalDemo, etc.), Elysia server, and an HTML template; removes old JSX demo files.
Removed Legacy Example Files
example/public/index.html, example/src/index.js, example/webpack.config.js, example/babel.config.js, example/src/*.jsx
Deletes webpack/babel configs, legacy bootstrap and multiple demo components (App.jsx, Counter.jsx, Modal.jsx, ReducerDemo.jsx, RefDemo.jsx, TodoList.jsx).
Project Config & Scripts
package.json, biome.json, .gitignore, .trae/rules/project_rules.md
Adds typecheck script and prepublish sequencing; updates biome rules/ignores and test-specific override; removes commit-hygiene doc block.
Tests — Fiber & Rendering
tests/fiber/*, tests/MiniReact.*.test.ts
Adds extensive tests for fiber creation, begin/complete/commit phases, reconciliation, work loop, integration, effect flags, falsy rendering and harness adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant App as User Code
    participant WL as WorkLoop
    participant BW as BeginWork
    participant CW as CompleteWork
    participant CM as CommitRoot
    participant DOM as Browser DOM

    App->>WL: scheduleUpdateOnFiber(fiber)
    WL->>WL: performSyncWorkOnRoot()
    WL->>WL: renderRootSync() - create WIP tree

    rect rgb(230,240,255)
      Note over WL,BW: Render Phase (build WIP)
      loop workLoopSync
        WL->>BW: beginWork(current, workInProgress)
        BW->>WL: reconciled children / returned child
        WL->>WL: descend or completeUnitOfWork
        WL->>CW: completeUnitOfWork() when needed
        CW->>CW: completeWork() — create stateNode, mark effects
      end
    end

    rect rgb(255,235,240)
      Note over CM,DOM: Commit Phase (mutation + layout)
      WL->>CM: commitRoot(root)
      CM->>CM: commitMutationEffects (deletions, placements, updates)
      CM->>DOM: insertBefore / removeChild / updateText / set attrs
      CM->>CM: commitLayoutEffects (refs, layout effects)
    end

    CM-->>App: render complete
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

This is a foundational architectural rewrite—tight coupling between types/flags, pointer management, and effect ordering. Review should be deep and methodical.

  • Pay extra attention to:
    • src/fiber/reconcileChildren.ts — keyed diffing, lastPlacedIndex, deletion wiring
    • src/fiber/completeWork.ts — DOM creation order, appendAllChildren, eventSystem VDOMInstance mapping
    • src/fiber/commitWork.ts — deletion traversal, ref attach/detach, effect execution order
    • src/fiber/fiberHooks.ts & src/hooks/fiberHooksImpl.ts — update queue semantics, eager bailout, scheduling correctness
    • src/context/index.ts — provider lookup along fiber.return vs. legacy stack

Possibly related PRs

Poem

🧵 Fibers stitched with sharp precision,
VDOM fades — we start the mission.
Begin, complete, commit — the trio,
Non-recursive loop, no ego.
Types tight, flags aligned — ship it, bro.

Pre-merge checks

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'implement fiber architecture with complete reconciliation eng…' directly and comprehensively describes the main architectural change introduced in the changeset—migrating from a recursive renderer to a complete fiber-based reconciliation system.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (6)
example/src/types.d.ts (1)

1-1: Yo, chat, are we really doing triple-slash directives in 2025?

Listen, I know this works and all, but we're out here creating an entire file just to triple-slash reference some types? That's like... that's like creating a whole new component just to wrap a div, you know what I'm saying?

Here's the thing - triple-slash directives are the TypeScript equivalent of using var in JavaScript. They work, but they're old-school. Modern TypeScript has better ways to handle this:

Option 1 (The Based Approach): Just add this to your example/tsconfig.json:

{
  "include": [
    "src/**/*",
    "../src/jsx.d.ts"
  ]
}

Option 2 (Also Valid): If you need the types globally available in your example app, you could add it to the types array in tsconfig.

This whole file could just... not exist. And that's beautiful, chat. Less code, more type safety, same result. Plus you're not maintaining a separate file that's literally one line of path magic.

But hey, it works, so if you wanna ship this and refactor later when you're not knee-deep in fiber architecture, I get it. Just putting it out there that we can do better.

example/src/server.ts (1)

213-213: Hardcoded port 3000 - make it configurable.

Look, I get it, it's an example app. But hardcoding ports is how you end up with "port already in use" errors and confusion in different environments.

Do this:

+const PORT = Number(process.env.PORT) || 3000;
+
-.listen(3000);
+.listen(PORT);

Two lines. That's all it takes to make your life easier when you inevitably need to run this on a different port.

example/README.md (1)

112-121: Give the file tree fence a language.

This naked triple backtick is pure chaos. markdownlint is already screaming (MD040), and adding a language keeps the docs tooling happy. Just slap a neutral label like text on that block:

-```
+```text
 example/
 ├── src/
 │   ├── server.ts          # ElysiaJS server
@@
 └── README.md
-```
+```

Let’s keep the README as tidy as the code.

docs/03-reconciliation.md (1)

338-345: Slap a language on these fences

Come on, we’re better than raw triple-backticks floating in space. Markdown lint is already yelling because the example list of keyed vs unkeyed children lacks a language tag, and readers lose syntax highlighting. Tag it as text (or whatever fits) so the docs stay as polished as the fiber architecture you just shipped.

docs/05-hooks-system.md (1)

461-466: Give the update-queue diagram a language tag

Same story here: the circular queue snippet is naked markdown. Toss a text (or similar) language hint on that block so the lint bots chill and humans get consistent formatting. Easy win, no excuses.

src/hooks/types.ts (1)

12-36: Stop nuking our generics on UpdateQueue

Dude, we just torched all the compile-time signal by slapping UpdateQueue<unknown> onto state/reducer hooks. The whole point of threading these queues through the fiber types is to keep the actual state payload wired up. Keep it generic—UpdateQueue<T> for state hooks and UpdateQueue<State> for reducers—so downstream code doesn’t fall back to any-land. Patch it up like this:

-	queue?: UpdateQueue<unknown>; // Update queue for Fiber integration
+	queue?: UpdateQueue<T>; // Preserve the state type in the queue
...
-	queue?: UpdateQueue<unknown>; // Update queue for Fiber integration
+	queue?: UpdateQueue<State>; // Preserve reducer state typing

That way the scheduler keeps its TypeScript edge instead of devolving into shrug emoji types.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12c42ee and 341cc53.

⛔ Files ignored due to path filters (1)
  • example/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (56)
  • .gitignore (1 hunks)
  • .trae/rules/project_rules.md (0 hunks)
  • biome.json (2 hunks)
  • docs/01-fiber-architecture.md (1 hunks)
  • docs/02-work-loop.md (1 hunks)
  • docs/03-reconciliation.md (1 hunks)
  • docs/04-event-system.md (1 hunks)
  • docs/05-hooks-system.md (1 hunks)
  • docs/README.md (1 hunks)
  • example/.gitignore (1 hunks)
  • example/README.md (1 hunks)
  • example/babel.config.js (0 hunks)
  • example/biome.json (1 hunks)
  • example/package.json (1 hunks)
  • example/public/index.html (0 hunks)
  • example/src/App.jsx (0 hunks)
  • example/src/Counter.jsx (0 hunks)
  • example/src/Modal.jsx (0 hunks)
  • example/src/ReducerDemo.jsx (0 hunks)
  • example/src/RefDemo.jsx (0 hunks)
  • example/src/TodoList.jsx (0 hunks)
  • example/src/app.tsx (1 hunks)
  • example/src/index.js (0 hunks)
  • example/src/server.ts (1 hunks)
  • example/src/types.d.ts (1 hunks)
  • example/tsconfig.json (1 hunks)
  • example/webpack.config.js (0 hunks)
  • package.json (1 hunks)
  • src/MiniReact.ts (2 hunks)
  • src/context/index.ts (3 hunks)
  • src/core/index.ts (3 hunks)
  • src/fiber/beginWork.ts (1 hunks)
  • src/fiber/commitWork.ts (1 hunks)
  • src/fiber/completeWork.ts (1 hunks)
  • src/fiber/domOperations.ts (1 hunks)
  • src/fiber/fiberCreation.ts (1 hunks)
  • src/fiber/fiberFlags.ts (1 hunks)
  • src/fiber/fiberHooks.ts (1 hunks)
  • src/fiber/fiberRoot.ts (1 hunks)
  • src/fiber/index.ts (1 hunks)
  • src/fiber/reconcileChildren.ts (1 hunks)
  • src/fiber/types.ts (1 hunks)
  • src/fiber/workLoop.ts (1 hunks)
  • src/hooks/fiberHooksImpl.ts (1 hunks)
  • src/hooks/index.ts (1 hunks)
  • src/hooks/index.ts.backup (1 hunks)
  • src/hooks/types.ts (3 hunks)
  • src/jsx-dev-runtime.ts (1 hunks)
  • tests/MiniReact.jsx.test.ts (2 hunks)
  • tests/fiber/beginWork.test.ts (1 hunks)
  • tests/fiber/commitWork.test.ts (1 hunks)
  • tests/fiber/completeWork.test.ts (1 hunks)
  • tests/fiber/fiberCreation.test.ts (1 hunks)
  • tests/fiber/integration.test.ts (1 hunks)
  • tests/fiber/reconciliation.test.ts (1 hunks)
  • tests/fiber/workLoop.test.ts (1 hunks)
💤 Files with no reviewable changes (11)
  • .trae/rules/project_rules.md
  • example/src/App.jsx
  • example/src/index.js
  • example/src/Counter.jsx
  • example/src/Modal.jsx
  • example/babel.config.js
  • example/webpack.config.js
  • example/src/RefDemo.jsx
  • example/src/TodoList.jsx
  • example/src/ReducerDemo.jsx
  • example/public/index.html
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: MarcelOlsen
PR: MarcelOlsen/mini-react#8
File: README.md:701-708
Timestamp: 2025-06-17T12:40:39.737Z
Learning: MarcelOlsen prefers to keep test documentation simple with just one example of running specific test files, rather than listing detailed commands for each test suite in the README.
🧬 Code graph analysis (23)
src/context/index.ts (3)
src/fiber/fiberHooks.ts (1)
  • getCurrentRenderingFiber (49-51)
src/fiber/index.ts (1)
  • getCurrentRenderingFiber (103-103)
src/hooks/index.ts (1)
  • getCurrentRenderingFiber (56-56)
src/fiber/fiberCreation.ts (3)
src/MiniReact.ts (5)
  • ElementType (11-11)
  • AnyMiniReactElement (8-8)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/fiber/types.ts (2)
  • Props (23-28)
  • Fiber (141-335)
src/fiber/fiberFlags.ts (2)
  • NoEffect (15-15)
  • NoLanes (42-42)
src/fiber/fiberRoot.ts (2)
src/fiber/types.ts (2)
  • FiberRoot (349-400)
  • Fiber (141-335)
src/fiber/fiberFlags.ts (1)
  • NoLanes (42-42)
tests/fiber/integration.test.ts (4)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/workLoop.ts (1)
  • scheduleUpdateOnFiber (39-46)
src/MiniReact.ts (3)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • AnyMiniReactElement (8-8)
src/fiber/types.ts (1)
  • Fiber (141-335)
tests/fiber/beginWork.test.ts (3)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
src/fiber/beginWork.ts (1)
  • beginWork (44-81)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
example/src/app.tsx (6)
src/MiniReact.ts (11)
  • MiniReactContext (37-37)
  • createContext (36-36)
  • useState (17-17)
  • useRef (20-20)
  • MutableRefObject (32-32)
  • useEffect (18-18)
  • memo (84-99)
  • useCallback (22-22)
  • useMemo (21-21)
  • useContext (36-36)
  • render (6-6)
src/context/index.ts (2)
  • createContext (33-80)
  • useContext (87-114)
src/hooks/fiberHooksImpl.ts (5)
  • useState (92-148)
  • useRef (293-323)
  • useEffect (241-285)
  • useCallback (384-428)
  • useMemo (332-375)
src/hooks/types.ts (1)
  • MutableRefObject (113-115)
src/portals/types.ts (1)
  • PortalElement (8-14)
src/core/index.ts (1)
  • render (64-100)
src/fiber/workLoop.ts (6)
src/fiber/types.ts (2)
  • Fiber (141-335)
  • FiberRoot (349-400)
src/performance/index.ts (2)
  • trackRenderStart (44-48)
  • trackRenderEnd (53-60)
src/fiber/commitWork.ts (1)
  • commitRoot (36-59)
src/fiber/fiberCreation.ts (1)
  • createWorkInProgress (102-161)
src/fiber/beginWork.ts (1)
  • beginWork (44-81)
src/fiber/completeWork.ts (1)
  • completeWork (39-67)
tests/fiber/reconciliation.test.ts (4)
src/fiber/fiberCreation.ts (1)
  • createFiber (25-85)
src/fiber/reconcileChildren.ts (1)
  • reconcileChildren (83-102)
src/fiber/fiberFlags.ts (2)
  • Placement (21-21)
  • UpdateEffect (27-27)
src/fiber/types.ts (1)
  • Fiber (141-335)
src/fiber/beginWork.ts (4)
src/fiber/types.ts (1)
  • Fiber (141-335)
src/fiber/reconcileChildren.ts (1)
  • reconcileChildren (83-102)
src/fiber/fiberCreation.ts (1)
  • createWorkInProgress (102-161)
src/fiber/fiberHooks.ts (1)
  • setCurrentRenderingFiber (34-40)
tests/fiber/completeWork.test.ts (6)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
src/fiber/completeWork.ts (1)
  • completeWork (39-67)
src/fiber/fiberFlags.ts (2)
  • Placement (21-21)
  • UpdateEffect (27-27)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/beginWork.ts (1)
  • beginWork (44-81)
src/fiber/types.ts (1)
  • Fiber (141-335)
src/fiber/reconcileChildren.ts (4)
src/MiniReact.ts (2)
  • AnyMiniReactElement (8-8)
  • TEXT_ELEMENT (133-133)
src/fiber/types.ts (2)
  • Fiber (141-335)
  • isSameElementType (511-516)
src/fiber/fiberCreation.ts (4)
  • createFiberFromElement (171-219)
  • getElementKey (335-353)
  • getElementType (389-411)
  • createWorkInProgress (102-161)
src/fiber/fiberFlags.ts (3)
  • Placement (21-21)
  • Deletion (35-35)
  • UpdateEffect (27-27)
src/fiber/commitWork.ts (3)
src/fiber/types.ts (4)
  • FiberRoot (349-400)
  • Fiber (141-335)
  • PortalContainer (341-343)
  • RefObject (15-15)
src/fiber/fiberFlags.ts (2)
  • Placement (21-21)
  • UpdateEffect (27-27)
src/fiber/domOperations.ts (4)
  • insertBefore (23-33)
  • updateTextContent (147-149)
  • updateProperties (83-139)
  • removeChild (41-43)
src/hooks/fiberHooksImpl.ts (3)
src/hooks/types.ts (12)
  • UseStateHook (71-74)
  • StateHook (8-13)
  • StateOrEffectHook (62-69)
  • Reducer (86-86)
  • ReducerHook (30-36)
  • EffectCallback (77-77)
  • DependencyList (78-78)
  • EffectHook (15-22)
  • MutableRefObject (113-115)
  • RefHook (38-41)
  • MemoHook (43-48)
  • CallbackHook (50-56)
src/fiber/fiberHooks.ts (7)
  • getCurrentRenderingFiber (49-51)
  • createUpdateQueue (62-72)
  • processUpdateQueue (151-182)
  • dispatchSetState (110-140)
  • processReducerQueue (236-270)
  • dispatchReducerAction (195-225)
  • areDepsEqual (282-304)
src/fiber/types.ts (1)
  • UpdateQueue (79-116)
tests/fiber/fiberCreation.test.ts (4)
src/fiber/fiberCreation.ts (9)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
  • createFiberFromElement (171-219)
  • createFiberFromText (227-240)
  • createFiberFromFragment (252-265)
  • cloneFiber (305-313)
  • getElementKey (335-353)
  • getElementProps (361-381)
  • getElementType (389-411)
src/fiber/fiberFlags.ts (2)
  • NoEffect (15-15)
  • NoLanes (42-42)
src/MiniReact.ts (2)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
src/hooks/types.ts (1)
  • StateOrEffectHook (62-69)
tests/fiber/workLoop.test.ts (4)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/fiberCreation.ts (1)
  • createFiber (25-85)
src/fiber/workLoop.ts (2)
  • scheduleUpdateOnFiber (39-46)
  • getCurrentFiber (282-284)
src/MiniReact.ts (3)
  • AnyMiniReactElement (8-8)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/hooks/types.ts (1)
src/fiber/types.ts (1)
  • UpdateQueue (79-116)
tests/fiber/commitWork.test.ts (6)
src/fiber/types.ts (1)
  • FiberRoot (349-400)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
src/MiniReact.ts (1)
  • TEXT_ELEMENT (133-133)
src/fiber/fiberFlags.ts (3)
  • Placement (21-21)
  • UpdateEffect (27-27)
  • Deletion (35-35)
src/fiber/commitWork.ts (1)
  • commitRoot (36-59)
src/fiber/fiberHooks.ts (3)
src/fiber/types.ts (3)
  • Fiber (141-335)
  • UpdateQueue (79-116)
  • Update (54-69)
src/hooks/index.ts (3)
  • setCurrentRenderingFiber (55-55)
  • setCurrentRenderingFiber (57-57)
  • getCurrentRenderingFiber (56-56)
src/fiber/workLoop.ts (1)
  • scheduleUpdateOnFiber (39-46)
src/fiber/completeWork.ts (4)
src/fiber/types.ts (1)
  • Fiber (141-335)
src/fiber/fiberFlags.ts (2)
  • UpdateEffect (27-27)
  • Placement (21-21)
src/fiber/domOperations.ts (1)
  • setInitialProperties (51-72)
src/core/types.ts (1)
  • VDOMInstance (98-107)
src/fiber/fiberFlags.ts (1)
src/fiber/types.ts (2)
  • EffectTag (34-34)
  • Lanes (41-41)
src/fiber/domOperations.ts (1)
src/fiber/types.ts (1)
  • Props (23-28)
src/fiber/types.ts (3)
src/fiber/index.ts (23)
  • RefObject (17-17)
  • RefCallback (18-18)
  • Ref (16-16)
  • Props (15-15)
  • EffectTag (12-12)
  • Lanes (13-13)
  • SuspenseState (14-14)
  • Update (19-19)
  • UpdateQueue (20-20)
  • Fiber (10-10)
  • FiberRoot (11-11)
  • isFiber (23-23)
  • isFiberHostComponent (24-24)
  • isFiberFunctionComponent (25-25)
  • isFiberText (26-26)
  • isFiberFragment (27-27)
  • isFiberPortal (28-28)
  • isFiberRoot (29-29)
  • fiberHasEffect (30-30)
  • fiberHasChildEffects (31-31)
  • isSameType (32-32)
  • isSameElementType (33-33)
  • fiberNeedsRef (34-34)
src/MiniReact.ts (2)
  • AnyMiniReactElement (8-8)
  • ElementType (11-11)
src/hooks/types.ts (1)
  • StateOrEffectHook (62-69)
src/core/index.ts (3)
src/fiber/types.ts (1)
  • FiberRoot (349-400)
src/fiber/fiberRoot.ts (3)
  • hasFiberRoot (140-142)
  • getFiberRoot (130-132)
  • createFiberRoot (23-59)
src/fiber/workLoop.ts (1)
  • scheduleUpdateOnFiber (39-46)
🪛 ast-grep (0.39.6)
src/fiber/fiberRoot.ts

[warning] 195-195: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ""
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

src/core/index.ts

[warning] 97-97: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: containerNode.innerHTML = ""
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 LanguageTool
docs/02-work-loop.md

[style] ~355-~355: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...(does workInProgress become null)? 3. Is the effect list being built correctly? ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~356-~356: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...e effect list being built correctly? 4. Is commit running after render completes? ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~377-~377: ‘overall structure’ might be wordy. Consider a shorter alternative.
Context: ...ures later without major changes to the overall structure.

(EN_WORDINESS_PREMIUM_OVERALL_STRUCTURE)

docs/04-event-system.md

[style] ~51-~51: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...ent` is cached so we can add/remove the exact same function reference. ## Initialization ...

(EN_WORDINESS_PREMIUM_EXACT_SAME)

docs/03-reconciliation.md

[style] ~228-~228: Consider using “who” when you are referring to people instead of objects.
Context: ...as PLACEMENT) - Delete any old children that don't match ## Array Reconciliation T...

(THAT_WHO)


[style] ~445-~445: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ..., we can reuse the fiber. Otherwise, we need to create a new one. ## Deletion When a ...

(REP_NEED_TO_VB)

docs/05-hooks-system.md

[style] ~164-~164: Consider a more concise word here.
Context: ...ng = null } ``` We process all updates in order to compute the new state. ## useEffect S...

(IN_ORDER_TO_PREMIUM)

docs/README.md

[style] ~177-~177: This wording could be more concise.
Context: ... performance: O(n) Reconciliation: Linear in number of children Effect Lists: Only pro...

(ADJECTIVE_IN_ATTRIBUTE)

🪛 markdownlint-cli2 (0.18.1)
example/README.md

112-112: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/02-work-loop.md

245-245: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/03-reconciliation.md

338-338: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


407-407: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/05-hooks-system.md

461-461: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/01-fiber-architecture.md

56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/README.md

110-110: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (10)
example/.gitignore (1)

1-4: LGTM! Clean gitignore setup.

Yo, this is exactly what you want in a gitignore. Build artifacts (*.js, *.map), dependencies (node_modules), and that .DS_Store for the Mac devs. Chef's kiss. No weird patterns, no over-engineering. Just the essentials.

.gitignore (1)

178-178: LGTM!

Just formatting, no functional changes. Moving on.

tests/MiniReact.jsx.test.ts (2)

1-1: YES! Adding afterEach for cleanup.

Look at you importing afterEach! This is the setup for proper test cleanup. Let's see what you do with it...


19-24: THIS is proper test hygiene! LOVE IT!

BRO! You're cleaning up the DOM after each test! This is exactly what you should be doing. No more test pollution, no more "why does this test pass individually but fail in the suite" nonsense. You're removing the container from the DOM, checking parentNode first - defensive coding at its finest.

This is the kind of test discipline that separates the pros from the "works on my machine" crowd. Respect.

example/src/server.ts (1)

1-12: Static file serving looks solid.

The /app.js route is clean. Serving with Bun.file, proper content-type, cache headers. This is how you do it. No complaints here.

example/biome.json (1)

1-32: Clean biome config.

Standard biome setup - nothing wild here. Formatter enabled, linter enabled with recommended rules, ignoring the right directories. Tab indentation with width 2, double quotes, trailing commas - all reasonable choices.

This is exactly what a biome.json should look like. No over-configuration, no weird rules. Just good defaults.

src/jsx-dev-runtime.ts (1)

6-6: Nice - exposing jsxDEV as a direct named export.

So you were only exporting jsxDEV through the jsx and jsxs aliases, but now you're also exporting it directly. Smart move. This gives consumers the flexibility to import jsxDEV explicitly if they need it for debugging or tooling purposes, while maintaining backward compatibility with the aliases.

Clean API expansion. No breaking changes. This is how you do it.

biome.json (3)

16-17: Good call ignoring example/public.

Yeah, you don't want built artifacts and generated files in your lint/format runs. Clean.


38-40: noNonNullAssertion as error - I LOVE this.

YES! Making non-null assertions an error in production code. This is the energy we need. Non-null assertions (the ! operator) are basically you telling TypeScript "trust me bro" and we all know how that ends.

If you think something can't be null, prove it with proper type guards or optional chaining. Don't just bang it away with ! and hope for the best.

This is the kind of strict TypeScript configuration that keeps your codebase clean and prevents runtime nullability bugs.


43-54: Test override for noNonNullAssertion - pragmatic choice.

So you're disabling the non-null assertion rule specifically for test files. I get it. In tests, you're often asserting on things you know exist because you just set them up. Using non-null assertions in test code to clean up noise is reasonable.

BUT - and I'm watching you here - don't let this become an excuse for sloppy test setup. If you find yourself banging away with ! all over your tests, that's a smell that your test fixtures might need better types or setup.

This is fine for now, but keep it clean.

Comment on lines +450 to +464
cleanup(): void {
if (this.rootContainer) {
for (const eventName of this.registeredEvents) {
this.rootContainer.removeEventListener(
eventName,
this.boundHandleDelegatedEvent
)
}
}

this.registeredEvents.clear()
this.instanceToNode = new WeakMap()
this.nodeToInstance = new WeakMap()
this.rootContainer = null
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Portal cleanup leaks delegated listeners

Bruh, we ship a whole fiber-powered delegation engine and then bail before unplugging the portal wires. cleanup() only detaches from rootContainer, leaving every entry in portalContainers still pointing at boundHandleDelegatedEvent. After the root swaps you’ve now got stale listeners firing for a tree that no longer exists, plus you keep those container references alive forever. Let’s actually walk the set, remove each listener, and clear the set so the GC can breathe.

 cleanup(): void {
-  if (this.rootContainer) {
-    for (const eventName of this.registeredEvents) {
-      this.rootContainer.removeEventListener(
-        eventName,
-        this.boundHandleDelegatedEvent
-      )
-    }
-  }
+  if (this.rootContainer) {
+    for (const eventName of this.registeredEvents) {
+      this.rootContainer.removeEventListener(
+        eventName,
+        this.boundHandleDelegatedEvent
+      )
+    }
+  }
+
+  for (const portalContainer of this.portalContainers) {
+    for (const eventName of this.registeredEvents) {
+      portalContainer.removeEventListener(
+        eventName,
+        this.boundHandleDelegatedEvent
+      )
+    }
+  }
 
   this.registeredEvents.clear()
+  this.portalContainers.clear()
   this.instanceToNode = new WeakMap()
   this.nodeToInstance = new WeakMap()
   this.rootContainer = null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup(): void {
if (this.rootContainer) {
for (const eventName of this.registeredEvents) {
this.rootContainer.removeEventListener(
eventName,
this.boundHandleDelegatedEvent
)
}
}
this.registeredEvents.clear()
this.instanceToNode = new WeakMap()
this.nodeToInstance = new WeakMap()
this.rootContainer = null
}
cleanup(): void {
if (this.rootContainer) {
for (const eventName of this.registeredEvents) {
this.rootContainer.removeEventListener(
eventName,
this.boundHandleDelegatedEvent
)
}
}
for (const portalContainer of this.portalContainers) {
for (const eventName of this.registeredEvents) {
portalContainer.removeEventListener(
eventName,
this.boundHandleDelegatedEvent
)
}
}
this.registeredEvents.clear()
this.portalContainers.clear()
this.instanceToNode = new WeakMap()
this.nodeToInstance = new WeakMap()
this.rootContainer = null
}
🤖 Prompt for AI Agents
In docs/04-event-system.md around lines 450 to 464, cleanup() currently only
removes delegated listeners from rootContainer but leaves listeners attached to
every entry in portalContainers and retains those container references; update
cleanup() to iterate over the portalContainers set and call
removeEventListener(eventName, this.boundHandleDelegatedEvent) for each
container/event pair (mirroring what you do for rootContainer), then clear the
portalContainers set and any related maps (registeredEvents, instanceToNode,
nodeToInstance) and null out rootContainer so all references are released for
GC.

Comment on lines +7 to +17
"dev": "bun run build && bun run --watch src/server.ts",
"build": "bun build src/app.tsx --outdir=public --target=browser",
"build:prod": "bun build src/app.tsx --outdir=public --target=browser --minify",
"start": "bun run build:prod && bun run src/server.ts",
"typecheck": "tsc --noEmit",
"lint": "biome lint .",
"format": "biome format .",
"check": "biome check .",
"lint:write": "biome lint --write .",
"format:write": "biome format --write .",
"check:write": "biome check --write ."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dev script isn’t actually watching anything.

This script promises hot reload, but bun run --watch src/server.ts never flips on Bun’s watcher. The CLI only honors flags that sit immediately after bun, so this ends up trying to execute a script literally named --watch. No rebuilds, no restarts, just sadness.

Let’s move the flag where Bun expects it:

-		"dev": "bun run build && bun run --watch src/server.ts",
+		"dev": "bun run build && bun --watch run src/server.ts",

Now the dev loop will actually rebound when you touch the code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dev": "bun run build && bun run --watch src/server.ts",
"build": "bun build src/app.tsx --outdir=public --target=browser",
"build:prod": "bun build src/app.tsx --outdir=public --target=browser --minify",
"start": "bun run build:prod && bun run src/server.ts",
"typecheck": "tsc --noEmit",
"lint": "biome lint .",
"format": "biome format .",
"check": "biome check .",
"lint:write": "biome lint --write .",
"format:write": "biome format --write .",
"check:write": "biome check --write ."
"dev": "bun run build && bun --watch run src/server.ts",
"build": "bun build src/app.tsx --outdir=public --target=browser",
"build:prod": "bun build src/app.tsx --outdir=public --target=browser --minify",
"start": "bun run build:prod && bun run src/server.ts",
"typecheck": "tsc --noEmit",
"lint": "biome lint .",
"format": "biome format .",
"check": "biome check .",
"lint:write": "biome lint --write .",
"format:write": "biome format --write .",
"check:write": "biome check --write ."
🤖 Prompt for AI Agents
In example/package.json around lines 7 to 17, the "dev" script places the
--watch flag after "run" so Bun treats it as a script name instead of enabling
its watcher; update the "dev" script to put the flag immediately after "bun"
(e.g., change "bun run build && bun run --watch src/server.ts" to "bun run build
&& bun --watch run src/server.ts") so Bun's watcher is actually enabled for the
server process.

…test\n stability\n\n Address multiple issues identified in code review:\n\n **Build & CI**\n - Add typecheck to prepublishOnly pipeline to catch type errors before\n publishing\n\n **Core Fixes**\n - Fix ref lifecycle: detach old refs before attaching new ones when ref\n identity changes\n - Fix memo bailout sibling chain corruption by cloning entire child list\n instead of just first child\n - Convert EffectTag from string literals to bitwise flags to support combining\n multiple effects (Placement + Update)\n - Fix effect tag assignments to use |= instead of = to preserve existing flags\n (e.g., Placement)\n - Fix createFiberFromElement to return null for null/undefined elements (valid\n "render nothing" case)\n - Fix double-deletion bug by checking Deletion flag before calling deleteChild\n\n **Rendering Fixes**\n - Change truthy checks to nullish checks (?? and != null) to preserve valid\n falsy renderables (0, "", false)\n - Remove manual DOM clearing (innerHTML) to let fiber commit phase handle \n deletions properly via deletion list\n\n **Test Stability**\n - Remove all wall-clock timing assertions (performance.now() +\n expect(duration).toBeLessThan())\n - Replace with deterministic structural assertions (node counts, depth\n verification, effect chains)\n - Eliminated 11 flaky timing checks across beginWork, completeWork,\n integration, reconciliation, and workLoop tests
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 341cc53 and adc490d.

📒 Files selected for processing (30)
  • docs/01-fiber-architecture.md (1 hunks)
  • docs/02-work-loop.md (1 hunks)
  • docs/03-reconciliation.md (1 hunks)
  • docs/05-hooks-system.md (1 hunks)
  • docs/README.md (1 hunks)
  • example/README.md (1 hunks)
  • example/src/server.ts (1 hunks)
  • example/src/templates/index.html (1 hunks)
  • example/tsconfig.json (1 hunks)
  • package.json (1 hunks)
  • src/core/index.ts (3 hunks)
  • src/events/eventSystem.ts (1 hunks)
  • src/fiber/beginWork.ts (1 hunks)
  • src/fiber/commitWork.ts (1 hunks)
  • src/fiber/completeWork.ts (1 hunks)
  • src/fiber/fiberCreation.ts (1 hunks)
  • src/fiber/fiberFlags.ts (1 hunks)
  • src/fiber/fiberRoot.ts (1 hunks)
  • src/fiber/reconcileChildren.ts (1 hunks)
  • src/fiber/types.ts (1 hunks)
  • src/hooks/fiberHooksImpl.ts (1 hunks)
  • src/hooks/types.ts (3 hunks)
  • tests/MiniReact.falsyValues.test.ts (1 hunks)
  • tests/fiber/beginWork.test.ts (1 hunks)
  • tests/fiber/completeWork.test.ts (1 hunks)
  • tests/fiber/effectTags.test.ts (1 hunks)
  • tests/fiber/fiberCreation.test.ts (1 hunks)
  • tests/fiber/integration.test.ts (1 hunks)
  • tests/fiber/reconciliation.test.ts (1 hunks)
  • tests/fiber/workLoop.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: MarcelOlsen
Repo: MarcelOlsen/mini-react PR: 8
File: README.md:701-708
Timestamp: 2025-06-17T12:40:39.737Z
Learning: MarcelOlsen prefers to keep test documentation simple with just one example of running specific test files, rather than listing detailed commands for each test suite in the README.
🧬 Code graph analysis (18)
tests/fiber/workLoop.test.ts (3)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/fiberCreation.ts (1)
  • createFiber (25-85)
src/fiber/workLoop.ts (2)
  • scheduleUpdateOnFiber (39-46)
  • getCurrentFiber (282-284)
src/fiber/commitWork.ts (4)
src/fiber/types.ts (4)
  • FiberRoot (350-401)
  • Fiber (142-336)
  • PortalContainer (342-344)
  • RefObject (15-15)
src/fiber/fiberFlags.ts (3)
  • hasEffectTag (75-77)
  • Placement (22-22)
  • UpdateEffect (28-28)
src/MiniReact.ts (2)
  • PORTAL (133-133)
  • TEXT_ELEMENT (133-133)
src/fiber/domOperations.ts (4)
  • insertBefore (23-33)
  • updateTextContent (147-149)
  • updateProperties (83-139)
  • removeChild (41-43)
tests/fiber/completeWork.test.ts (6)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
src/fiber/completeWork.ts (1)
  • completeWork (39-67)
src/fiber/fiberFlags.ts (2)
  • Placement (22-22)
  • UpdateEffect (28-28)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/beginWork.ts (1)
  • beginWork (44-81)
src/fiber/types.ts (1)
  • Fiber (142-336)
tests/fiber/beginWork.test.ts (3)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
src/fiber/beginWork.ts (1)
  • beginWork (44-81)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
tests/fiber/effectTags.test.ts (1)
src/fiber/fiberFlags.ts (1)
  • hasEffectTag (75-77)
src/fiber/reconcileChildren.ts (3)
src/fiber/types.ts (2)
  • Fiber (142-336)
  • isSameElementType (512-517)
src/fiber/fiberCreation.ts (4)
  • createFiberFromElement (220-270)
  • getElementKey (386-404)
  • getElementType (440-462)
  • createWorkInProgress (102-161)
src/fiber/fiberFlags.ts (4)
  • Placement (22-22)
  • hasEffectTag (75-77)
  • Deletion (36-36)
  • UpdateEffect (28-28)
tests/fiber/fiberCreation.test.ts (3)
src/fiber/fiberCreation.ts (9)
  • createFiber (25-85)
  • createWorkInProgress (102-161)
  • createFiberFromElement (220-270)
  • createFiberFromText (278-291)
  • createFiberFromFragment (303-316)
  • cloneFiber (356-364)
  • getElementKey (386-404)
  • getElementProps (412-432)
  • getElementType (440-462)
src/fiber/fiberFlags.ts (3)
  • NoEffect (16-16)
  • NoLanes (43-43)
  • UpdateEffect (28-28)
src/hooks/types.ts (1)
  • StateOrEffectHook (62-69)
src/fiber/completeWork.ts (4)
src/fiber/types.ts (1)
  • Fiber (142-336)
src/fiber/fiberFlags.ts (2)
  • UpdateEffect (28-28)
  • Placement (22-22)
src/fiber/domOperations.ts (1)
  • setInitialProperties (51-72)
src/core/types.ts (1)
  • VDOMInstance (98-107)
tests/fiber/reconciliation.test.ts (4)
src/fiber/fiberCreation.ts (1)
  • createFiber (25-85)
src/fiber/reconcileChildren.ts (1)
  • reconcileChildren (83-102)
src/fiber/fiberFlags.ts (2)
  • Placement (22-22)
  • UpdateEffect (28-28)
src/fiber/types.ts (1)
  • Fiber (142-336)
src/fiber/types.ts (3)
src/MiniReact.ts (2)
  • AnyMiniReactElement (8-8)
  • ElementType (11-11)
src/fiber/fiberFlags.ts (1)
  • Update (30-30)
src/hooks/types.ts (1)
  • StateOrEffectHook (62-69)
src/hooks/fiberHooksImpl.ts (3)
src/hooks/types.ts (12)
  • UseStateHook (71-74)
  • StateHook (8-13)
  • StateOrEffectHook (62-69)
  • Reducer (86-86)
  • ReducerHook (30-36)
  • EffectCallback (77-77)
  • DependencyList (78-78)
  • EffectHook (15-22)
  • MutableRefObject (113-115)
  • RefHook (38-41)
  • MemoHook (43-48)
  • CallbackHook (50-56)
src/fiber/fiberHooks.ts (7)
  • getCurrentRenderingFiber (49-51)
  • createUpdateQueue (62-72)
  • processUpdateQueue (151-182)
  • dispatchSetState (110-140)
  • processReducerQueue (236-270)
  • dispatchReducerAction (195-225)
  • areDepsEqual (282-304)
src/fiber/types.ts (1)
  • UpdateQueue (80-117)
src/fiber/fiberRoot.ts (2)
src/fiber/types.ts (2)
  • FiberRoot (350-401)
  • Fiber (142-336)
src/fiber/fiberFlags.ts (1)
  • NoLanes (43-43)
src/fiber/beginWork.ts (4)
src/fiber/types.ts (1)
  • Fiber (142-336)
src/fiber/reconcileChildren.ts (1)
  • reconcileChildren (83-102)
src/fiber/fiberCreation.ts (1)
  • cloneChildFibers (174-210)
src/fiber/fiberHooks.ts (1)
  • setCurrentRenderingFiber (34-40)
tests/fiber/integration.test.ts (4)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/workLoop.ts (1)
  • scheduleUpdateOnFiber (39-46)
src/MiniReact.ts (3)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • AnyMiniReactElement (8-8)
src/fiber/types.ts (1)
  • Fiber (142-336)
src/fiber/fiberFlags.ts (2)
src/fiber/index.ts (18)
  • NoEffect (39-39)
  • EffectTag (12-12)
  • Placement (40-40)
  • UpdateEffect (41-41)
  • Update (19-19)
  • Deletion (42-42)
  • NoLanes (43-43)
  • Lanes (13-13)
  • SyncLane (44-44)
  • DefaultLane (45-45)
  • IdleLane (46-46)
  • isEffectTagMutation (47-47)
  • includesSomeLane (48-48)
  • mergeLanes (49-49)
  • removeLanes (50-50)
  • isSubsetOfLanes (51-51)
  • getHighestPriorityLane (52-52)
  • includesSyncLane (53-53)
src/fiber/types.ts (3)
  • EffectTag (35-35)
  • Update (55-70)
  • Lanes (42-42)
src/hooks/types.ts (1)
src/fiber/types.ts (1)
  • UpdateQueue (80-117)
src/core/index.ts (3)
src/fiber/types.ts (1)
  • FiberRoot (350-401)
src/fiber/fiberRoot.ts (3)
  • hasFiberRoot (140-142)
  • getFiberRoot (130-132)
  • createFiberRoot (23-59)
src/fiber/workLoop.ts (1)
  • scheduleUpdateOnFiber (39-46)
src/fiber/fiberCreation.ts (3)
src/MiniReact.ts (5)
  • ElementType (11-11)
  • AnyMiniReactElement (8-8)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/fiber/types.ts (2)
  • Props (23-28)
  • Fiber (142-336)
src/fiber/fiberFlags.ts (2)
  • NoEffect (16-16)
  • NoLanes (43-43)
🪛 ast-grep (0.39.7)
src/fiber/fiberRoot.ts

[warning] 195-195: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ""
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 LanguageTool
docs/02-work-loop.md

[style] ~355-~355: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...(does workInProgress become null)? 3. Is the effect list being built correctly? ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~356-~356: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...e effect list being built correctly? 4. Is commit running after render completes? ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~377-~377: ‘overall structure’ might be wordy. Consider a shorter alternative.
Context: ...ures later without major changes to the overall structure.

(EN_WORDINESS_PREMIUM_OVERALL_STRUCTURE)

docs/05-hooks-system.md

[style] ~164-~164: Consider a more concise word here.
Context: ...ng = null } ``` We process all updates in order to compute the new state. ## useEffect S...

(IN_ORDER_TO_PREMIUM)

docs/03-reconciliation.md

[style] ~228-~228: Consider using “who” when you are referring to people instead of objects.
Context: ...as PLACEMENT) - Delete any old children that don't match ## Array Reconciliation T...

(THAT_WHO)


[style] ~445-~445: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ..., we can reuse the fiber. Otherwise, we need to create a new one. ## Deletion When a ...

(REP_NEED_TO_VB)

docs/README.md

[style] ~177-~177: This wording could be more concise.
Context: ... performance: O(n) Reconciliation: Linear in number of children Effect Lists: Only pro...

(ADJECTIVE_IN_ATTRIBUTE)

🪛 markdownlint-cli2 (0.18.1)
docs/README.md

7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


12-12: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


21-21: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


64-64: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


81-81: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


84-84: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


91-91: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


97-97: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


104-104: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


107-107: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🔇 Additional comments (32)
package.json (1)

45-46: YOOO, this is CLEAN! Past review concern properly addressed.

The typecheck script is now integrated into prepublishOnly exactly as requested. No more vibes-based type safety - this will catch type errors before publish. Beautiful follow-through on the feedback.

src/events/eventSystem.ts (1)

672-698: Portal cleanup implementation is PERFECT. This is how you prevent memory leaks.

The symmetry here is beautiful - you add portal containers with addEventDelegation, you clean them up in cleanup. The loop at lines 684-690 removes all delegated listeners from each portal container, and line 694 clears the set.

This is exactly what you want to see. No orphaned listeners, no memory leaks, proper resource management. Chef's kiss.

docs/05-hooks-system.md (1)

1-649: This hooks documentation is INSANE. Like, actually incredible.

The depth here is perfect for an educational project. You're not just explaining WHAT hooks do, you're showing HOW they work internally - the hook array, the cursor, the update queues, the circular linked lists. This is the content that makes people understand React, not just use it.

The code examples are clean, the explanations are thorough, and you even covered the gotchas (stale closures, missing deps, wrong number of hooks). The Rules of Hooks section explains WHY the rules exist, not just WHAT they are.

Only thing I'd verify: make sure these code examples actually match your implementation. If someone's reading this to understand your codebase and the implementation differs, that's gonna be confusing.

example/README.md (2)

1-179: Example README is CLEAN. This is how you showcase a library.

The transformation from basic example to comprehensive showcase is perfect. You're demonstrating:

  • All the hooks
  • Portals with proper event bubbling
  • Context API
  • Type safety throughout

The Bun + ElysiaJS stack is modern and fast. Good choice. The README explains what each demo does and why it matters. The Type Safety section showing how to properly type events and refs? Chef's kiss.

Only other nitpick: The Browser Support section (lines 169-176) listing specific version numbers might get stale. Maybe just say "Modern browsers with ES2020+ support" and link to caniuse or something.


70-77: Claim is verified as accurate - nice TypeScript discipline here.

The verification confirms "No any types used" is legitimately true across both example/src/app.tsx and example/src/server.ts. Every hook has explicit generics (useState<number>, useRef<HTMLInputElement | null>), interfaces are properly defined (ThemeContextType, Todo, ExpensiveComponentProps), and contexts are typed at the point of creation.

The @ts-expect-error comments you've got are the right way to handle MiniReact/React type incompatibilities—they're not any escape hatches, they're deliberate suppressions with context. That's the type-safe way to do it.

No action needed. This README statement holds up.

docs/03-reconciliation.md (1)

1-550: This reconciliation doc is ABSOLUTELY CRACKED.

The deep dive into the O(n) algorithm is exactly what people need to understand React's performance story. You explained:

  • Why it's O(n) not O(n³)
  • The lastPlacedIndex trick for movement detection (lines 382-418)
  • The keyed vs unkeyed mixing problem (lines 332-351)
  • Type matching logic
  • The mount vs update split optimization

The code examples are detailed enough to be useful but not overwhelming. The diagrams at line 407-416 showing how lastPlacedIndex works? Perfect.

The "Debugging Tips" section at line 508-525 is clutch - this is what people actually need when reconciliation goes wrong.

This is the kind of documentation that makes a project legendary. It's not just "here's how to use it" - it's "here's how it ACTUALLY works."

example/tsconfig.json (1)

3-23: YOOO you turned on strict mode! This is the way.

Line 12: "strict": true - this is BASED. Too many projects skip this and then wonder why their types are lying to them. Strict mode is the only way to do TypeScript properly.

The other changes are solid too:

  • ESNext target/lib: You're using Bun, might as well use modern features
  • moduleResolution: "bundler": Perfect for Bun-based workflows
  • allowImportingTsExtensions: Necessary with bundler resolution
  • Path mappings (lines 19-23): Clean way to dev against local source

The jsxImportSource change from "mini-react" to "@marcelolsen/mini-react" aligns with the published package name.

This config is tight. No notes.

docs/01-fiber-architecture.md (1)

1-254: This fiber architecture doc is the PERFECT introduction. This is how you onboard people.

You're starting with the fundamentals - what is a Fiber, why do we have two trees, what are the phases - and building up from there. The progression is logical and clear.

Highlights:

  • The Fiber interface at lines 12-43 with inline comments is great reference material
  • The ASCII diagram at lines 56-69 showing the double-buffer pattern? Clean
  • The explanation of why we have two trees (lines 71-76): error recovery, concurrent mode ready, efficient updates - these are the RIGHT reasons
  • The tree traversal code at lines 124-158 showing how to do depth-first without recursion is GOLD

The "Priority and Scheduling" section at lines 213-228 is good forward-looking content - shows the architecture supports features you haven't built yet.

This doc sets up the rest of the documentation perfectly. Someone reading this first will understand the "why" behind the implementation details in the other docs.

src/fiber/types.ts (6)

51-70: LGTM on the Update queue structure!

Nice circular linked list setup here. The action: State | ((prev: State) => State) is clean - you're handling both direct state updates and updater functions. The per-update lane priority is chef's kiss for concurrent mode later.

One tiny thing though: there's no runtime validation that next actually forms a valid circular list. If you accidentally break the circle or create an infinite non-circular chain, you're gonna have a bad time debugging it. Might want to add a dev-mode validator.


72-117: UpdateQueue is CLEAN architecture!

Okay so you've got the pending circular list for incoming updates, AND you've got the base queue for skipped low-priority updates. This is the real deal concurrent mode stuff. The separation between lastRenderedState (optimization) and baseState (correctness) shows you understand the priority system implications.

The lastRenderedReducer signature being (state: State, action: State | ((prev: State) => State)) => State is perfect - works for both useState (identity reducer) and useReducer (custom reducer).


142-336: This Fiber interface is THICC but well-organized.

40+ fields in one interface? That's a lot, my guy. But I'll give you credit - the organization with comments (// ===== IDENTITY =====, etc.) makes it navigable. The ASCII tree diagram at line 125-136 is chef's kiss - more code should have diagrams like this.

Few observations:

  1. memoizedState: unknown at line 238 - you've completely given up type safety here. I get it, different component types have different state shapes, but unknown means zero compile-time help. Consider a union type or generics if possible.

  2. hooks: StateOrEffectHook<unknown>[] | null at line 282 - same issue. That <unknown> is doing a lot of heavy lifting (or rather, NO lifting).

  3. The stateNode: Node | FiberRoot | PortalContainer | null union at line 275 - this is actually good! Shows proper type discrimination.

But here's the thing - this is a LOT of state in one place. Have you considered if any of these fields could be optional or moved to sub-structures? Like error boundary stuff, suspense stuff, concurrent mode stuff could be their own objects?

The size is manageable for now but keep an eye on it as you add more features. Also verify the unknown types don't bite you later.


350-401: FiberRoot is solid but has some sus unknowns.

Line 380: callbackNode: unknown - okay so what IS this actually? A timeout ID? A promise? A scheduler task? The lack of type information here is rough. At least add a comment explaining what goes in here.

Lines 390-395: eventTimes: number[] and expirationTimes: number[] - these arrays are gonna grow over time. Do you have a strategy for cleaning them up? Or do they just accumulate forever until the component unmounts? Might want to document the lifecycle of these arrays.

Line 400: pendingPassiveEffects?: Fiber[] - this is optional but the UpdateQueue stuff wasn't. Is there a reason for the inconsistency? Are some roots not gonna have passive effects?

Otherwise the double-buffering with current and finishedWork is exactly right. That's the React pattern and you're following it well.


488-507: isSameType is critical for reconciliation and it's solid.

This is the heart of whether you can reuse a fiber or need to create a new one. Type AND key must match (or both be null). The strict equality checks are correct here.

One edge case though: line 497 if (fiber1.type !== fiber2.type) works fine for strings and symbols, but for function components, you're comparing function references. If someone recreates the component function (like in hot reloading), this will fail to match even though it's "the same" component conceptually. Not much you can do about this without a more sophisticated system though.


522-524: fiberNeedsRef is clean.

Only host components (DOM elements) get refs attached. Function components can't have refs unless you do forwardRef shenanigans. This is correct.

src/fiber/fiberFlags.ts (1)

10-36: Bitwise flags are CLEAN!

Binary literals (0b0001, 0b0010, etc.) are chef's kiss for readability. Much better than hex or decimal magic numbers. Each flag is a power of 2 so you can OR them together.

Line 30: export const Update = UpdateEffect; with comment "Keep old name for backwards compatibility temporarily" - okay so TEMPORARILY for how long? Is there a TODO to remove this? Is there a migration plan? Or is "temporarily" just gonna become "forever" like it does in every codebase?

Either commit to keeping both exports long-term, or create a migration plan to remove Update. Don't let temporary aliases become permanent cruft.

src/hooks/types.ts (2)

8-13: StateHook queue integration is solid!

Adding queue?: UpdateQueue<T> to StateHook is the right way to bridge hooks with the fiber update system. The optional nature means old code still works while fiber-based code can use the queue.

The comment "Update queue for Fiber integration" is helpful. Same pattern repeated for ReducerHook at line 35.

One question though: what happens if a hook has BOTH a direct setState function AND a queue? Can they get out of sync? Or does one always delegate to the other?


15-22: needsRun flag makes sense for commit-phase effects.

Adding needsRun?: boolean to EffectHook is clean. During render you mark which effects need to run, then in commit phase you actually run them. This separation is exactly right for the fiber architecture.

tests/fiber/workLoop.test.ts (5)

141-155: Deep nesting test is CRITICAL and you nailed it!

Testing 100 levels of nesting (line 147-149) is exactly what separates iterative fiber from recursive rendering. With recursion, you'd blow the stack. With fibers, this should work fine.

This test proves your architecture works at scale. Love it.


284-312: Effect list order test is good defensive programming.

Walking the nextEffect linked list to count effects (lines 303-308) validates that the effect list is built correctly. This is important because effect list bugs would cause commits to miss updates or crash.

The test expects effectCount > 0 which is a bit weak - you could assert the exact count is 3 (for the 3 divs at lines 291-293). But I get it, implementation details might vary.


360-381: WIP reuse test simulates the full cycle - nice!

Lines 364-376 simulate a full render cycle:

  1. First render
  2. Commit (line 371: root.current = firstWIP)
  3. Second render
  4. Validate reuse

This tests the heart of fiber architecture - reusing fibers via the alternate pointer. The double-buffering is what makes fiber performant.


411-434: Error handling tests are ESSENTIAL!

Line 416: orphaned fiber (no path to root) should throw - correct!
Line 433: component error propagates - also correct!

These tests validate that broken states fail fast rather than silently corrupting the tree. That's defensive programming at its finest.

The comment at line 432 "error boundaries in Phase 7 will catch this" is a good roadmap hint.


513-569: Performance tests got FIXED - thank you!

Previous review complained about wall-clock timing assertions (< 100ms, etc) being flaky. You fixed it! Now you're validating structure:

  • Line 532-538: count siblings, expect 100
  • Line 560-567: count depth, expect 51

This is MUCH better. Structural validation is deterministic and doesn't flake on slow CI. The tests still exercise performance scenarios (large trees) but validate correctness instead of timing.

This is the right way to do it!

example/src/templates/index.html (1)

1-191: HTML template is clean for an example.

The embedded CSS (lines 7-184) is fine for a showcase/example app. It's self-contained, which is good for demos. If this were production, I'd want external stylesheets, but for an example it's totally acceptable.

Nice touches:

  • Gradient background (line 16)
  • Clean card design (lines 26-32)
  • Smooth button transitions (lines 55-68)
  • Portal-specific positioning (lines 133-143)

The two mount points (#root and #portal-root) are clear and well-named.

Only tiny nitpick: the viewport meta tag (line 5) could include user-scalable=no for a more app-like feel, but that's purely aesthetic preference.

tests/fiber/reconciliation.test.ts (5)

23-54: Single child reconciliation tests hit the fundamentals!

Mount case (lines 23-34): new fiber gets PLACEMENT tag - correct!

Update case (lines 36-54): same type and key means fiber reuse via alternate - CORRECT! This is the optimization that makes React fast. Line 53 validates parent.child?.alternate === currentChild which proves the fiber was reused, not recreated.


159-186: Reordering test is THE money test for reconciliation!

This test (lines 159-186) is crucial. It validates that when you have:

  • Old order: a, b, c
  • New order: c, a, b

The reconciler:

  1. Finds all three fibers by key
  2. Reuses them (UPDATE not PLACEMENT)
  3. Reorders them correctly

This is what separates O(n) reconciliation from O(n²) or O(n³). If you were recreating fibers instead of reusing them, keys would be pointless.

Lines 179-181 validate the new order is correct.
Lines 184-185 validate all fibers were reused (UPDATE effect).

Perfect test!


351-362: Duplicate key test is interesting!

Line 351-362: multiple children with the same key. This is a user error but the system shouldn't crash. The test just validates that both children are created (line 361).

React would warn about this in dev mode. You might want to consider adding a dev-mode warning when duplicate keys are detected. But for now, handling it gracefully is good enough.


477-499: Large list test validates O(n) algorithm!

Creating and reconciling 1000 children (lines 477-499) is important for proving your algorithm scales linearly. If reconciliation were O(n²), this test would be noticeably slow or timeout.

The test counts children in a while loop (lines 492-497) to validate all 1000 were created. This is deterministic and much better than timing assertions.

Previous review complaints about timing were fixed here too!


570-603: Complex reordering test (reverse 10 items) is SPICY!

Lines 570-603: create 10 fibers in order 0-9, then reverse to 9-0.

This tests the worst-case reordering scenario. Every fiber moved to the opposite end. If your algorithm is naive, this could cause O(n²) behavior or create unnecessary deletions/placements.

Lines 597-602 validate that ALL fibers were reused (UPDATE effect), not recreated. This proves the key-based diffing works even in the worst case.

Excellent test!

docs/02-work-loop.md (4)

1-141: Documentation is SOLID - this is how you doc architecture!

The conversational tone works really well here. Line 5: "Think of it as the main game loop in a video game" - perfect analogy! Makes the concept immediately graspable.

The code examples are helpful and appear to match the actual implementation. The progression from entry point (scheduleUpdateOnFiber) to root discovery to work loop is logical and easy to follow.

Line 81: explaining "sync" means synchronous without yielding, and hinting at future "performConcurrentWorkOnRoot" - good foreshadowing!


182-231: completeUnitOfWork explanation is MONEY!

The explanation of effect list building (lines 197-218) is crucial. This is the optimization that makes commit phase O(changes) instead of O(tree-size).

As you bubble up, you append child effects to parent effects, building a linked list. By the time you reach root, you have a complete list of everything that changed.

This is one of the key insights of the fiber architecture and you explained it clearly.


242-263: Tree traversal diagram is CLUTCH!

        A
       / \
      B   C
     / \
    D   E

Order:
1. beginWork(A) -> returns B
2. beginWork(B) -> returns D
...

This ASCII diagram makes the depth-first traversal concrete. Visual learners (like me) appreciate this. More code should have diagrams!


321-377: Wrapping up the doc with practical considerations - nice!

The "Memory and Performance" section (lines 321-334) hits the key points:

  • No recursion = no stack overflow
  • Minimal allocations via alternate pointer reuse
  • Effect list optimization
  • Early bailout opportunities

The "Integration Points" (lines 335-348) show how work loop coordinates with the rest of the system. This context is valuable for understanding the bigger picture.

Debugging tips (lines 349-366) are actually useful! Most docs skip the "how to debug when it breaks" part.

Overall this is high-quality documentation. Clear, comprehensive, with good examples and diagrams.

Comment on lines +45 to +66
if (typeof type === "string") {
// Check if it's TEXT_ELEMENT (also a string)
if (type === TEXT_ELEMENT) {
completeHostText(current, workInProgress);
return;
}
// Regular host component (div, span, etc)
completeHostComponent(current, workInProgress);
} else if (typeof type === "symbol") {
// Symbol types: FRAGMENT or PORTAL
if (type === FRAGMENT) {
completeFragment(current, workInProgress);
} else if (type === PORTAL) {
completePortal(current, workInProgress);
}
} else if (typeof type === "function") {
// Functional component (no DOM node)
completeFunctionComponent(current, workInProgress);
} else if (type === null) {
// Root fiber (no DOM node)
completeHostRoot(current, workInProgress);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

TEXT fibers are straight up getting skipped

Yo, this dispatch is throwing TEXT_ELEMENT under the bus. TEXT_ELEMENT is a Symbol.for("mini.react.text") in src/core/types, which means every text fiber shows up here with typeof type === "symbol". Because we only branch on "string" first and we don’t have a symbol-path case for TEXT_ELEMENT, completeHostText never runs. We bail out before building the text node, so a simple render(<div>hi</div>) renders <div></div> — zero text, zero chill. It gets worse: even if you patched the dispatch, appendAllChildren only appends string-typed fibers, so the text node would still never hit the DOM.

Fix: short-circuit on type === TEXT_ELEMENT before the string branch and let appendAllChildren append TEXT_ELEMENT fibers as well.

@@
-export function completeWork(
-	current: Fiber | null,
-	workInProgress: Fiber,
-): void {
-	const { type } = workInProgress;
-
-	if (typeof type === "string") {
-		// Check if it's TEXT_ELEMENT (also a string)
-		if (type === TEXT_ELEMENT) {
-			completeHostText(current, workInProgress);
-			return;
-		}
+export function completeWork(
+	current: Fiber | null,
+	workInProgress: Fiber,
+): void {
+	const { type } = workInProgress;
+
+	if (type === TEXT_ELEMENT) {
+		completeHostText(current, workInProgress);
+		return;
+	}
+
+	if (typeof type === "string") {
 		// Regular host component (div, span, etc)
 		completeHostComponent(current, workInProgress);
@@
-		if (node.stateNode !== null && typeof node.type === "string") {
-			// Host component or TEXT_ELEMENT - append its DOM node
+		if (
+			node.stateNode !== null &&
+			(typeof node.type === "string" || node.type === TEXT_ELEMENT)
+		) {
+			// Host component or text fiber - append its DOM node
 			parent.appendChild(node.stateNode as Node);

Without this, every keyed reorder involving text, every text update, literally all text rendering is broken. Gotta land this fix before shipping.

Also applies to: 352-387

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 29

♻️ Duplicate comments (1)
tests/fiber/completeWork.test.ts (1)

98-118: This test is validating implementation details, not behavior.

Lines 98-118: You're checking that parentDOM.children[0] === childDOM. This is testing that completeWork physically appends the DOM node, which is an implementation detail.

What if tomorrow you decide to use DocumentFragment for batching, or implement a different append strategy? This test breaks even though the BEHAVIOR (child is under parent in DOM) is the same.

Test the behavior, not the implementation:

-const parentDOM = parent.stateNode as HTMLElement;
-const childDOM = child.stateNode as HTMLElement;
-
-expect(parentDOM.children.length).toBe(1);
-expect(parentDOM.children[0]).toBe(childDOM);
+const parentDOM = parent.stateNode as HTMLElement;
+const childDOM = child.stateNode as HTMLElement;
+
+expect(parentDOM.children.length).toBe(1);
+expect(parentDOM.contains(childDOM)).toBe(true);
+expect(childDOM.parentElement).toBe(parentDOM);

contains() and parentElement check the DOM relationship without assuming HOW it was established. More resilient to refactoring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between adc490d and 8d98c27.

📒 Files selected for processing (13)
  • docs/README.md (1 hunks)
  • example/src/server.ts (1 hunks)
  • src/fiber/beginWork.ts (1 hunks)
  • src/fiber/fiberCreation.ts (1 hunks)
  • src/fiber/fiberFlags.ts (1 hunks)
  • src/fiber/fiberRoot.ts (1 hunks)
  • src/fiber/index.ts (1 hunks)
  • src/fiber/reconcileChildren.ts (1 hunks)
  • src/fiber/types.ts (1 hunks)
  • tests/MiniReact.falsyValues.test.ts (1 hunks)
  • tests/fiber/beginWork.test.ts (1 hunks)
  • tests/fiber/completeWork.test.ts (1 hunks)
  • tests/fiber/workLoop.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: MarcelOlsen
Repo: MarcelOlsen/mini-react PR: 8
File: README.md:701-708
Timestamp: 2025-06-17T12:40:39.737Z
Learning: MarcelOlsen prefers to keep test documentation simple with just one example of running specific test files, rather than listing detailed commands for each test suite in the README.
🧬 Code graph analysis (9)
tests/fiber/workLoop.test.ts (4)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/fiberCreation.ts (1)
  • createFiber (25-88)
src/fiber/workLoop.ts (2)
  • scheduleUpdateOnFiber (39-46)
  • getCurrentFiber (282-284)
src/MiniReact.ts (3)
  • AnyMiniReactElement (8-8)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/fiber/beginWork.ts (5)
src/fiber/types.ts (1)
  • Fiber (164-364)
src/fiber/reconcileChildren.ts (1)
  • reconcileChildren (83-102)
src/fiber/fiberCreation.ts (1)
  • cloneChildFibers (177-213)
src/fiber/fiberHooks.ts (1)
  • setCurrentRenderingFiber (34-40)
src/fiber/fiberFlags.ts (1)
  • Deletion (36-36)
tests/fiber/beginWork.test.ts (3)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-88)
  • createWorkInProgress (105-164)
src/fiber/beginWork.ts (1)
  • beginWork (45-82)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
tests/fiber/completeWork.test.ts (6)
src/fiber/fiberCreation.ts (2)
  • createFiber (25-88)
  • createWorkInProgress (105-164)
src/fiber/completeWork.ts (1)
  • completeWork (39-67)
src/fiber/fiberFlags.ts (2)
  • Placement (22-22)
  • UpdateEffect (28-28)
src/fiber/fiberRoot.ts (1)
  • createFiberRoot (23-59)
src/fiber/beginWork.ts (1)
  • beginWork (45-82)
src/fiber/types.ts (1)
  • Fiber (164-364)
src/fiber/reconcileChildren.ts (4)
src/MiniReact.ts (2)
  • AnyMiniReactElement (8-8)
  • TEXT_ELEMENT (133-133)
src/fiber/types.ts (2)
  • Fiber (164-364)
  • isSameElementType (544-549)
src/fiber/fiberCreation.ts (4)
  • createFiberFromElement (223-273)
  • getElementKey (389-407)
  • getElementType (443-465)
  • createWorkInProgress (105-164)
src/fiber/fiberFlags.ts (4)
  • Placement (22-22)
  • hasEffectTag (95-97)
  • Deletion (36-36)
  • UpdateEffect (28-28)
src/fiber/types.ts (2)
src/MiniReact.ts (5)
  • AnyMiniReactElement (8-8)
  • ElementType (11-11)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/hooks/types.ts (1)
  • StateOrEffectHook (62-69)
src/fiber/fiberRoot.ts (2)
src/fiber/types.ts (3)
  • FiberRoot (378-429)
  • Fiber (164-364)
  • FIBER_BRAND (18-18)
src/fiber/fiberFlags.ts (1)
  • NoLanes (45-45)
src/fiber/fiberCreation.ts (3)
src/MiniReact.ts (4)
  • ElementType (11-11)
  • TEXT_ELEMENT (133-133)
  • FRAGMENT (133-133)
  • PORTAL (133-133)
src/fiber/types.ts (3)
  • Props (50-50)
  • Fiber (164-364)
  • FIBER_BRAND (18-18)
src/fiber/fiberFlags.ts (2)
  • NoEffect (16-16)
  • NoLanes (45-45)
src/fiber/fiberFlags.ts (1)
src/fiber/types.ts (3)
  • EffectTag (57-57)
  • Update (77-92)
  • Lanes (64-64)
🪛 ast-grep (0.39.7)
src/fiber/fiberRoot.ts

[warning] 198-198: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ""
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 LanguageTool
docs/README.md

[style] ~182-~182: This wording could be more concise.
Context: ... performance: O(n) Reconciliation: Linear in number of children Effect Lists: Only pro...

(ADJECTIVE_IN_ATTRIBUTE)

🔇 Additional comments (7)
docs/README.md (3)

17-19: All referenced docs exist—no issues here.

The verification confirms all five documentation files are present in the docs directory. Every link in the README.md is pointing to an actual file, so there's nothing to fix. The documentation structure is solid. Ship it.


238-240: Minor punctuation: Sentence fragmentation on line 240.

Line 240 reads: "...the debugger. Set a breakpoint..." — you've got a period between two closely related independent clauses. Use a semicolon or split it:

-The best way to understand this code is to step through it with a debugger. Set a breakpoint in `performUnitOfWork` and watch the tree get built.
+The best way to understand this code is to step through it with a debugger; set a breakpoint in `performUnitOfWork` and watch the tree get built.

Or keep it as two sentences but make the second one less command-y. Super minor, but it reads cleaner with a semicolon.

Likely an incorrect or invalid review comment.


212-216: Testing section assumes Bun but PR context suggests flexibility.

Line 212–216 states "Run tests with: bun test". The PR objectives mention this is an ElysiaJS + Bun example, but if the project supports multiple runtimes (npm, pnpm, etc.), consider adding alternatives:

 Run tests with:
 
 \`\`\`bash
+npm test
+# or with Bun:
 bun test
 \`\`\`

This makes it clear which is the "normal" path vs. which is Bun-specific, if that's the case.

⛔ Skipped due to learnings
Learnt from: MarcelOlsen
Repo: MarcelOlsen/mini-react PR: 8
File: README.md:701-708
Timestamp: 2025-06-17T12:40:39.737Z
Learning: MarcelOlsen prefers to keep test documentation simple with just one example of running specific test files, rather than listing detailed commands for each test suite in the README.
src/fiber/fiberCreation.ts (2)

177-213: cloneChildFibers is solid, but the comment is MORE important than the code.

Yo, the comment at lines 168-172 is CRUCIAL:

"This is critical for memo bailout to maintain the double-buffering invariant. If we only clone the first child, sibling pointers still point to the current tree, which causes the work loop to mutate the live tree instead of the WIP tree."

THIS is the bug that took React like 3 years to fully iron out. I'm glad you documented it, but this deserves even more emphasis. This function is the ONLY thing preventing silent tree corruption during bailouts.

The implementation is correct (cloning entire sibling chain, setting return pointers, null-terminating). But add a stronger warning:

 /**
  * Clone the entire child list (all siblings) into the work-in-progress tree
  *
+ * ⚠️ CRITICAL: This function MUST clone ALL siblings, not just the first child.
+ * Failing to clone the complete sibling chain will cause the work loop to mutate
+ * the committed tree during traversal, corrupting the double-buffer invariant.
+ * This is a common source of subtle bugs in fiber reconcilers.
+ *
  * This is critical for memo bailout to maintain the double-buffering invariant.

Comments that prevent architectural disasters deserve the shouting treatment, chat.


105-164: The WIP reuse pattern is properly secured—no tree corruption risk here.

The code already implements the exact pattern React uses. After searching through all bailout paths:

  1. Memo bailout (line 133 of beginWork.ts) — ✓ Calls cloneChildFibers(current, workInProgress) correctly
  2. Error handling — Errors are caught and rethrown (workLoop.ts:185-188, 228-231), causing the entire WIP tree to be discarded, which prevents any inconsistent state
  3. Normal reconciliation — All other paths properly call reconcileChildren, which handles the WIP state through createWorkInProgress

The reconcileSingleElement function (line 216 in reconcileChildren.ts) is the only place where createWorkInProgress gets invoked during diffing, and it's followed by proper setup. When beginWork processes the returned fiber, if the memo bailout fires, cloneChildFibers is already there waiting. If an error occurs, the WIP tree gets thrown away. There's no window where you've got a half-baked WIP tree pointing into the current tree.

This is solid. The double-buffering invariant is maintained.

src/fiber/beginWork.ts (1)

238-262: The suggested container comparison approach doesn't match the code architecture.

Here's what I found, and honestly, this is a classic case of "right problem, completely wrong solution":

  1. The identity comparison IS there (oldContainer !== newContainer), comparing HTMLElement objects directly - that part the reviewer nailed.

  2. BUT — the suggested fix about comparing .containerInfo is dead wrong. Look at the code flow:

    • beginWork.ts works with raw HTMLElement references from pendingProps.targetContainer
    • Those elements don't have a .containerInfo property
    • .containerInfo only exists in stateNode (the wrapper object created in completeWork.ts)
    • They're in different contexts entirely — this is architectural confusion
  3. In practice, this isn't actually a problem:

    • Tests show usage patterns where document.createElement() or document.getElementById() are passed
    • Both return stable references (same object = same DOM node)
    • The identity comparison works fine for real-world usage
  4. The real architectural question (which the reviewer should have asked):

    • Should portal container changes use strict identity or compare actual DOM node values?
    • If we want to be pedantic about it: compare oldContainer === newContainer directly (not object identity) or normalize references upfront

The review comment conflates two different architectural layers. The concern about expensive remounts is theoretically valid but the diagnosis is wrong.

Likely an incorrect or invalid review comment.

tests/fiber/beginWork.test.ts (1)

654-673: This review comment is based on incorrect assumptions about the implementation.

The normalizeChildren function already handles all the edge cases you're concerned about:

  • Nested arrays: Handled via explicit recursive call if (Array.isArray(child)) { result.push(...normalizeChildren(child)); continue; }
  • Empty arrays: Correctly returns [] after loop iteration yields nothing
  • Nested nulls: Recursion combined with upfront filtering if (child === null || child === undefined || typeof child === "boolean") { continue; } catches them at every level
  • Fragments: Handled at the fiber creation level (not in normalizeChildren), where createFiberFromFragment unwraps Fragment elements

The implementation is architecturally solid. The test coverage at lines 654-673 is simplistic but doesn't indicate bugs—the actual reconciliation logic is correct.

If you want to strengthen test confidence, yeah, add explicit cases for [[], span], [[null, span]], etc. But that's test comprehensiveness, not a reconciliation bug.

Likely an incorrect or invalid review comment.


MiniReact is a simplified React clone built with a modern fiber architecture. It's not trying to be a production framework - it's an educational project that demonstrates how React works under the hood.

The codebase is fully functional with 539 passing tests covering all major features. It's a great way to understand React's internals without getting lost in production complexity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Test count is inconsistent with PR objectives.

Line 9 claims 539 passing tests, but the PR objectives state 484 passing tests reported. Which is the source of truth here? This mismatch kills credibility in the docs — docs need to match reality.

Before we ship this, we need to nail down the actual test count:

Once you run this, update line 9 with the actual count. Use the real number — no cap.


🏁 Script executed:

#!/bin/bash
# Find the actual test count by checking test files and running them
cd "$(git rev-parse --show-toplevel)"

# Count test files
echo "=== Test Files ==="
fd -e test.ts -e test.tsx -e spec.ts | wc -l

# Try to find test count from package.json or test output
echo "=== Checking package.json for test config ==="
jq '.scripts.test' package.json 2>/dev/null || echo "No test script found"

# Look for any CI config that might report test count
echo "=== GitHub Actions workflow ==="
find .github/workflows -name "*.yml" -o -name "*.yaml" | head -3

Length of output: 165


Alright, we found 27 test files, and the test runner is bun test. But here's the thing — finding 27 files doesn't tell us jack about how many tests actually live in those files. One file could have 2 tests, another could have 50. This is sloppy.

We need to check the CI workflow and see what the actual test count is. Let me dig into that workflow file and also poke around for any test output or metadata.


🏁 Script executed:

#!/bin/bash
cd "$(git rev-parse --show-toplevel)"

echo "=== CI Workflow File ==="
cat .github/workflows/ci.yml | head -60

echo -e "\n=== Looking for test report/output files ==="
find . -name "*test*report*" -o -name "*coverage*" -o -name ".test-results*" 2>/dev/null | head -10

echo -e "\n=== Check package.json for test metadata ==="
cat package.json | jq '.devDependencies, .scripts' 2>/dev/null

echo -e "\n=== Sample test file to understand structure ==="
fd -e test.ts -e test.tsx 2>/dev/null | head -1 | xargs head -30

Length of output: 2966


Okay, so the CI doesn't actually report test counts — it just yells into the void with bun test. Classic. No metadata, no JSON reports. We gotta count this ourselves.

Let me search for where these magic numbers 539 and 484 are actually coming from, and then count the actual test cases.


🏁 Script executed:

#!/bin/bash
cd "$(git rev-parse --show-toplevel)"

echo "=== Search for 539 in repo ==="
rg "539" --type-list | head -20

echo -e "\n=== Search for 484 in repo ==="
rg "484" --type-list | head -20

echo -e "\n=== Count all test() declarations ==="
rg -c 'test\s*\(' --type ts | awk -F: '{sum += $2} END {print "Total test() calls: " sum}'

echo -e "\n=== Count all describe() declarations ==="
rg -c 'describe\s*\(' --type ts | awk -F: '{sum += $2} END {print "Total describe() calls: " sum}'

echo -e "\n=== Count expect() calls (assertions) ==="
rg -c 'expect\s*\(' --type ts | awk -F: '{sum += $2} END {print "Total expect() calls: " sum}'

echo -e "\n=== Check README.md for where 539 is mentioned ==="
rg "539|484" docs/README.md -C 2

Length of output: 1610


Update the test count in docs — it's completely wrong.

Bro, the README is straight-up lying. You're claiming 539 tests but there are only 525 test() declarations in the codebase. That's a 14-test gap. This isn't some philosophical debate — we can literally count them.

The docs say it twice:

  • Line 9: "539 passing tests"
  • Later section: "The test suite has 539 passing tests"

Both need to say 525, not 539. You can't just make up numbers here — docs have to match reality or nobody trusts anything else you write. This is architecture credibility 101.

Fix both instances in docs/README.md and use the real number. Done.

🤖 Prompt for AI Agents
In docs/README.md around line 9 (and the later duplicate mention), the README
incorrectly states "539 passing tests" — update the text at line 9 and the other
section where it repeats to read "525 passing tests" instead; perform the
replacement for both occurrences in README.md and verify no other "539"
instances remain in that file.

Comment on lines +113 to +134
### Code Organization

```text
src/
├── fiber/
│ ├── workLoop.ts # Main rendering loop
│ ├── beginWork.ts # Process each fiber
│ ├── completeWork.ts # Finalize each fiber
│ ├── commitWork.ts # Apply DOM changes
│ ├── reconcileChildren.ts # Diffing algorithm
│ ├── fiberHooks.ts # Hook implementations
│ └── types.ts # Fiber type definitions
├── events/
│ ├── eventSystem.ts # Event delegation
│ └── types.ts # Event type definitions
├── hooks/
│ └── fiberHooksImpl.ts # Public hook API
├── context/
│ └── index.ts # Context implementation
└── portals/
└── index.ts # Portal implementation
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Code organization tree is helpful but verify paths match reality.

The code organization section lays out a clean tree structure. Make sure this matches what's actually in src/fiber/ and src/ at the time this PR lands.

If the actual structure differs from what's documented (lines 116–134), update the tree to match reality. Docs lying about file structure is super confusing for new contributors.


🏁 Script executed:

#!/bin/bash
# Verify code organization matches the documented structure
echo "=== Actual file structure in src/ ==="
find src -type f -name "*.ts" -o -name "*.tsx" | sort

Length of output: 823


Update the README file tree—it's missing entire directories and ~35 files.

The documented structure is severely out of sync with reality. The docs only show 7 files in fiber/ when there are 12. Missing entire top-level directories like core/, dom-renderer/, fragments/, jsx-runtime/, performance/, and reconciler/. Also missing index.ts and types.ts files in several documented modules.

This creates friction for new contributors trying to navigate the codebase. The tree needs to match what's actually there. Here's what's missing:

  • Root level: MiniReact.ts, jsx-dev-runtime.ts, jsx-runtime.ts, jsx.d.ts
  • New directories: core/, dom-renderer/, fragments/, jsx-runtime/, performance/, reconciler/
  • Fiber directory: domOperations.ts, fiberCreation.ts, fiberFlags.ts, fiberRoot.ts, index.ts
  • Partial updates needed: events/ (add index.ts), context/ (add types.ts), portals/ (add types.ts), hooks/ (add index.ts, types.ts)

Update lines 116–134 to reflect the actual structure or document why these files/directories are intentionally omitted from the user-facing guide.

🤖 Prompt for AI Agents
In docs/README.md around lines 113 to 134, the displayed project tree is
outdated and missing many top-level files, new directories, and several files
inside fiber/events/context/portals/hooks; update the README tree to exactly
reflect the repository layout by adding the root files (MiniReact.ts,
jsx-dev-runtime.ts, jsx-runtime.ts, jsx.d.ts), the new directories (core,
dom-renderer, fragments, jsx-runtime, performance, reconciler), the missing
fiber files (domOperations.ts, fiberCreation.ts, fiberFlags.ts, fiberRoot.ts,
index.ts), add index.ts to events, add types.ts to context and portals, and add
index.ts and types.ts to hooks (or, if omission is intentional, add a short note
in lines 116–134 explaining why those files/directories are excluded from the
guide); ensure the tree formatting and file names match the actual repo
structure and update line ranges 116–134 accordingly.


The architecture is designed for performance:

**O(n) Reconciliation**: Linear in number of children
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Tighten the wording — "O(n)" already implies linear.

Line 182: **O(n) Reconciliation**: Linear in number of children is doing double-work. O(n) is linear. Pick one.

-**O(n) Reconciliation**: Linear in number of children
+**O(n) Reconciliation**: Optimal diffing complexity

Or if you want to be more specific:

-**O(n) Reconciliation**: Linear in number of children
+**O(n) Reconciliation**: Scales linearly with sibling count

This fixes the static analysis hint too. Keep it tight.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**O(n) Reconciliation**: Linear in number of children
**O(n) Reconciliation**: Optimal diffing complexity
🧰 Tools
🪛 LanguageTool

[style] ~182-~182: This wording could be more concise.
Context: ... performance: O(n) Reconciliation: Linear in number of children Effect Lists: Only pro...

(ADJECTIVE_IN_ATTRIBUTE)

🤖 Prompt for AI Agents
In docs/README.md around line 182, the heading "O(n) Reconciliation: Linear in
number of children" is redundant; replace it with a single, tighter phrasing
such as either "O(n) Reconciliation" or "Reconciliation — O(n) in number of
children" (or "Reconciliation: proportional to number of children") so the
complexity notation and description aren’t duplicated, and update the line
accordingly.

Comment on lines +228 to +239
## Further Reading

Want to go deeper?

**React Fiber Architecture**: The original React Fiber design doc

**React Reconciliation**: Official React docs on reconciliation

**Inside Fiber**: Deep dive talks by React team members

**Build Your Own React**: Tutorials building React from scratch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"Further Reading" section needs actual links or should be removed.

Lines 228–239 reference:

  • "React Fiber Architecture: The original React Fiber design doc"
  • "React Reconciliation: Official React docs on reconciliation"
  • "Inside Fiber: Deep dive talks by React team members"
  • "Build Your Own React: Tutorials building React from scratch"

These look like they should be links but aren't formatted as markdown links. Either add actual URLs or remove this section entirely. Dead references in docs are confusing.

If these are meant to be real links, use markdown syntax:

-**React Fiber Architecture**: The original React Fiber design doc
+**[React Fiber Architecture](https://react.dev/learn/understanding-react)**: The original React Fiber design doc

Or if you want to keep it simple and these aren't critical, just remove the section. Users can Google if they need more info.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In docs/README.md around lines 228 to 239, the "Further Reading" entries are
plain text and must be converted to real links or the section removed; update
each bullet to use markdown link syntax with appropriate URLs (for example:
React Fiber Architecture -> https://github.com/acdlite/react-fiber-architecture,
React Reconciliation -> https://reactjs.org/docs/reconciliation.html, Inside
Fiber talks -> https://www.youtube.com/watch?v=ZCuYPiUIONs, Build Your Own React
-> https://pomb.us/build-your-own-react/) or, if you prefer not to maintain
external links, delete the entire section. Ensure links open in new tabs by
using standard markdown link formatting and keep the list concise.

Comment on lines +5 to +27
const app = new Elysia()
.get("/app.js", () => {
const file = Bun.file("./public/app.js");
return new Response(file, {
headers: {
"Content-Type": "application/javascript; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
})
.get("/", () => {
const file = Bun.file("./src/templates/index.html");
const headers: Record<string, string> = {
"Content-Type": "text/html",
};

// Add caching in production
if (process.env.NODE_ENV === "production") {
headers["Cache-Control"] = "public, max-age=3600";
}

return new Response(file, { headers });
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing some production-ready concerns (but it's an example so... maybe fine?)

Look, this is an example app, so I'm not gonna go full enterprise architect on you. But since you're already thinking about production (line 22, the NODE_ENV check), let me drop some knowledge:

  1. No security headers - You're serving content but not setting security headers like X-Content-Type-Options: nosniff, X-Frame-Options: DENY, etc. These are basically free security wins.

  2. No request logging - In production, you'd want to know what requests are hitting your server. Elysia has middleware for this.

  3. No global error handler - If something throws, what happens? Does Elysia catch it? Does the server crash? Unknown.

Again, for an example app showcasing a fiber renderer, this is probably fine. But if this pattern gets copied to production code, these things matter.

If you want to make this more production-ready:

import { Elysia } from "elysia";
import { join } from "path";

const PROJECT_ROOT = join(import.meta.dir, "..");
const PORT = Number(process.env.PORT) || 3000;

const app = new Elysia()
  // Security headers middleware
  .onAfterHandle(({ response, set }) => {
    set.headers = {
      ...set.headers,
      "X-Content-Type-Options": "nosniff",
      "X-Frame-Options": "DENY",
      "X-XSS-Protection": "1; mode=block",
    };
  })
  // Error handling
  .onError(({ code, error }) => {
    console.error(`Error [${code}]:`, error);
    return new Response("Internal Server Error", { status: 500 });
  })
  .get("/app.js", () => {
    // ... with fixed paths ...
  })
  .listen(PORT);

But again - example app. Your call.

🤖 Prompt for AI Agents
In example/src/server.ts around lines 5 to 27, the example routes lack
production-safe concerns: add a middleware to inject common security headers
(e.g. X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection:
1; mode=block, and relevant CSP/Referrer-Policy as needed), add a
request-logging middleware to record incoming requests (method, path, status,
timing) for observability, and add a global error handler (.onError or
equivalent) that logs the error and returns a 500 response; keep the existing
caching logic for production but ensure middleware runs for every route and
imports/config (PORT, PROJECT_ROOT) are added if needed for listening in
production.

Comment on lines +61 to +78
// After processing, root should have finishedWork
// Phase 3: finishedWork is cleared after commit
expect(root.finishedWork).toBeNull();
});

test("should set finishedWork after render", () => {
const root = createFiberRoot(container);
const rootFiber = root.current;

rootFiber.pendingProps = { children: [] };

scheduleUpdateOnFiber(rootFiber);

// In Phase 2, finishedWork is set after render phase completes
// It will be cleared in Phase 4 (commit phase) after being committed
// Phase 3: finishedWork is cleared after commit
expect(root.finishedWork).toBeNull();
expect(root.current.alternate).toBe(rootFiber);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

These comments about "Phase 3" are confusing AF.

Lines 62, 63, 77:

// Phase 3: finishedWork is cleared after commit
expect(root.finishedWork).toBeNull();

What is "Phase 3"? Is that some internal development phase? A reconciliation phase? The comment doesn't help me understand WHY finishedWork should be null.

The test name says "should set finishedWork after render" but then expects it to be null because it was cleared after commit. That's... testing the opposite of the name?

Either:

  1. Fix the test name to match what you're actually testing:
-test("should set finishedWork after render", () => {
+test("should clear finishedWork after commit completes", () => {
  1. Or update comments to be clearer:
-// Phase 3: finishedWork is cleared after commit
+// finishedWork is set during render but cleared after commit completes

And remove "Phase 3" unless you've documented what these phases are elsewhere. If you're referring to React's actual reconciliation phases (render/commit), say that explicitly.

Comment on lines +233 to +242
// Check that effects were collected
const finishedWork = root.current; // Phase 3: check committed tree
expect(finishedWork).not.toBeNull();

// Root should have effects from children
// Either firstEffect or child should have effects
const hasEffects =
finishedWork?.firstEffect !== null || finishedWork?.child !== null;
expect(hasEffects).toBe(true);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This test is checking the wrong thing.

Lines 233-242:

const finishedWork = root.current; // Phase 3: check committed tree
expect(finishedWork).not.toBeNull();

// Root should have effects from children
// Either firstEffect or child should have effects
const hasEffects =
  finishedWork?.firstEffect !== null || finishedWork?.child !== null;
expect(hasEffects).toBe(true);

Bruh, you're checking "firstEffect OR child" exists? That's like saying "the pizza has pepperoni OR it exists" - of course it's true if the fiber has ANY child, regardless of effects!

You should be checking that effects were actually collected:

-const hasEffects =
-  finishedWork?.firstEffect !== null || finishedWork?.child !== null;
-expect(hasEffects).toBe(true);
+// Root should have collected placement effects from children
+expect(
+  finishedWork?.firstEffect !== null ||
+  (finishedWork?.child && finishedWork.child.effectTag !== 0)
+).toBe(true);

Or better yet, just check what you actually care about:

expect(finishedWork?.firstEffect).not.toBeNull();

Be specific about what you're testing, chat.

🤖 Prompt for AI Agents
In tests/fiber/workLoop.test.ts around lines 233-242, the test currently checks
a boolean that ORs finishedWork.firstEffect with finishedWork.child which
falsely passes when any child exists; update the assertion to specifically
verify that effects were collected by asserting finishedWork is not null and
that finishedWork.firstEffect is not null (i.e., remove the OR-with-child check
and replace it with expect(finishedWork?.firstEffect).not.toBeNull()).

Comment on lines +508 to +533
test("should handle large trees efficiently", () => {
const root = createFiberRoot(container);
const rootFiber = root.current;

// Create a wide tree (100 siblings)
const children = [];
for (let i = 0; i < 100; i++) {
children.push({ type: "div", props: { key: i, children: [] } });
}

rootFiber.pendingProps = { children };

scheduleUpdateOnFiber(rootFiber);

// Verify tree was constructed correctly
expect(root.current).not.toBeNull();

// Count siblings to verify all 100 children were created
let count = 0;
let child = root.current.child;
while (child) {
count++;
child = child.sibling;
}
expect(count).toBe(100);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test name is lying about what it validates.

Line 508: test("should handle large trees efficiently")

But then the test just creates 100 children and checks they were all created (lines 526-532). There's no efficiency validation - no timing, no memory checks, nothing performance-related. You literally just count to 100.

After past review removed timing assertions (good call), the test name became misleading.

-test("should handle large trees efficiently", () => {
+test("should handle wide trees with many siblings", () => {

And same for line 535:

-test("should handle deep trees efficiently", () => {
+test("should handle deeply nested trees", () => {

Don't claim "efficiently" unless you're measuring something. These are correctness tests, not performance tests. Own it, chat.

🤖 Prompt for AI Agents
In tests/fiber/workLoop.test.ts around lines 508–533 the test title "should
handle large trees efficiently" is misleading because the test only asserts that
100 children were created and does not measure performance; rename the test to
something like "should create wide trees with 100 children" (and similarly
update the other test around line 535), or alternatively add explicit
timing/memory assertions if you really want to validate efficiency—make the
change so the test name accurately reflects whether it is a correctness check or
a performance test.

Comment on lines +1 to +449
/**
* Comprehensive tests for falsy value rendering
*
* Tests cover:
* - Rendering 0, empty string, false, null, undefined
* - Conditional rendering patterns
* - Arrays with falsy values
* - Switching between falsy and truthy values
*/

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { render } from "../src/MiniReact";

describe("MiniReact.FalsyValues - Comprehensive", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});

afterEach(() => {
if (container?.parentNode) {
container.parentNode.removeChild(container);
}
});

describe("Rendering Number 0", () => {
test("should render 0 as text content", () => {
render({ type: "div", props: { children: [0] } }, container);

expect(container.textContent).toBe("0");
});

test("should render 0 in nested structure", () => {
render(
{
type: "div",
props: {
children: [{ type: "span", props: { children: [0] } }],
},
},
container,
);

expect(container.querySelector("span")?.textContent).toBe("0");
});

test("should handle switching from 0 to another number", () => {
render({ type: "div", props: { children: [0] } }, container);
expect(container.textContent).toBe("0");

render({ type: "div", props: { children: [42] } }, container);
expect(container.textContent).toBe("42");
});

test("should handle switching from truthy to 0", () => {
render({ type: "div", props: { children: ["hello"] } }, container);
expect(container.textContent).toBe("hello");

render({ type: "div", props: { children: [0] } }, container);
expect(container.textContent).toBe("0");
});

test("should render multiple zeros", () => {
render(
{
type: "div",
props: {
children: [0, 0, 0],
},
},
container,
);

expect(container.textContent).toBe("000");
});

test("should render 0 in array with other values", () => {
render(
{
type: "div",
props: {
children: ["count: ", 0],
},
},
container,
);

expect(container.textContent).toBe("count: 0");
});
});

describe("Rendering Empty String", () => {
test("should render empty string", () => {
render({ type: "div", props: { children: [""] } }, container);

expect(container.textContent).toBe("");
expect(container.childNodes.length).toBeGreaterThan(0);
});

test("should handle switching from empty string to content", () => {
render({ type: "div", props: { children: [""] } }, container);
expect(container.textContent).toBe("");

render({ type: "div", props: { children: ["hello"] } }, container);
expect(container.textContent).toBe("hello");
});

test("should handle switching from content to empty string", () => {
render({ type: "div", props: { children: ["hello"] } }, container);
expect(container.textContent).toBe("hello");

render({ type: "div", props: { children: [""] } }, container);
expect(container.textContent).toBe("");
});

test("should render empty string as prop value", () => {
render({ type: "input", props: { value: "", children: [] } }, container);

const input = container.querySelector("input");
expect(input?.value).toBe("");
});

test("should render empty string in array", () => {
render(
{
type: "div",
props: {
children: ["start", "", "end"],
},
},
container,
);

expect(container.textContent).toBe("startend");
});
});

describe("Rendering Boolean False", () => {
test("should not render false as text content", () => {
render({ type: "div", props: { children: [false] } }, container);

expect(container.textContent).toBe("");
});

test("should handle conditional rendering with false", () => {
render(
{
type: "div",
props: {
children: [
false,
{ type: "span", props: { children: ["visible"] } },
],
},
},
container,
);

expect(container.textContent).toBe("visible");
expect(container.querySelector("span")).not.toBeNull();
});

test("should handle switching from false to true (both render nothing)", () => {
render({ type: "div", props: { children: [false] } }, container);
expect(container.textContent).toBe("");

render({ type: "div", props: { children: [true] } }, container);
// true also doesn't render as text (React behavior)
expect(container.textContent).toBe("");
});

test("should use false as prop value", () => {
render(
{ type: "input", props: { disabled: false, children: [] } },
container,
);

const input = container.querySelector("input");
expect(input?.disabled).toBe(false);
});
});

describe("Rendering Null", () => {
test("should render nothing for null", () => {
render(null, container);

expect(container.textContent).toBe("");
expect(container.childNodes.length).toBe(0);
});

test("should handle null in children array", () => {
render(
{
type: "div",
props: {
children: ["before", null, "after"],
},
},
container,
);

expect(container.textContent).toBe("beforeafter");
});

test("should handle switching from element to null", () => {
render({ type: "div", props: { children: ["content"] } }, container);
expect(container.textContent).toBe("content");

render(null, container);
expect(container.textContent).toBe("");
expect(container.childNodes.length).toBe(0);
});

test("should handle switching from null to element", () => {
render(null, container);
expect(container.textContent).toBe("");

render({ type: "div", props: { children: ["content"] } }, container);
expect(container.textContent).toBe("content");
});

test("should handle nested null values", () => {
render(
{
type: "div",
props: {
children: [{ type: "span", props: { children: [null] } }, "text"],
},
},
container,
);

expect(container.textContent).toBe("text");
expect(container.querySelector("span")).not.toBeNull();
});
});

describe("Rendering Undefined", () => {
test("should render nothing for undefined", () => {
render(undefined, container);

expect(container.textContent).toBe("");
expect(container.childNodes.length).toBe(0);
});

test("should handle undefined in children array", () => {
render(
{
type: "div",
props: {
children: ["before", undefined, "after"],
},
},
container,
);

expect(container.textContent).toBe("beforeafter");
});

test("should handle switching from element to undefined", () => {
render({ type: "div", props: { children: ["content"] } }, container);
expect(container.textContent).toBe("content");

render(undefined, container);
expect(container.textContent).toBe("");
});
});

describe("Mixed Falsy Values", () => {
test("should handle array with multiple falsy types", () => {
render(
{
type: "div",
props: {
children: [0, "", false, null, undefined, "text"],
},
},
container,
);

// 0 and "" render, false/null/undefined don't
expect(container.textContent).toBe("0text");
});

test("should handle switching between different falsy values", () => {
render({ type: "div", props: { children: [0] } }, container);
expect(container.textContent).toBe("0");

render({ type: "div", props: { children: [""] } }, container);
expect(container.textContent).toBe("");

render({ type: "div", props: { children: [false] } }, container);
expect(container.textContent).toBe("");

render({ type: "div", props: { children: [null] } }, container);
expect(container.textContent).toBe("");
});

test("should handle elements with falsy props", () => {
render(
{
type: "div",
props: {
id: "",
"data-count": 0,
"data-flag": false,
children: [],
},
},
container,
);

const div = container.querySelector("div");
expect(div?.getAttribute("id")).toBe("");
expect(div?.getAttribute("data-count")).toBe("0");
// false removes the attribute (doesn't set it)
expect(div?.getAttribute("data-flag")).toBeNull();
});
});

describe("Conditional Rendering Patterns", () => {
test("should handle logical AND with falsy left side", () => {
render(
{
type: "div",
props: {
children: [
false,
{ type: "span", props: { children: ["never shown"] } },
],
},
},
container,
);

expect(container.querySelector("span")).not.toBeNull();
});

test("should handle ternary with falsy values", () => {
// Simulating: condition ? 0 : "fallback"
render(
{
type: "div",
props: {
children: [0],
},
},
container,
);

expect(container.textContent).toBe("0");

render(
{
type: "div",
props: {
children: ["fallback"],
},
},
container,
);

expect(container.textContent).toBe("fallback");
});

test("should handle nested conditionals with falsy values", () => {
render(
{
type: "div",
props: {
children: [
{
type: "div",
props: {
children: [
false,
{
type: "span",
props: {
children: [0],
},
},
],
},
},
],
},
},
container,
);

expect(container.querySelector("span")?.textContent).toBe("0");
});
});

describe("Edge Cases", () => {
test("should handle NaN", () => {
render({ type: "div", props: { children: [Number.NaN] } }, container);

expect(container.textContent).toBe("NaN");
});

test("should handle -0", () => {
render({ type: "div", props: { children: [-0] } }, container);

expect(container.textContent).toBe("0");
});

test("should handle very small numbers", () => {
render({ type: "div", props: { children: [0.0000001] } }, container);

// JavaScript may use scientific notation for very small numbers
expect(container.textContent).toMatch(/1e-7|0\.0000001/);
});

test("should handle empty array", () => {
render({ type: "div", props: { children: [] } }, container);

expect(container.textContent).toBe("");
});

test("should handle array of only falsy values", () => {
render(
{
type: "div",
props: {
children: [false, null, undefined],
},
},
container,
);

expect(container.textContent).toBe("");
});

test("should preserve whitespace-only strings", () => {
render({ type: "div", props: { children: [" "] } }, container);

expect(container.textContent).toBe(" ");
});

test("should handle newline and tab characters", () => {
render({ type: "div", props: { children: ["\n\t"] } }, container);

expect(container.textContent).toBe("\n\t");
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

This test suite is CLEAN, chat. Props where props are due.

The organization is chef's kiss:

  • Clear describe blocks by value type
  • Tests for switching between values (lines 49-63)
  • Edge cases properly isolated (lines 398-449)
  • Proper setup/teardown

But I'm seeing one missing test case that React actually handles: Symbol values. Check this out:

render(<div>{Symbol('test')}</div>, container)

React throws an error for Symbols in children. Your tests cover 0, "", false, null, undefined, NaN, but not Symbols. Since Symbols are falsy-adjacent (can't be rendered) and increasingly common with Symbol.iterator, Symbol.asyncIterator, etc., you should test this behavior.

Add to the Edge Cases section:

test("should handle Symbol values", () => {
  const sym = Symbol("test");
  
  // Symbols can't be rendered as children
  // Should either skip them or throw a clear error
  expect(() => 
    render(
      { type: "div", props: { children: [sym] } },
      container
    )
  ).toThrow(); // or test your skip behavior if you silently ignore
});

This will catch if you accidentally try to String(Symbol()) somewhere, which throws.

🤖 Prompt for AI Agents
In tests/MiniReact.falsyValues.test.ts around lines 398 to 449 (Edge Cases),
there is no test for rendering Symbol children — React throws for Symbol
children and we should cover that; add a test named "should handle Symbol
values" that creates a Symbol("test") and asserts the render call with that
symbol child either throws (expect(() => render(...)).toThrow()) or, if your
implementation intentionally ignores Symbols, asserts that container.textContent
remains "" — implement the assertion that matches your intended behavior and
place the test in the Edge Cases block.

Comment on lines +79 to +91
test("should render 0 in array with other values", () => {
render(
{
type: "div",
props: {
children: ["count: ", 0],
},
},
container,
);

expect(container.textContent).toBe("count: 0");
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test name doesn't match what it's testing.

Line 79: test("should render 0 in array with other values")

But the test does children: ["count: ", 0] - that's not "in array with other values", that's specifically testing 0 following a string. The name makes it sound like you're testing [1, 0, 2].

More precise name:

-test("should render 0 in array with other values", () => {
+test("should render 0 adjacent to string content", () => {

Or if you want to test the "array with other values" case, add:

test("should render 0 in array with other numbers", () => {
  render(
    {
      type: "div",
      props: { children: [1, 0, 2] }
    },
    container
  );
  expect(container.textContent).toBe("102");
});

Minor nitpick but test names are documentation, my dude.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants