The React Library Ecosystem

React turns your components into a tree of elements and schedules the work of putting that tree on screen. It has no date picker, no chart, no modal, no HTTP client, no design system. That omission is why a real application pulls twenty packages out of npm before it renders its first screen.The good part of the bargain is choice; the bad part is that a dependency is a promise someone else has to keep. Packages every tutorial recommended five years ago are now archived repositories that never got a React 19 release. The survivors fall into four kinds: component kits such as React Bootstrap, which ship finished styled widgets; headless libraries, which ship the behavior and none of the appearance; domain libraries that solve one problem well; and development tools such as Storybook, which never reach users at all.Every version, licence and download number here was read from the npm registry and the GitHub API on 17 September 2026; the next subsection shows the commands.

2.19.1 Choosing a Library

Star counts are the worst way to judge a React package: stars accumulate and never expire. Three registry queries tell you more, in ten seconds.

npm view react-bootstrap version time.modified peerDependencies license

npm view react-bootstrap dist-tags

curl -s https://api.npmjs.org/downloads/point/last-week/react-bootstrap

Listing 2.180: Interrogating a package before you install it

version = '2.10.10'

time.modified = '2025-09-22T05:34:15.235Z'

peerDependencies = { react: '>=16.14.0', 'react-dom': '>=16.14.0', ... }

license = 'MIT'

Output of Listing 2.180 time.modified is the last time anything was published: a year of silence in a library with open React 19 issues is a warning, the same silence in a small, finished utility is fine. peerDependencies is the compatibility contract — react: '>=16.14.0' accepts React 19, while a range stopping at ^18.0.0 means npm refuses the install or you pin --legacy-peer-deps forever. Weekly downloads measure how many people hit a breaking change with you; under about 50,000 a week there is no crowd to file the bugs you will find. dist-tags shows whether a rewrite is in beta — React Bootstrap's next points at 3.0.0-beta.5. Package Version (published) Licence Weekly downloads @tanstack/react-query 5.103.1 (Sep 2026) MIT 45.6 M recharts 3.10.1 (Jul 2026) MIT 40.5 M storybook 10.6.0 (Sep 2026) MIT 16.0 M motion 13.4.0 (Sep 2026) MIT 15.4 M radix-ui 1.6.7 (Jul 2026) MIT 10.1 M @headlessui/react 2.2.10 (Apr 2026) MIT 5.4 M react-aria-components 1.21.1 (Sep 2026) Apache-2.0 3.1 M react-bootstrap 2.10.10 (May 2025) MIT 1.1 M Table 2.25: The libraries used in this section, as published on npm on 17 September 2026 Four names still appear in tutorials and are a bad choice today: react-charts (a beta from May 2020, repository archived — use Recharts), react-motion (October 2017 — use Motion), Create React App (sunset February 2025 — use Vite, Section 2.1.4) and react-router-dom (folded into react-router, Section 2.16.2). Tip: npm outdated lists dependencies with a newer version. It cannot tell you which have no newer version because nobody publishes any more. Once a quarter, print time.modified for every direct dependency and sort by date: the packages at the bottom are your next migration.

2.19.2 React Bootstrap

Bootstrap is a CSS toolkit plus a JavaScript bundle that makes its interactive pieces work. That JavaScript is the problem in React: Bootstrap's plugins find elements with querySelector, toggle classes on them and keep state in DOM attributes — exactly the mutations React assumes nobody else is making. React Bootstrap rebuilds every widget as a real component whose state lives in React, with no jQuery. You keep the stylesheet, imported once at the entry point.

import 'bootstrap/dist/css/bootstrap.min.css'; // npm install react-bootstrap bootstrap

import { Card, Button, Badge } from 'react-bootstrap';

export default function ProductCard({ product, onAdd }) {

return (

<Card style={{ width: '18rem' }}>

<Card.Body>

<Card.Title>{product.name} <Badge bg="success">new</Badge></Card.Title>

<Card.Text className="text-muted">${product.price.toFixed(2)}</Card.Text>

<Button variant="primary" size="sm" onClick={() => onAdd(product.id)}>

Add to cart</Button>

</Card.Body>

</Card>

);

}

Listing 2.181: A card built from React Bootstrap components Three conventions carry across the library. Compound components use dot notation (Card.Body, Card.Title), keeping the JSX shaped like the markup Bootstrap expects. Class-name choices become props: variant="primary" and size="sm" produce btn btn-primary btn-sm. Unrecognized props — className, id, data-*, aria-*, ref — are forwarded to the DOM element underneath, the escape hatch for whatever the API did not anticipate.Version 2.10.10 targets Bootstrap 5.3 and runs against React 19.3.0, as the next subsection's demo shows; 3.0.0-beta.5 sits on the next tag, so read the v3 migration notes before starting something large. Bigger styled kits exist — MUI 9.4.0 at 7.2 M weekly downloads, Ant Design 6.6.4 at 2.7 M, Mantine 9.6.1 at 1.8 M — but this is the shortest step from the Bootstrap you know. Note: Bootstrap's CSS is global. If your project already uses Tailwind or CSS modules, importing bootstrap.min.css restyles every <button> and <h1> on the page, not only the React Bootstrap ones.

2.19.3 Carousel

<Carousel> holds the strip of items, <Carousel.Item> wraps each slide, and <Carousel.Caption> positions text over it. Left alone it advances every five seconds; pass activeIndex and onSelect and the current slide becomes your state, which you need as soon as anything else on the page cares what is showing. The demo runs with no build step: an import map resolves the bare specifiers and @babel/standalone compiles the JSX.

<link rel="stylesheet"

href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">

<div id="root" class="p-3"></div>

<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

<script type="importmap">{"imports": {

"react": "https://esm.sh/react@19.3.0",

"react/jsx-runtime": "https://esm.sh/react@19.3.0/jsx-runtime",

"react-dom": "https://esm.sh/react-dom@19.3.0",

"react-dom/client": "https://esm.sh/react-dom@19.3.0/client",

"react-bootstrap": "https://esm.sh/react-bootstrap@2.10.10?external=react,react-dom"

}}</script>

<script type="text/babel" data-type="module">

import React, { useState } from 'react';

import { createRoot } from 'react-dom/client';

import { Carousel } from 'react-bootstrap';

const slides = [['Split keyboards', '#1565c0'], ['Studio monitors', '#2e7d32'],

['Braided cables', '#6a1b9a']];

function Gallery() {

const [i, setI] = useState(0);

return (

<Carousel activeIndex={i} onSelect={setI} interval={null}>

{slides.map(([name, color]) => (

<Carousel.Item key={name}>

<div style={{ height: 150, background: color }} />

<Carousel.Caption><h3 className="h5">{name} ({i + 1} of 3)</h3></Carousel.Caption>

</Carousel.Item>))}

</Carousel>);

}

createRoot(document.getElementById('root')).render(<Gallery />);

</script>

Listing 2.182: A controlled carousel running React Bootstrap without a build step Figure 2.62: Browser output of Listing 2.182onSelect receives the index being moved to, so setI alone keeps the pair in sync. Other props: interval in milliseconds, pause="hover" (the default) or pause={false}, fade for a cross-fade, controls={false} and indicators={false} to drop the chrome, and touch for swipes. Warning: Older carousel examples set src="holder.js/800x400" on the slide images. Holder.js was a placeholder script Bootstrap dropped in v5, so those slides now render empty. Use real images or a colored block. Carousels are hostile to accessibility: content moves without user intent, and a screen reader can be read a slide that has already rotated away. The indicators are real buttons and the controls take prevLabel and nextLabel, but ask first whether a grid showing every item serves the reader better.

2.19.4 Tooltip

A tooltip is a label that appears beside an element while it is hovered or focused. React Bootstrap splits the job: <Tooltip> is the bubble, <OverlayTrigger> decides when to show it and where.

import { OverlayTrigger, Tooltip, Button } from 'react-bootstrap';

const places = ['top', 'right', 'bottom', 'left'];

export default function Toolbar() {

return places.map((placement) => (

<OverlayTrigger key={placement} placement={placement} delay={{ show: 250, hide: 100 }}

overlay={<Tooltip id={`tip-${placement}`}>On <strong>{placement}</strong></Tooltip>}>

<Button variant="secondary" className="me-2">Tooltip on {placement}</Button>

</OverlayTrigger>

));

}

Listing 2.183: One tooltip per placement, driven by a map over four positions OverlayTrigger takes exactly one child and clones it to attach onMouseEnter, onMouseLeave, onFocus, onBlur and a ref, so a custom child must forward its props and ref to a real DOM node. The bubble renders through a portal (Section 2.11.7) into document.body, which is how it escapes an overflow: hidden ancestor. Popper (@popperjs/core, via @restart/ui) positions it at open time and flips the placement when the preferred side has no room. Set trigger to 'click', 'focus' or ['hover', 'focus'], and show with onToggle to drive it from state.One thing it does not do is announce itself: version 2.10.10 puts no aria-describedby on the trigger, so a screen reader user gets the button's label and nothing else. Add aria-describedby="tip-top" to the <Button> yourself, matching the id you gave <Tooltip>, whenever the bubble carries information the label does not. Note: A tooltip is not a place for links or buttons: hover-only content is unreachable by keyboard and by touch. When the content is interactive, use <Popover> with trigger="click", which manages focus.

2.19.5 Button Group

<ButtonGroup> glues buttons into one control: inner borders collapse and only the outer corners stay rounded. Its whole API is size, vertical and an aria-label, and it composes with dropdowns through as, the prop every React Bootstrap component accepts to change what it renders as.The version you reach for more often is <ToggleButtonGroup>, which turns the same row into a radio or checkbox control with state. Each <ToggleButton> needs an id, because it renders a real <input> behind the label, and that hidden input is what makes the group keyboard-navigable and readable by assistive technology.

// every component here is a named export of 'react-bootstrap'

<ButtonGroup aria-label="Page size">

<Button variant="outline-secondary">10</Button>

<Button variant="outline-secondary">25</Button>

<DropdownButton as={ButtonGroup} title="More" variant="outline-secondary" id="more">

<Dropdown.Item eventKey="50">50 per page</Dropdown.Item>

</DropdownButton>

</ButtonGroup>

<ToggleButtonGroup type="radio" name="sort" value={value} onChange={onChange}>

<ToggleButton id="s-new" value="new" variant="outline-primary">Newest</ToggleButton>

<ToggleButton id="s-price" value="price" variant="outline-primary">Price</ToggleButton>

</ToggleButtonGroup>

Listing 2.184: A nested dropdown, and a segmented control from a radio toggle group type="radio" hands onChange the selected value; type="checkbox" hands it an array. Omit value and the group keeps its own state — the uncontrollable package React Bootstrap depends on implements exactly that pattern, so one component works controlled or uncontrolled without two APIs.

2.19.6 Headless Component Libraries

A component kit gives you behavior and appearance, and you fight the appearance whenever your design differs from the kit's. A headless library ships only the hard half: focus management, keyboard interaction, ARIA roles and states, portals, collision-aware positioning. It renders unstyled elements carrying data attributes such as data-state="open"; you supply every class name.

import { Dialog } from 'radix-ui'; // inside a DeleteButton({ onConfirm }) component

<Dialog.Root>

<Dialog.Trigger className="btn-danger">Delete</Dialog.Trigger>

<Dialog.Portal>

<Dialog.Overlay className="overlay" />

<Dialog.Content className="modal">

<Dialog.Title>Delete this product?</Dialog.Title>

<Dialog.Description>This cannot be undone.</Dialog.Description>

<button onClick={onConfirm}>Delete</button>

<Dialog.Close>Cancel</Dialog.Close>

</Dialog.Content>

</Dialog.Portal>

</Dialog.Root>

Listing 2.185: A modal dialog assembled from Radix primitives Those lines get you a focus trap, aria-modal with the title and description wired to it, Escape to close, a scroll lock, click-outside dismissal and focus returned to the trigger — a week of work if you write it yourself. Every part also accepts asChild, merging its behavior into your own element. Library npm package From Weekly downloads Radix Primitives radix-ui 1.6.7 WorkOS 10.1 M Base UI @base-ui/react 1.8.0 Radix, MUI and Floating UI authors 9.7 M Headless UI @headlessui/react 2.2.10 Tailwind Labs 5.4 M React Aria Components react-aria-components 1.21.1 Adobe 3.1 M Table 2.26: Headless React libraries, npm figures from 17 September 2026 Radix is the safe default and the one most other projects build on; React Aria Components goes furthest on internationalization, at the cost of a larger API; Headless UI is the smallest and assumes Tailwind. shadcn/ui belongs here but not in the table: its CLI copies Radix-based, Tailwind-styled component source files into your repository, which you then own and edit — no upgrade path, total control. Tip: The same idea runs through Section 2.9: Downshift is a headless combobox and React Hook Form is headless form state. Behavior as Hooks, markup left to you.

2.19.7 Charting

A chart library turns numbers into coordinates and coordinates into marks. Recharts does both with React components: declare <XAxis>, <CartesianGrid> and <Line> inside a chart and it renders SVG you can style.

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,

ResponsiveContainer } from 'recharts';

const data = [{ month: 'Jan', orders: 120, returns: 9 },

{ month: 'Feb', orders: 145, returns: 12 }, { month: 'Mar', orders: 131, returns: 7 },

{ month: 'Apr', orders: 188, returns: 14 }];

// the body of an OrdersChart component:

<ResponsiveContainer width="100%" height={280}>

<LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>

<CartesianGrid stroke="#eceff1" /><XAxis dataKey="month" /><YAxis /><Tooltip /><Legend />

<Line type="monotone" dataKey="orders" stroke="#1565c0" strokeWidth={2} />

<Line type="monotone" dataKey="returns" stroke="#ef6c00" strokeWidth={2} />

</LineChart>

</ResponsiveContainer>

Listing 2.186: A two-series line chart with a tooltip and legend dataKey is the whole data binding: each axis and series names a property of the row objects, so reshaping the API response into flat objects is most of the work. <ResponsiveContainer> watches its parent with a ResizeObserver, which is why the chart takes no width of its own. Under the hood: Recharts 3 computes scales and paths with the D3 modules the Victory team repackaged as victory-vendor, and coordinates axes, tooltips and brush state through a private Redux store — Redux Toolkit and React Redux are in its dependencies (Section 2.18). None of it is exposed to your app. Library Version Renders Weekly downloads Recharts 3.10.1 SVG, React components 40.5 M Chart.js with react-chartjs-2 4.5.1 / 5.3.1 Canvas, config object 9.2 M Nivo 0.99.0 SVG and canvas, pre-1.0 1.2 M Victory 37.3.6 SVG, also React Native 0.3 M Table 2.27: React charting libraries, all MIT, npm figures from 17 September 2026 Pick on rendering mode before features. SVG gives one DOM node per point — easy to style, slow past a few thousand marks. Canvas draws pixels: Chart.js handles tens of thousands of points and gives you nothing to select.

2.19.8 Animation

The thing CSS cannot do alone is animate an element React is about to unmount: by the time the transition would run, the node is gone. Motion — formerly published as Framer Motion — keeps removed children in the tree until their exit animation finishes.

import { motion, AnimatePresence } from 'motion/react';

// the body of a CartLines({ lines, onRemove }) component:

<ul>

<AnimatePresence initial={false}>

{lines.map((line) => (

<motion.li key={line.id} layout

initial={{ opacity: 0, x: -24 }} animate={{ opacity: 1, x: 0 }}

exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.18 }}>

{line.name} <button onClick={() => onRemove(line.id)}>Remove</button>

</motion.li>

))}

</AnimatePresence>

</ul>

Listing 2.187: Animating items in and out of a list Any tag can be prefixed: motion.div, motion.li, motion.path. initial, animate and exit are style snapshots that Motion interpolates between; whileHover and whileTap add gesture states; layout animates a component's own position and size when siblings change, measured with getBoundingClientRect before and after the commit. Version 13.4.0 ships a motion/react-client entry for Server Components (Chapter 5).Two alternatives matter. @react-spring/web 10.1.2 animates with spring physics instead of durations, which feels better for drag work. react-transition-group 4.4.5 animates nothing itself — it adds and removes class names at the right moments and leaves the rest to CSS — which is why it shows 37 M weekly downloads: React Bootstrap and many other kits depend on it. Check first whether a CSS transition or View Transitions (Section 2.8.10) already covers the effect.

2.19.9 Data Fetching with TanStack Query

Data that lives on a server is not application state. You do not own it, it goes stale without telling you, and two components asking for it at once should not produce two requests. The Effect-based fetch of Section 2.4.4 handles none of that; npm install @tanstack/react-query handles all of it behind two Hooks. One QueryClient holds the cache and one provider puts it in context; each query is identified by a queryKey, an array that doubles as a dependency list — change the key and Query fetches the new entry while showing the old.

import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient }

from '@tanstack/react-query';

const client = new QueryClient();

const api = 'https://api.example.com/products';

function Products({ category }) {

const qc = useQueryClient();

const { data, isPending, isError, error } = useQuery({

queryKey: ['products', category], staleTime: 30_000,

queryFn: () => fetch(`${api}?category=${category}`).then((r) => r.json()),

});

const add = useMutation({

mutationFn: (p) => fetch(api, { method: 'POST', body: JSON.stringify(p) }),

onSuccess: () => qc.invalidateQueries({ queryKey: ['products'] }),

});

if (isPending) return <p>Loading...</p>;

if (isError) return <p>{error.message}</p>;

return (<>

<ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>

<button onClick={() => add.mutate({ name: 'New', category })}>Add</button>

</>);

}

export default () => (

<QueryClientProvider client={client}><Products category="keyboards"/></QueryClientProvider>);

Listing 2.188: A cached query and a mutation that invalidates it What those lines buy: identical keys mounted anywhere share one request and one cache entry; a query older than staleTime refetches in the background on mount, on window focus and on reconnect while the stale data stays on screen; failures retry with exponential backoff; and invalidateQueries marks every key beginning with ['products'] stale, so the list refetches itself after a successful mutation.The alternatives are SWR (2.5.1, smaller) and RTK Query (Section 2.18.17), the right answer when the project already runs Redux Toolkit. Never run two: each keeps its own cache, and the same endpoint fetched through both will disagree with itself.

2.19.10 Storybook

Storybook runs your components outside the application: no routes, no store, no login screen, just a component and the props you want to see it with. Each combination is a story; together they form a catalog designers, testers and new team members can browse without running the app.npx storybook@latest init detects Vite and React, writes .storybook/ plus sample stories, and adds the scripts, so npm run storybook opens the catalog. A story file sits next to its component: metadata in the default export, one named export per state.

import ProductCard from './ProductCard';

export default {

component: ProductCard,

args: { product: { id: 1, name: 'Split keyboard', price: 89 } },

};

export const Default = {};

export const LongName = {

args: { product: { id: 2, name: 'Programmable 68-key split board', price: 129 } },

};

export const FreeItem = { args: { product: { id: 3, name: 'Sticker pack', price: 0 } } };

Listing 2.189: Stories for the ProductCard component args are the props, merged from the default export into each story, and the panel under the preview lets anyone edit them live. A story for the empty state, the long-text state and the error state catches the layout bugs a happy-path application never shows you.Storybook is by far the most used tool of its kind. Two things beyond the catalog justify the setup: the accessibility add-on runs axe against every story and lists violations beside it, and the test runner turns the same stories into assertions, so the states you documented get tested (Section 2.15).