Skip to content

Commit d9f72f0

Browse files
committed
Add error handling example.
1 parent 059eac0 commit d9f72f0

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed

src/content/reference/react/useActionState.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,190 @@ For example, if the Action performs a mutation (like writing to a database), abo
12071207
12081208
</Pitfall>
12091209
1210+
---
1211+
1212+
### Handling errors {/*handling-errors*/}
1213+
1214+
There are two ways to handle errors with `useActionState`.
1215+
1216+
For known errors, such as "quantity not available" validation errors from your backend, you can return it as part of your `reducerAction` state and display it in the UI.
1217+
1218+
For unknown errors, such as `undefined is not a function`, you can throw an error. React will cancel all queued Actions and shows the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary) by rethrowing the error from the `useActionState` hook.
1219+
1220+
<Sandpack>
1221+
1222+
```js src/App.js
1223+
import {useActionState, startTransition} from 'react';
1224+
import {ErrorBoundary} from 'react-error-boundary';
1225+
import {addToCart} from './api';
1226+
import Total from './Total';
1227+
1228+
function Checkout() {
1229+
const [state, dispatchAction, isPending] = useActionState(
1230+
async (prevState, quantity) => {
1231+
const result = await addToCart(prevState.count, quantity);
1232+
if (result.error) {
1233+
// Return the error from the API as state
1234+
return {...prevState, error: `Could not add quanitiy ${quantity}: ${result.error}`};
1235+
}
1236+
1237+
if (!isPending) {
1238+
// Clear the error state for the first dispatch.
1239+
return {count: result.count, error: null};
1240+
}
1241+
1242+
// Return the new count, and any errors that happened.
1243+
return {count: result.count, error: prevState.error};
1244+
1245+
1246+
},
1247+
{
1248+
count: 0,
1249+
error: null,
1250+
}
1251+
);
1252+
1253+
function handleAdd(quantity) {
1254+
startTransition(() => {
1255+
dispatchAction(quantity);
1256+
});
1257+
}
1258+
1259+
return (
1260+
<div className="checkout">
1261+
<h2>Checkout</h2>
1262+
<div className="row">
1263+
<span>Eras Tour Tickets</span>
1264+
<span>
1265+
{isPending && '🌀 '}Qty: {state.count}
1266+
</span>
1267+
</div>
1268+
<div className="buttons">
1269+
<button onClick={() => handleAdd(1)}>Add 1</button>
1270+
<button onClick={() => handleAdd(10)}>Add 10</button>
1271+
<button onClick={() => handleAdd(NaN)}>Add NaN</button>
1272+
</div>
1273+
{state.error && <div className="error">{state.error}</div>}
1274+
<hr />
1275+
<Total quantity={state.count} isPending={isPending} />
1276+
</div>
1277+
);
1278+
}
1279+
1280+
1281+
1282+
export default function App() {
1283+
return (
1284+
<ErrorBoundary
1285+
fallbackRender={({resetErrorBoundary}) => (
1286+
<div className="checkout">
1287+
<h2>Something went wrong</h2>
1288+
<p>The action could not be completed.</p>
1289+
<button onClick={resetErrorBoundary}>Try again</button>
1290+
</div>
1291+
)}>
1292+
<Checkout />
1293+
</ErrorBoundary>
1294+
);
1295+
}
1296+
```
1297+
1298+
```js src/Total.js
1299+
const formatter = new Intl.NumberFormat('en-US', {
1300+
style: 'currency',
1301+
currency: 'USD',
1302+
minimumFractionDigits: 0,
1303+
});
1304+
1305+
export default function Total({quantity, isPending}) {
1306+
return (
1307+
<div className="row total">
1308+
<span>Total</span>
1309+
<span>
1310+
{isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}
1311+
</span>
1312+
</div>
1313+
);
1314+
}
1315+
```
1316+
1317+
```js src/api.js hidden
1318+
export async function addToCart(count, quantity) {
1319+
await new Promise((resolve) => setTimeout(resolve, 1000));
1320+
if (quantity > 5) {
1321+
return {error: 'Quantity not available'};
1322+
} else if (isNaN(quantity)) {
1323+
throw new Error('Quantity must be a number');
1324+
}
1325+
return {count: count + quantity};
1326+
}
1327+
```
1328+
1329+
```css
1330+
.checkout {
1331+
display: flex;
1332+
flex-direction: column;
1333+
gap: 12px;
1334+
padding: 16px;
1335+
border: 1px solid #ccc;
1336+
border-radius: 8px;
1337+
font-family: system-ui;
1338+
}
1339+
1340+
.checkout h2 {
1341+
margin: 0 0 8px 0;
1342+
}
1343+
1344+
.row {
1345+
display: flex;
1346+
justify-content: space-between;
1347+
align-items: center;
1348+
}
1349+
1350+
.total {
1351+
font-weight: bold;
1352+
}
1353+
1354+
hr {
1355+
width: 100%;
1356+
border: none;
1357+
border-top: 1px solid #ccc;
1358+
margin: 4px 0;
1359+
}
1360+
1361+
button {
1362+
padding: 8px 16px;
1363+
cursor: pointer;
1364+
}
1365+
1366+
.buttons {
1367+
display: flex;
1368+
gap: 8px;
1369+
}
1370+
1371+
.error {
1372+
color: red;
1373+
font-size: 14px;
1374+
}
1375+
```
1376+
1377+
```json package.json hidden
1378+
{
1379+
"dependencies": {
1380+
"react": "19.0.0",
1381+
"react-dom": "19.0.0",
1382+
"react-scripts": "^5.0.0",
1383+
"react-error-boundary": "4.0.3"
1384+
},
1385+
"main": "/index.js"
1386+
}
1387+
```
1388+
1389+
</Sandpack>
1390+
1391+
In this example, "Add 10" simulates an API that returns a validation error, which `updateCartAction` stores in state and displays inline. "Add NaN" results in an invalid count, so `updateCartAction` throws, which propagates through `useActionState` to the `ErrorBoundary` and shows a reset UI.
1392+
1393+
12101394
---
12111395
12121396
## Troubleshooting {/*troubleshooting*/}

0 commit comments

Comments
 (0)