Skip to content

Latest commit

 

History

History
325 lines (272 loc) · 10.2 KB

File metadata and controls

325 lines (272 loc) · 10.2 KB

import {Layout} from '../../src/Layout'; export default Layout;

import docs from 'docs:react-aria-components'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2'

export const section = 'Guides'; export const description = 'Implementing collections in React Aria';

Collections

Many components display a collection of items, and provide functionality such as keyboard navigation, selection, and more. React Aria has a consistent, compositional API to define the items displayed in these components.

Static collections

A static collection is a collection that does not change over time (e.g. hard coded). This is common for components like menus where the items are built into the application rather than user data.

"use client";
import {MenuButton, MenuItem} from 'vanilla-starter/Menu';

<MenuButton label="Menu">
  <MenuItem>Open</MenuItem>
  <MenuItem>Edit</MenuItem>
  <MenuItem>Delete</MenuItem>
</MenuButton>

Sections

Sections or groups of items can be constructed by wrapping the items in a section element. A <Header> can also be rendered within a section to provide a section title.

<ExampleSwitcher type="component" examples={['Menu', 'ListBox']}>

"use client";
import {MenuButton, MenuItem, MenuSection} from 'vanilla-starter/Menu';
import {Header} from 'react-aria-components';

<MenuButton label="Menu">
  {/*- begin highlight -*/}
  <MenuSection>
    <Header>Styles</Header>
    {/*- end highlight -*/}
    <MenuItem>Bold</MenuItem>
    <MenuItem>Underline</MenuItem>
  </MenuSection>
  <MenuSection>
    <Header>Align</Header>
    <MenuItem>Left</MenuItem>
    <MenuItem>Middle</MenuItem>
    <MenuItem>Right</MenuItem>
  </MenuSection>
</MenuButton>
"use client";
import {ListBox, ListBoxItem, ListBoxSection} from 'vanilla-starter/ListBox';
import {Header} from 'react-aria-components';

<ListBox aria-label="Text style" selectionMode="multiple">
  {/*- begin highlight -*/}
  <ListBoxSection>
    <Header>Styles</Header>
    {/*- end highlight -*/}
    <ListBoxItem>Bold</ListBoxItem>
    <ListBoxItem>Underline</ListBoxItem>
  </ListBoxSection>
  <ListBoxSection>
    <Header>Align</Header>
    <ListBoxItem>Left</ListBoxItem>
    <ListBoxItem>Middle</ListBoxItem>
    <ListBoxItem>Right</ListBoxItem>
  </ListBoxSection>
</ListBox>

Dynamic collections

A dynamic collection is a collection that is based on external data, for example from an API. In addition, it may change over time as items are added, updated, or removed from the collection by a user.

Use the items prop to provide an array of objects, and a function to render each item as the children. This is similar to using array.map to render the children, but automatically memoizes the rendering of each item to improve performance.

"use client";
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';
import {Button} from 'vanilla-starter/Button';
import {useState} from 'react';

function Example() {
  let [animals, setAnimals] = useState([
    {id: 1, name: 'Aardvark'},
    {id: 2, name: 'Kangaroo'},
    {id: 3, name: 'Snake'}
  ]);

  let addItem = () => {
    setAnimals([
      ...animals,
      {id: animals.length + 1, name: 'Lion'}
    ]);
  };

  return (
    <div>
      {/*- begin highlight -*/}
      <ListBox items={animals}>
        {item => <ListBoxItem>{item.name}</ListBoxItem>}
      </ListBox>
      {/*- end highlight -*/}
      <Button onPress={addItem} style={{marginTop: 8}}>Add item</Button>
    </div>
  );
}
useListData For convenience, React Aria provides a built-in [useListData](https://react-spectrum.adobe.com/react-stately/useListData.html) hook to manage state for an immutable list of items. It includes methods to add, remove, update, and re-order items, and manage corresponding selection state. See the docs for more details.

Unique ids

All items in a collection must have a unique id, which is used for selection and to track item updates. By default, React Aria looks for an id property on each object in the items array. You can also specify an id prop when rendering each item. This example uses item.name as the id.

let animals = [
  {name: 'Aardvark'},
  {name: 'Kangaroo'},
  {name: 'Snake'}
];

<ListBox items={animals}>
  {item => (
    /*- begin highlight -*/
    <ListBoxItem id={item.name}>
    {/*- end highlight -*/}
      {item.name}
    </ListBoxItem>
  )}
</ListBox>
React keys React Aria automatically sets the React `key` using `id`. If using `array.map`, you'll need to set both `key` and `id`.

Dependencies

Dynamic collections are automatically memoized to improve performance. Rendered item elements are cached based on the object identity of the list item. If rendering an item depends on additional external state, the dependencies prop must be provided. This invalidates rendered elements similar to dependencies in React's useMemo hook.

"use client";
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';
import {Text} from 'react-aria-components';
import {ToggleButtonGroup} from 'vanilla-starter/ToggleButtonGroup';
import {ToggleButton} from 'vanilla-starter/ToggleButton';
import {useState} from 'react';

const items = [
  {id: 1, name: 'Charizard', type: 'Fire, Flying'},
  {id: 2, name: 'Blastoise', type: 'Water'},
  {id: 3, name: 'Venusaur', type: 'Grass, Poison'},
  {id: 4, name: 'Pikachu', type: 'Electric'}
];

export default function Example() {
  let [layout, setLayout] = useState('vertical');

  return (
    <div>
      <ToggleButtonGroup
        aria-label="Layout"
        selectedKeys={[layout]}
        onSelectionChange={keys => setLayout([...keys][0])}
        disallowEmptySelection
        style={{marginBottom: 8}}>
        <ToggleButton id="vertical">Vertical</ToggleButton>
        <ToggleButton id="horizontal">Horizontal</ToggleButton>
      </ToggleButtonGroup>
      <ListBox
        items={items}
        selectionMode="multiple"
        /*- begin highlight -*/
        dependencies={[layout]}>
        {/*- end highlight -*/}
        {item => (
          <MyItem
            /*- begin highlight -*/
            layout={layout}
            /*- end highlight -*/
            name={item.name}
            type={item.type} />
        )}
      </ListBox>
    </div>
  );
}

interface MyItemProps {
  layout: 'horizontal' | 'vertical',
  name: string,
  type: string
}

function MyItem(props: MyItemProps) {
  return (
    <ListBoxItem
      style={{
        display: 'flex',
        flexDirection: props.layout === 'horizontal' ? 'row' : 'column',
        justifyContent: 'space-between',
        alignItems: props.layout === 'horizontal' ? 'center' : 'start'
      }}>
      <Text slot="label">{props.name}</Text>
      <Text slot="description">{props.type}</Text>
    </ListBoxItem>
  );
}

Note that adding dependencies will result in the entire list being invalidated when a dependency changes. To avoid this and invalidate only an individual item, update the item object itself rather than accessing external state.

Combining collections

To combine multiple sources of data, or mix static and dynamic items, use the <Collection> component.

"use client";
import {ListBox, ListBoxSection, ListBoxItem} from 'vanilla-starter/ListBox';
import {Collection, Header} from 'react-aria-components';

let animals = [
  {id: 1, species: 'Aardvark'},
  {id: 2, species: 'Kangaroo'},
  {id: 3, species: 'Snake'}
];

let people = [
  {id: 4, name: 'David'},
  {id: 5, name: 'Mike'},
  {id: 6, name: 'Jane'}
];

<ListBox>
  <ListBoxSection>
    <Header>Animals</Header>
    {/*- begin highlight -*/}
    <Collection items={animals}>
      {item => <ListBoxItem id={item.species}>{item.species}</ListBoxItem>}
    </Collection>
    {/*- end highlight -*/}
  </ListBoxSection>
  <ListBoxSection>
    <Header>People</Header>
    <Collection items={people}>
      {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
    </Collection>
  </ListBoxSection>
</ListBox>
Globally unique ids Unlike React keys which must only be unique within each element, the `id` prop must be globally unique across all sections. In the above example, the ids of `animals` and `people` do not conflict.

Asynchronous loading

Data can be loaded asynchronously using any data fetching library. useAsyncList is a built-in option.

Several components also support infinite scrolling by rendering a LoadMoreItem at the end of the list. These trigger loading of additional pages of items and display a loading spinner. Multiple load more items can be rendered at once, e.g. when loading multiple levels of a tree or sections in a list.

"use client";
import {Collection, ListBoxLoadMoreItem} from 'react-aria-components';
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';
import {ProgressCircle} from 'vanilla-starter/ProgressCircle';
import {useAsyncList} from 'react-stately';

interface Character {
  name: string
}

function AsyncLoadingExample() {
  /*- begin focus -*/
  let list = useAsyncList<Character>({
    async load({signal, cursor}) {
      let res = await fetch(
        cursor || `https://pokeapi.co/api/v2/pokemon`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });
  /*- end focus -*/

  return (
    <ListBox
      aria-label="Pick a Pokemon"
      selectionMode="single"
      renderEmptyState={() => (
        <ProgressCircle isIndeterminate aria-label="Loading..." />
      )}>
      <Collection items={list.items}>
        {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>}
      </Collection>
      <ListBoxLoadMoreItem
        onLoadMore={list.loadMore}
        isLoading={list.loadingState === 'loadingMore'}>
        <ProgressCircle isIndeterminate aria-label="Loading more..." />
      </ListBoxLoadMoreItem>
    </ListBox>
  );
}