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'}); +```