Skip to content

Commit 99d8920

Browse files
committed
add examples
1 parent 405dbd1 commit 99d8920

1 file changed

Lines changed: 148 additions & 2 deletions

File tree

rfcs/2026-async-react.md

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Components with state will use `useOptimistic` to update immediately in response
3333

3434
To implement this, we can create a new hook that wraps `useControlledState` and also supports action props. When the value setter is called, we start a transition, set the optimistic value, and trigger the change action. We will also continue emit the `onChange` event and support both controlled and uncontrolled state.
3535

36-
We could also potentially catch errors that are thrown by actions and expose these as render props, enabling [in-line contextual error UIs](https://x.com/devongovett/status/1989788456751697958).
36+
We will also catch errors that are thrown by actions and expose them as an `actionError` render prop, or via the `FieldError` component, enabling [in-line contextual error UIs](https://x.com/devongovett/status/1989788456751697958). This will help reduce over-reliance on toasts as a catch-all way of handling errors in applications by making inline errors just as easy to implement.
3737

38-
All together, this significantly simplifies the implementation of loading states for component libraries and applications. Simply render a `<ProgressBar>` when `isPending` is true, add an async function as an action prop, and React Aria handles the rest.
38+
All together, this significantly simplifies the implementation of loading states and error handling for component libraries and applications. Simply render a `<ProgressBar>` when `isPending` is true, add an async function as an action prop, and React Aria handles the rest.
3939

4040
Here's a potential list of components that could support actions:
4141

@@ -50,6 +50,7 @@ Here's a potential list of components that could support actions:
5050
* DatePicker - `changeAction`
5151
* DateRangePicker - `changeAction`
5252
* Disclosure - `expandAction`
53+
* Form – `submitAction` (add a `FormError` component to display form-level errors)
5354
* NumberField - `changeAction`
5455
* RadioGroup - `changeAction`
5556
* SearchField - `changeAction`, `submitAction`, `clearAction`
@@ -61,6 +62,151 @@ Here's a potential list of components that could support actions:
6162
* TimeField - `changeAction`
6263
* ToggleButton - `changeAction`
6364

65+
### Examples
66+
67+
#### Pending button
68+
69+
A save button that displays a spinner while an action is running (e.g. calling a server). Pending buttons are not interactive (but remain focusable).
70+
71+
```tsx
72+
function App() {
73+
return (
74+
<Button
75+
action={async () => {
76+
await save();
77+
}}>
78+
{({isPending}) => (
79+
<>
80+
{isPending
81+
? <ProgressBar aria-label="Saving" isIndeterminate />
82+
: 'Save'}
83+
</>
84+
)}
85+
</Button>
86+
);
87+
}
88+
```
89+
90+
#### Async search results
91+
92+
A search field for a filterable list. Typing in the field causes a state update, and the results list suspends. While the results are loading, the search field displays a spinner and the previous results display in the list and remain interactive.
93+
94+
This illustrates that the pending state may display for longer than the action itself if another part of the UI suspends as a result. The `Suspense` wrapping the result list only displays its fallback during the initial load sequence, not when the update is triggered by an action (this is React's default behavior for transitions).
95+
96+
```tsx
97+
function App() {
98+
let [search, setSearch] = useState('');
99+
100+
return (
101+
<>
102+
<SearchField
103+
value={search}
104+
changeAction={value => setSearch(value)}>
105+
{({isPending}) => (
106+
<>
107+
<Label>Search</Label>
108+
<Input />
109+
{isPending && <ProgressBar aria-label="Saving" isIndeterminate />}
110+
</>
111+
)}
112+
</SearchField>
113+
<React.Suspense fallback="Initial loading state">
114+
<ResultList search={search} />
115+
</React.Suspense>
116+
</>
117+
);
118+
}
119+
```
120+
121+
#### Error handling
122+
123+
If an error occurs in an action, it is available via the `actionError` render prop. This button has a shake animation when an error occurs, and displays an error icon.
124+
125+
```tsx
126+
function App() {
127+
return (
128+
<Button
129+
action={async () => {
130+
await save();
131+
}}
132+
style={({actionError}) => ({
133+
animation: actionError ? `shake 1s` : undefined
134+
})}>
135+
{({isPending, actionError}) => (
136+
<>
137+
{actionError && <ErrorIcon aria-label="Error" />}
138+
{isPending
139+
? <ProgressBar aria-label="Saving" isIndeterminate />
140+
: 'Save'}
141+
</>
142+
)}
143+
</Button>
144+
);
145+
}
146+
```
147+
148+
If you didn't want the Button itself to handle errors and wanted to show errors in a different way, you could add a try/catch statement within the action and catch the error there.
149+
150+
```diff
151+
action={async () => {
152+
+ try {
153+
await save();
154+
+ } catch (err) {
155+
+ showToast(err);
156+
+ }
157+
}}
158+
```
159+
160+
In field components, we could also use the existing `FieldError` to show action errors. In this example, if saving a setting failed, the error would be displayed below the checkbox.
161+
162+
```tsx
163+
function App() {
164+
return (
165+
<CheckboxField
166+
changeAction={async (isSelected) => {
167+
try {
168+
await saveSetting(isSelected);
169+
} catch {
170+
throw 'Failed to save setting.';
171+
}
172+
}}>
173+
<CheckboxButton>Setting</CheckboxButton>
174+
<FieldError />
175+
</CheckboxField>
176+
);
177+
}
178+
```
179+
180+
**Note**: Errors are only caught when they occur within the action itself, not if another component suspends as a result. This makes sense from a UI perspective: if an error occurred while loading something, it should display where the results would have been (via an error boundary). If it occurred while saving something, it should display where the action was initiated.
181+
182+
#### In a design system
183+
184+
In a design system such as Spectrum, the loading and error states in the above examples would be built-in. This means application code does not need to worry these states at all.
185+
186+
```tsx
187+
import {Button, Checkbox} from '@react-spectrum/s2';
188+
189+
function App() {
190+
return (
191+
<>
192+
<Button action={async () => await doIt()}>
193+
Do something
194+
</Button>
195+
<Checkbox
196+
changeAction={async (isSelected) => {
197+
try {
198+
await saveSetting(isSelected);
199+
} catch {
200+
throw 'Failed to save setting.';
201+
}
202+
}}>
203+
Setting
204+
</Checkbox>
205+
</>
206+
);
207+
}
208+
```
209+
64210
## Documentation
65211

66212
We'll add new examples to our documentation showing how to use action props, and add pending states to components in our starter kits.

0 commit comments

Comments
 (0)