diff --git a/packages/dev/s2-docs/pages/react-aria/Disclosure/useDisclosure.mdx b/packages/dev/s2-docs/pages/react-aria/Disclosure/useDisclosure.mdx
index f2463b5c136..c0fa77ce5fa 100644
--- a/packages/dev/s2-docs/pages/react-aria/Disclosure/useDisclosure.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Disclosure/useDisclosure.mdx
@@ -48,6 +48,7 @@ import {Disclosure} from 'hooks-starter/Disclosure';
diff --git a/packages/dev/s2-docs/pages/react-aria/GridList/useGridList.mdx b/packages/dev/s2-docs/pages/react-aria/GridList/useGridList.mdx
index 51500823d52..b1e1acbc1aa 100644
--- a/packages/dev/s2-docs/pages/react-aria/GridList/useGridList.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/GridList/useGridList.mdx
@@ -100,6 +100,7 @@ import {GridList, GridListItem, Text} from 'hooks-starter/GridList';
{function: statelyDocs.exports.useListState, links: statelyDocs.links},
{function: docs.exports.useGridList, links: docs.links},
{function: docs.exports.useGridListItem, links: docs.links},
+ {function: docs.exports.useGridListSelectionCheckbox, links: docs.links},
]}
/>
diff --git a/packages/dev/s2-docs/pages/react-aria/Popover/usePopover.mdx b/packages/dev/s2-docs/pages/react-aria/Popover/usePopover.mdx
index 05245b64f28..4a030e1e0e0 100644
--- a/packages/dev/s2-docs/pages/react-aria/Popover/usePopover.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Popover/usePopover.mdx
@@ -54,6 +54,7 @@ import {PopoverTrigger} from 'hooks-starter/Popover';
diff --git a/packages/dev/s2-docs/pages/react-aria/Table/useTable.mdx b/packages/dev/s2-docs/pages/react-aria/Table/useTable.mdx
index fd1985fbff9..0be99626989 100644
--- a/packages/dev/s2-docs/pages/react-aria/Table/useTable.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Table/useTable.mdx
@@ -83,12 +83,16 @@ import {Table, TableHeader, TableBody, Column, Row, Cell} from 'hooks-starter/Ta
diff --git a/packages/dev/s2-docs/pages/react-aria/Toast/useToast.mdx b/packages/dev/s2-docs/pages/react-aria/Toast/useToast.mdx
index e09dd6aa4e7..b86e7c0b126 100644
--- a/packages/dev/s2-docs/pages/react-aria/Toast/useToast.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Toast/useToast.mdx
@@ -55,6 +55,7 @@ import {ToastProvider, Button} from 'hooks-starter/Toast';
{docs.exports.useAsyncList.description}
+
+## Introduction
+
+`useAsyncList` extends on [useListData](useListData.html), adding support for async loading, pagination, sorting, and filtering.
+It manages loading and error states, supports abortable requests, and works with any data fetching library or the built-in
+browser [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API.
+
+## API
+
+
+
+## Options
+
+
+
+## Interface
+
+
+
+## Example
+
+To construct an async list, pass a `load` function to `useAsyncList` that returns the items to render.
+You can use the state returned by `useAsyncList` to render a [collection component](collections.html).
+
+This example fetches a list of Pokemon from an API and displays them in a Picker. It uses
+[fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to load the data, passing through the abort signal
+given by `useAsyncList` and returning the results from the API. The `isLoading` prop is passed to the Picker
+to tell it to render the loading spinner while data is loading.
+
+```tsx
+let list = useAsyncList({
+ async load({signal}) {
+ let res = await fetch('https://pokeapi.co/api/v2/pokemon', {signal});
+ let json = await res.json();
+ return {items: json.results};
+ }
+});
+
+
+ {item => - {item.name}
}
+
+```
+
+### Infinite loading
+
+`useAsyncList` also supports paginated data, which is common in many APIs to avoid loading too many items at once.
+This is accomplished by returning a cursor in addition to `items` from the load function. When the `loadMore` method
+is called, the cursor is passed back to your `load` function, which you can use to determine the URL for the next
+page. The `onLoadMore` prop supported by many collection components notifies you when you should load more data
+as the user scrolls.
+
+This example expands on the previous one to support infinite scrolling through all known Pokemon. It returns the
+`next` property from the API response as the `cursor`, and uses it instead of the original API URL for subsequent
+page loads. It passes the `onLoadMore` prop to Picker, which triggers loading more items as the user scrolls down.
+
+```tsx
+let list = useAsyncList({
+ async load({signal, cursor}) {
+ // If no cursor is available, then we're loading the first page.
+ // Otherwise, the cursor is the next URL to load, as returned from the previous page.
+ let res = await fetch(cursor || 'https://pokeapi.co/api/v2/pokemon', {signal});
+ let json = await res.json();
+ return {
+ items: json.results,
+ cursor: json.next
+ };
+ }
+});
+
+
+ {item => - {item.name}
}
+
+```
+
+### Reloading data
+
+Data can be reloaded by calling the `reload` method of the list.
+
+```tsx
+list.reload();
+```
+
+## Sorting
+
+Some components like tables support sorting data. You may also have custom UI to implement this in other components.
+This can be implemented by passing a `sort` function to `useAsyncList`, or by using the `sortDescriptor` passed to
+`load` if no `sort` function is given. Passing a separate `sort` function could be useful when implementing client side
+sorting. Using the `sortDescriptor` in `load` is useful when you need to implement server side sorting, which might be
+an API parameter.
+
+### Client side sorting
+
+This example shows how to implement client side sorting by passing a `sort` function to `useAsyncList` and sorting the
+items array.
+
+```tsx
+let collator = useCollator();
+
+let list = useAsyncList({
+ async load({signal}) {
+ // Same load function as before
+ },
+ sort({items, sortDescriptor}) {
+ return {
+ items: items.sort((a, b) => {
+ // Compare the items by the sorted column
+ let cmp = collator.compare(a[sortDescriptor.column], b[sortDescriptor.column]);
+
+ // Flip the direction if descending order is specified.
+ if (sortDescriptor.direction === 'descending') {
+ cmp *= -1;
+ }
+
+ return cmp;
+ })
+ };
+ }
+});
+```
+
+### Server side sorting
+
+Server side sorting could be implemented by using the `sortDescriptor` in the `load` function, and passing a
+parameter to the API.
+
+```tsx
+let list = useAsyncList({
+ async load({signal, sortDescriptor}) {
+ let url = new URL('http://example.com/api');
+ if (sortDescriptor) {
+ url.searchParams.append('sort_key', sortDescriptor.column);
+ url.searchParams.append('sort_direction', sortDescriptor.direction);
+ }
+
+ let res = await fetch(url, {signal});
+ let json = await res.json();
+ return {
+ items: json.results
+ };
+ }
+});
+```
+
+## Filtering
+
+There are many instances where your list of data may need to be filtered, such as during user lookup or query searches.
+For server side filtering, this can be implemented by using the `filterText` option passed to the `load` function.
+The `setFilterText` method updates the current `filterText` value and triggers the `load` function. This allows
+you to reload the results with the new filter text.
+
+### Server side filtering
+
+The example below shows how server side filtering could be implemented by using `filterText` in the `load` function and passing a parameter to the API.
+The input value of the ComboBox is controlled by providing `list.filterText` as the ComboBox's `inputValue` prop, and `list.setFilterText` as the `onInputChange` prop.
+The `loadingState` prop is also used to show the appropriate loading indicator depending on the state of the list.
+
+```tsx
+let list = useAsyncList({
+ async load({signal, filterText}) {
+ let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
+ let json = await res.json();
+
+ return {
+ items: json.results
+ };
+ }
+});
+
+
+ {item => - {item.name}
}
+
+```
+
+## Pre-selecting items
+
+`useAsyncList` manages selection state for the list in addition to its data. If you need to programmatically select items
+during the initial load, you can do so using the `initialSelectedKeys` option or by returning `selectedKeys` from the
+`load` function in addition to `items`.
+
+### Selecting before loading
+
+If you know what keys to select before items are loaded from the server, use the `initialSelectedKeys` option.
+
+```tsx
+let list = useAsyncList({
+ initialSelectedKeys: ['foo', 'bar'],
+ async load({signal}) {
+ // Same load function as before
+ }
+});
+```
+
+### Selecting based on loaded data
+
+If you need to compute which keys to select based on the loaded data, return `selectedKeys` from the `load` function
+in addition to the `items`.
+
+```tsx
+let list = useAsyncList({
+ async load({signal}) {
+ let res = await fetch('http://example.com/api', {signal});
+ let json = await res.json();
+
+ // Return items and compute selectedKeys based on the data and return a list of ids.
+ return {
+ items: json.results,
+ selectedKeys: json.results.filter(item => item.isSelected).map(item => item.id)
+ };
+ }
+});
+```
+
+## Client side updates
+
+All client side updating methods supported by `useListData` are also supported by `useAsyncList`.
+See the docs for [useListData](useListData.html) for more details.
diff --git a/packages/dev/s2-docs/pages/react-aria/useListData.mdx b/packages/dev/s2-docs/pages/react-aria/useListData.mdx
new file mode 100644
index 00000000000..7ad5be94441
--- /dev/null
+++ b/packages/dev/s2-docs/pages/react-aria/useListData.mdx
@@ -0,0 +1,144 @@
+{/* Copyright 2026 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License. */}
+
+import {Layout} from '../../src/Layout';
+export default Layout;
+import {FunctionAPI} from '../../src/FunctionAPI';
+import docs from 'docs:@react-stately/data';
+
+export const section = 'Utilities';
+export const description = 'Manages an immutable list of items and its selection state.';
+
+# useListData
+
+{docs.exports.useListData.description}
+
+## Introduction
+
+React requires all data structures passed as props to be immutable. This enables them to be diffed correctly to determine
+what has changed since the last render. This can be challenging to accomplish from scratch in a performant way in JavaScript.
+
+`useListData` helps manage an immutable list data structure, with helper methods to update the data in an efficient way.
+Since the data is stored in React state, calling these methods to update the data automatically causes the component
+to re-render accordingly.
+
+In addition, `useListData` stores selection state for the list, based on unique item keys. This can be updated programmatically,
+and is automatically updated when items are removed from the list.
+
+## API
+
+
+
+## Options
+
+
+
+## Interface
+
+
+
+## Example
+
+To construct a list, pass an initial set of items along with a function to get a key for each item.
+You can use the state returned by `useListData` to render a [collection component](collections.html).
+
+This example renders a `ListBox` using the items managed by `useListData`. It uses the `name` property of each item
+as the unique key for that item, and the `items` property as the children. In addition, it manages the selection state
+for the listbox, which will automatically be updated when items are removed from the tree.
+
+```tsx
+let list = useListData({
+ initialItems: [
+ {name: 'Aardvark'},
+ {name: 'Kangaroo'},
+ {name: 'Snake'}
+ ],
+ initialSelectedKeys: ['Kangaroo'],
+ getKey: item => item.name
+});
+
+
+ {item => - {item.name}
}
+
+```
+
+### Inserting items
+
+To insert a new item into the list, use the `insert` method or one of the other convenience methods.
+Each of these methods also accepts multiple items, so you can insert multiple items at once.
+
+```tsx
+// Insert an item after the first one
+list.insert(1, {name: 'Horse'});
+
+// Insert multiple items
+list.insert(1, {name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Insert an item before another item
+list.insertBefore('Kangaroo', {name: 'Horse'});
+
+// Insert multiple items before another item
+list.insertBefore('Kangaroo', {name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Insert an item after another item
+list.insertAfter('Kangaroo', {name: 'Horse'});
+
+// Insert multiple items after another item
+list.insertAfter('Kangaroo', {name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Append an item
+list.append({name: 'Horse'});
+
+// Append multiple items
+list.append({name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Prepend an item
+list.prepend({name: 'Horse'});
+
+// Prepend multiple items
+list.prepend({name: 'Horse'}, {name: 'Giraffe'});
+```
+
+### Removing items
+
+```tsx
+// Remove an item
+list.remove('Kangaroo');
+
+// Remove multiple items
+list.remove('Kangaroo', 'Snake');
+```
+
+```tsx
+// Remove all selected items
+list.removeSelectedItems();
+```
+
+### Moving items
+
+```tsx
+list.move('Snake', 0);
+```
+
+### Updating items
+
+```tsx
+list.update('Snake', {name: 'Rattle Snake'});
+```
diff --git a/packages/dev/s2-docs/pages/react-aria/useTreeData.mdx b/packages/dev/s2-docs/pages/react-aria/useTreeData.mdx
new file mode 100644
index 00000000000..d169db78153
--- /dev/null
+++ b/packages/dev/s2-docs/pages/react-aria/useTreeData.mdx
@@ -0,0 +1,181 @@
+{/* Copyright 2026 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License. */}
+
+import {Layout} from '../../src/Layout';
+export default Layout;
+import {FunctionAPI} from '../../src/FunctionAPI';
+import docs from 'docs:@react-stately/data';
+
+export const section = 'Utilities';
+export const description = 'Manages an immutable tree of items and its selection state.';
+
+# useTreeData
+
+{docs.exports.useTreeData.description}
+
+## Introduction
+
+React requires all data structures passed as props to be immutable. This enables them to be diffed correctly to determine
+what has changed since the last render. This can be challenging to accomplish from scratch in a performant way in JavaScript.
+
+`useTreeData` helps manage an immutable tree data structure, with helper methods to update the data in an efficient way.
+Since the data is stored in React state, calling these methods to update the data automatically causes the component
+to re-render accordingly.
+
+In addition, `useTreeData` stores selection state for the tree, based on unique item keys. This can be updated programmatically,
+and is automatically updated when items are removed from the tree.
+
+## API
+
+
+
+## Options
+
+
+
+## Interface
+
+
+
+## Example
+
+To construct a tree, pass an initial set of items along with functions to get a key for each item, and its children.
+`useTreeData` processes these items into nodes, which you can use to render a [collection component](collections.html).
+Each node has `key`, `value`, and `children` properties.
+
+This example renders a `ListBox` with two sections, each with three child items. It uses the `name` property of each item
+as the unique key for that item, and the `items` property as the children. In addition, it manages the selection state
+for the listbox, which will automatically be updated when items are removed from the tree.
+
+```tsx
+interface ItemValue {
+ name: string;
+ items?: Array;
+}
+
+let tree = useTreeData({
+ initialItems: [
+ {
+ name: 'People',
+ items: [
+ {name: 'David'},
+ {name: 'Sam'},
+ {name: 'Jane'}
+ ]
+ },
+ {
+ name: 'Animals',
+ items: [
+ {name: 'Aardvark'},
+ {name: 'Kangaroo'},
+ {name: 'Snake'}
+ ]
+ }
+ ],
+ initialSelectedKeys: ['Sam', 'Kangaroo'],
+ getKey: item => item.name,
+ getChildren: item => item.items || []
+});
+
+ {
+ if (keys !== 'all') {
+ tree.setSelectedKeys(keys);
+ }
+ }}>
+ {node =>
+
+ {node => - {node.value.name}
}
+
+ }
+
+```
+
+### Inserting items
+
+To insert a new item into the tree, use the `insert` method or use one of the other convenience methods.
+Pass a `parentKey` to insert into, or `null` to insert a root item.
+
+```tsx
+// Insert an item into the root, after 'People'
+tree.insert(null, 1, {name: 'Plants'});
+
+// Insert an item into the 'People' node, after 'David'
+tree.insert('People', 1, {name: 'Judy'});
+```
+
+```tsx
+// Insert an item before another item
+tree.insertAfter('Kangaroo', {name: 'Horse'});
+
+// Insert multiple items before another item
+tree.insertAfter('Kangaroo', {name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Insert an item after another item
+tree.insertAfter('Kangaroo', {name: 'Horse'});
+
+// Insert multiple items after another item
+tree.insertAfter('Kangaroo', {name: 'Horse'}, {name: 'Giraffe'});
+```
+
+```tsx
+// Append an item to the root
+tree.append(null, {name: 'Plants'});
+
+// Append an item to the 'People' node
+tree.append('People', {name: 'Plants'});
+```
+
+```tsx
+// Prepend an item to the root
+tree.prepend(null, {name: 'Plants'});
+
+// Prepend an item at the start of the 'People' node
+tree.prepend('People', {name: 'Plants'});
+```
+
+### Removing items
+
+```tsx
+// Remove an item
+list.remove('Kangaroo');
+
+// Remove multiple items
+list.remove('Kangaroo', 'Snake');
+```
+
+```tsx
+// Remove all selected items
+list.removeSelectedItems();
+```
+
+### Moving items
+
+```tsx
+// Move an item within the same parent
+tree.move('Sam', 'People', 0);
+
+// Move an item to a different parent
+tree.move('Sam', 'Animals', 1);
+
+// Move an item to the root
+tree.move('Sam', null, 1);
+```
+
+### Updating items
+
+```tsx
+tree.update('Sam', {name: 'Samantha'});
+```