React · Build and review an expense interface · 40 MIN
Project · Handle stale requests and failed responses
A response belongs to the request that created it.
Changing the month while a request is in flight can cause an older response to overwrite newer data. The cleanup both aborts the request and marks its result as obsolete. State includes the request month so a render after a prop change does not display the previous month as current. React development mode may exercise effect setup and cleanup more than once; cleanup should remain correct. The endpoint must scope data to the signed-in owner on the server. This component expects that endpoint and does not expose a service token to the browser.
The interface is a projection of state. Make each transition explicit so you can explain it.
Read the example
'use client';
import {useEffect,useState} from 'react';
type Expense={id:string;label:string;cents:number};
function parseRows(value:unknown):Expense[] {
if(!Array.isArray(value)||!value.every(x=>x&&typeof x==='object'&&typeof x.id==='string'&&typeof x.label==='string'&&Number.isSafeInteger(x.cents))) throw new Error('Unexpected response');
return value;
}
export default function ExpenseFeed({month}:{month:string}) {
const [state,setState]=useState<{month:string;rows:Expense[];status:'loading'|'ready'|'error'}>({month,rows:[],status:'loading'});
useEffect(()=>{
const controller=new AbortController();let current=true;
setState({month,rows:[],status:'loading'});
fetch('/api/expenses?month='+encodeURIComponent(month),{signal:controller.signal,credentials:'same-origin'})
.then(r=>{if(!r.ok)throw new Error('Request failed');return r.json()})
.then(parseRows).then(rows=>{if(current)setState({month,rows,status:'ready'})})
.catch(()=>{if(current)setState({month,rows:[],status:'error'})});
return ()=>{current=false;controller.abort()};
},[month]);
if(state.month!==month||state.status==='loading')return <p role="status">Loading expenses…</p>;
if(state.status==='error')return <p role="alert">Could not load expenses.</p>;
return state.rows.length?<ul>{state.rows.map(e=><li key={e.id}>{e.label}: {e.cents}</li>)}</ul>:<p>No expenses this month.</p>;
}Check the expected output
Render this component in a React application; verify the checklist below.
Your challenge
Connect an owner-authorized expense endpoint, then deploy your React application. Provide screenshots or a recorded walkthrough of loading, empty, error and populated states, plus a README covering setup and the API contract.
Solution cost: Rendering grows with the number of visible items. Each reducer update below copies its collection. time · O(n) for n records, excluding framework internals. space
Common trap
A component rendering successfully does not prove its data or accessibility behavior.
Study the project implementation
'use client';
import {useEffect,useState} from 'react';
type Expense={id:string;label:string;cents:number};
function parseRows(value:unknown):Expense[] {
if(!Array.isArray(value)||!value.every(x=>x&&typeof x==='object'&&typeof x.id==='string'&&typeof x.label==='string'&&Number.isSafeInteger(x.cents))) throw new Error('Unexpected response');
return value;
}
export default function ExpenseFeed({month}:{month:string}) {
const [state,setState]=useState<{month:string;rows:Expense[];status:'loading'|'ready'|'error'}>({month,rows:[],status:'loading'});
useEffect(()=>{
const controller=new AbortController();let current=true;
setState({month,rows:[],status:'loading'});
fetch('/api/expenses?month='+encodeURIComponent(month),{signal:controller.signal,credentials:'same-origin'})
.then(r=>{if(!r.ok)throw new Error('Request failed');return r.json()})
.then(parseRows).then(rows=>{if(current)setState({month,rows,status:'ready'})})
.catch(()=>{if(current)setState({month,rows:[],status:'error'})});
return ()=>{current=false;controller.abort()};
},[month]);
if(state.month!==month||state.status==='loading')return <p role="status">Loading expenses…</p>;
if(state.status==='error')return <p role="alert">Could not load expenses.</p>;
return state.rows.length?<ul>{state.rows.map(e=><li key={e.id}>{e.label}: {e.cents}</li>)}</ul>:<p>No expenses this month.</p>;
}Further reading: Official React documentation