Building a custom compositional API with React Aria
Tags: react, webdev
Published: 2024-02-07 15:37:22

**Written by [Manu Ustenko
](https://blog.logrocket.com/author/mikhailustenko/)✏️**

There are countless [UI libraries and kits for React](https://blog.logrocket.com/top-react-ui-libraries-kits/) that we can use to build our application's user interface. Some of these libraries provide built-in components with prewritten rendering and behaviors that aren’t easy to customize at lower levels. 

In this article, you will learn how to create a compositional API by extending React Aria's built-in components to create your own custom components. To follow along with this tutorial, I will assume you can create a React app using Vite and have a basic understanding of React Aria components. 

You can find the [full code for the demo project on GitHub](https://github.com/popoolatopzy/vite-project).

## What is React Aria?

React Aria is a library of React Hooks that provides accessible UI primitives for creating design systems. It was developed by Adobe to provide common UI components with accessibility and behavior using [ARIA (Accessible Rich Internet Applications)](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) patterns. 

React Aria supports multiple input modes that ensure inclusive interaction experiences for users using mice, touch, keyboards, and screen readers to navigate through components.

## Overview of React Aria’s composable API

React Aria’s built-in components are designed around composition, which makes them reusable when creating larger composite component patterns. When you need more control over the components with React Aria, it's easy to drop down to the lower-level Hook-based API to create a custom composable API. 

React Aria’s Hook-based API allows you to create compositional components with complete customization options, including the ability to change the DOM structure, access the internal state, and override event handlers to control the rendering and behavior of the component.

## When and why to extend the API

When React Aria's built-in components don't fully meet your application's requirements, extending their behaviors using a Hook-based API allows you to tailor them to your specific needs. This is particularly important when your application's UI requires unique structures or interactions that you can’t achieve with the default built-in components. 

Here are some reasons you may want to extend the React Aria Component API:

*   To create components with custom behaviors that meet your application requirements
*   To customize component rendering using your desired styles
*   To intercept a DOM event with your conditional logic
*   To override a built-in component's internal state management behavior
*   To remove unused features to reduce bundle size

## Project setup

### Creating a new React project

To get started, let's [create a new React app using the Vite CLI](https://blog.logrocket.com/build-react-typescript-app-vite/). To do this, run the command below in your terminal:

```shell
npm create vite@latest
```

Next, you will be prompted to configure the project. Use the project configuration shown below: ![Project Config for Our Vite React App](https://blog.logrocket.com/wp-content/uploads/2024/01/react-vite-app-project-config.png)

### Installing React Aria Components

Now we need to install the React Aria Components library into the project. Go ahead and do that using the command below:

```shell
npm install react-aria-components
```

After installing, open the project folder in your code editor, navigate to the `src` folder, and create a `Components` folder to store the component files.

## Creating a custom compositional API with React Aria

Now let's explore building a custom React Aria compositional API with customized rendering and behavior using contexts, slots, and consume contexts.

### Customizing using contexts

Every React Aria component has a corresponding context you can use to create a custom compositional API. For instance, we can create a custom `ButtonGroup` component marked as disabled using the `isDisabled` prop. This prop is then passed to all child buttons through the `ButtonContext` provider. 

Let's create a `ButtonGroup` component with customized rendering behavior for the child button components using context. Inside the `Components` folder, create a `ButtonGroupAPI.tsx` file and add the following code to it:

```typescript
// ButtonGroupAPI.tsx

import { ReactNode } from 'react';
import { ButtonContext, Button } from 'react-aria-components';

interface ButtonGroupProps {
  children: ReactNode;
  isDisabled?: boolean;
}

export function ButtonGroup({ children, isDisabled }: ButtonGroupProps): JSX.Element {
  return (
    <div style={{ display: 'flex', gap: 8, marginLeft:10 }}>
      <ButtonContext.Provider value={{ isDisabled }}>
        {children}
      </ButtonContext.Provider>
    </div>
  );
}
```

In the code above, we import `ButtonContext` and `Button` from `react-aria-components`, which provides us with the customization options needed to create customized components. 

Next, we define the `ButtonGroupProps` interface. This interface is used to define the props and their data types that are passed to the `ButtonGroup` component as an object. 

The `ButtonGroup` functional component takes two props, `children` and `isDisabled`, which have been defined in the `ButtonGroupProps` interface. These props are then passed to the child button component via `ButtonContext.Provider`. 

To utilize this component, open the `App.tsx` file and add the following code to it:

```typescript
import { ButtonGroup } from './Components/ButtonGroupAPI';
import { Button } from 'react-aria-components';
function App() {
  return (
    <>
      <ButtonGroup isDisabled={true}>
        <Button>Edit</Button>
        <Button>Delete</Button>
        <Button>Save</Button>
      </ButtonGroup>
      <br />
      <ButtonGroup isDisabled={false}>
        <Button >Back</Button>
        <Button >Share</Button>
        <Button>Bookmark</Button>
      </ButtonGroup>
    </>
  );
}
export default App;
```

In the code above, we import the `ButtonGroup` component from the `ButtonGroupAPI` and wrap the button components into it. Every button wrapped inside the `ButtonGroup` component receives the `isDisabled` property, as shown in the image below: ![Our Custom React Aria ButtonGroup Component with the isDisabled Prop](https://blog.logrocket.com/wp-content/uploads/2024/01/custom-react-aria-buttongroup-component.png)

### Using the slots prop instance

When there are multiple instances of the same component, the slots prop can be used to identify each instance along with its actions. A slot is a name given to child components as a prop within a parent component to receive separate behaviors and styles. 

For example, we can use slots to identify which button the user presses and customize the action associated with the corresponding button. 

Let's create a `ButtonAction` component that uses slots to identify which of its child button components is pressed and customize their behaviors. Open the `ButtonGroupAPI.tsx` file and add the following code to create the `ButtonAction` component:

```typescript
// . . .
interface ButtonActionProps {
  children: ReactNode;
}
export function ButtonAction({ children }: ButtonActionProps) {
  return (
    <ButtonContext.Provider
      value={{
        slots: {
          Edit: {
            onPress: () => (alert("Edit button pressed")),
          },
          Delete: {
            onPress: () => alert("Delete button pressed"),
          },
          Save: {
            onPress: () => alert("Save button pressed"),
          },
          Back: {
            onPress: () => alert("Back button pressed"),
          },
          Share: {
            onPress: () => alert("Share button pressed"),
          },
          Bookmark: {
            onPress: () => alert("Bookmark button pressed"),
          },
        }
      }}
    >
      {children}
    </ButtonContext.Provider>
  );
}
```

From the code above, we define the slot instances as an object with six button actions: `Edit`, `Delete`, `Save`, `Back`, `Share`, and `Bookmark`. The slot object is then passed as the value prop to the `ButtonContext.Provider,` allowing its child button components to access and utilize the corresponding slot property. 

Next, to use this component, open the `App.tsx` file and add the `ButtonAction` component using the code below:

```typescript
import { ButtonAction } from './Components/ButtonGroupAPI';
import { Button } from 'react-aria-components';
function App() {
  return (
    <>
      <ButtonAction>
        <Button slot="Back">Back</Button>
        <Button slot="Share">Share</Button>
        <Button slot="Bookmark">Bookmark</Button>
      </ButtonAction>
    </>
  );
}

export default App;
```

In the code above, we import `ButtonAction` from the `ButtonGroupAPI` component and wrap the button components within it. Each button takes its corresponding slot props that are triggered when the button is pressed, as shown in the image below: ![Displaying Use of Slot Props on Our React Aria Button Component](https://blog.logrocket.com/wp-content/uploads/2024/01/react-aria-button-slot-props.gif)

### Using consume contexts

We can also create a customized component that is compatible with React Aria's built-in components using consume contexts. Consume contexts allow us to replace a built-in component used as part of a larger pattern with a custom implementation. 

Let’s create a custom `MyCustomLabel` component that is compatible with React Aria's built-in components using `LabelContext`. To do so, open the `ButtonGroupAPI.tsx` file and add the following component function:

```typescript
// ButtonGroupAPI.tsx

import React,{ ReactNode } from 'react';
import { ButtonContext } from 'react-aria-components';
import { LabelProps,LabelContext, useContextProps } from 'react-aria-components';

//...

export function MyCustomLabel(props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) {
  [props, ref] = useContextProps(props, ref, LabelContext);

  return <label  {...props} ref={ref}style={{}} />;
}
```

In the code above, we import `LabelProps`, `LabelContext`, and `useContextProps` from `react-aria-components`. Then, we create the `MyCustomLabel` component, which takes props and ref properties that are part of the built-in label properties. 

Now we can utilize the `MyCustomLabel` component in place of the built-in label component. To use it, head back over to the `App.tsx` file, import `MyCustomLabel`, and add the following code to it:

```typescript
<TextField>
  <MyCustomLabel>Enter Username</MyCustomLabel>
  <Input />
</TextField>
```

### Using states

When customizing a group component, like the `Calendar` component, we can use the corresponding `StateContext` to manipulate and exchange state within its child components. For instance, `CalendarStateContext` allows us to access and manipulate the state of the calendar within its child components. 

Let’s create a `CalendarValue` component that returns the selected date in a `Calendar` component using `CalendarStateContext`. To do that, open the `App.tsx` file and add the following code:

```typescript
import { CalendarStateContext } from 'react-aria-components';
import { useDateFormatter } from 'react-aria';
import { getLocalTimeZone } from '@internationalized/date';
//...
export function CalendarValue() {
  let state = React.useContext(CalendarStateContext)!;
  let date = state.value?.toDate(getLocalTimeZone());
  let formatted = date ? useDateFormatter().format(date) : 'None';
  return <small>Selected date: {formatted}</small>;
}
```

In the code above, we create a `CalendarValue` component that utilizes the `CalendarStateContext` to retrieve the selected date from a `Calendar` component and then converts the selected date to the local time zone format using a date formatted. 

To use the component, wrap it inside the `Calendar` component. Open the `App.tsx` file and add the following code:

```typescript
     <Calendar aria-label="Appointment date">
       <header>
         <Button slot="previous">◀</Button>
         <Heading />
         <Button slot="next">▶</Button>
       </header>
       <CalendarGrid>{(date) => <CalendarCell date={date} />}</CalendarGrid>
       <CalendarValue />
     </Calendar>
```

We import `CalendarValue` and wrap it in the `CalendarGroup` component. When the user selects a date, the selected date is displayed in the `CalendarValue` component, as shown in the image below: ![Displaying Our Custom React Aria CalendarValue Component](https://blog.logrocket.com/wp-content/uploads/2024/01/react-aria-calendarvalue-component.gif)

## Conclusion

In this article, we learned how to create a customized compositional API by extending React Aria's built-in components. This approach allows us to create components with the desired rendering and behaviors that align with the application requirements. 

[React Aria Components](https://react-spectrum.adobe.com/react-aria/react-aria-components.html#what-is-react-aria-components) is a library of unstyled components built using a flexible and composable API, which can be used to create your new patterns. You can visit the [React Aria documentation](https://react-spectrum.adobe.com/react-aria/advanced.html) to learn more about the advanced customization options for React Aria Components, and again, you can find the full code for this tutorial [on my GitHub](https://github.com/popoolatopzy/vite-project).

---

##Get set up with LogRocket's modern React error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)