- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Writing Readable React Components: Six Habits, One Messy Example
About Post
Every React codebase has one. A component called something like Page or Dashboard, 400 lines long, with eleven useState calls, three useEffects that depend on each other, and props named data, flag and cb.
Nobody wrote it like that on purpose. It grew, one "quick addition" at a time, until changing anything felt like defusing a bomb.
Readable components aren't about style preferences. They're about how fast the next person (often you, three months later) can answer "where does this value come from, and what happens if I change it?" Let's take a typical messy component and fix it habit by habit.
The starting point
Here's a trimmed-down version of the kind of component I mean, a list of maintenance requests:
export default function Page({ data, user, flag, cb }) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('open');
const [count, setCount] = useState(0);
const [modalOpen, setModalOpen] = useState(false);
useEffect(() => {
fetch(`/api/users/${user.id}/maintenance-requests`)
.then((r) => r.json())
.then((d) => { setItems(d); setLoading(false); });
}, [user.id]);
useEffect(() => {
setCount(items.filter((i) => i.status === filter).length);
}, [items, filter]);
if (loading) return <Spinner />;
return <div>{/* 150 more lines of tabs, rows and a modal */}</div>;
}
It works. It's just hard to read, hard to test and easy to break.
Habit 1: names that say what things are
Page, data, flag and cb force every reader to go and find out. A few conventions remove the guesswork:
- Components are named for what they show:
MaintenanceRequestsPage,RequestRow. - Booleans read as a yes/no question:
isReadOnly,hasUnread,canApprove. - Callback props start with
on(onSelect), and the functions you pass to them start withhandle(handleSelect). - Data props are named for the data, not its shape:
requests, notitemsorlist.
And watch for boolean prop explosions. <Button primary small danger outline /> allows combinations that make no sense. <Button variant="danger" size="sm" /> doesn't.
Habit 2: derive, don't sync
That count state and the effect that updates it are a classic. Any value you can calculate from props or other state should simply be calculated during render:
const visibleRequests = requests.filter((r) => r.status === status);
const visibleCount = visibleRequests.length;
One less piece of state, one less effect, and no render where the count is briefly out of date. If the calculation is genuinely expensive, wrap it in useMemo. Otherwise, don't bother.
Habit 3: move logic into custom hooks
The component shouldn't have to know how requests are fetched. That's a job for a custom hook, which gives the logic a name and a single home:
function useMaintenanceRequests(userId) {
const [state, setState] = useState({ requests: [], isLoading: true, error: null });
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}/maintenance-requests`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((requests) => !ignore && setState({ requests, isLoading: false, error: null }))
.catch((error) => !ignore && setState({ requests: [], isLoading: false, error }));
return () => { ignore = true; };
}, [userId]);
return state;
}
This is simplified. In a real app I'd usually let a data-fetching library such as TanStack Query handle caching and retries. The nice thing is that the hook hides that decision: swap the inside later and no component has to change.
Habit 4: split by responsibility
A useful test: describe what the component does in one sentence. If the sentence contains "and" more than once, it's probably several components. Our page fetches requests and shows filter tabs and renders rows and manages a modal.
After habits 1 to 4, the page reads almost like a sentence:
export default function MaintenanceRequestsPage({ userId }) {
const { requests, isLoading, error } = useMaintenanceRequests(userId);
const [status, setStatus] = useState('open');
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
const visibleRequests = requests.filter((r) => r.status === status);
return (
<>
<RequestFilterTabs value={status} onChange={setStatus} />
<RequestList requests={visibleRequests} emptyText="No requests here yet" />
</>
);
}
Notice the early returns for loading and error. They keep the main return focused on the normal case, instead of burying it in nested ternaries.
Habit 5: keep state close to where it's used
Where did modalOpen go? It moved into RequestRow, because that's the only place that opens the modal. State at the top of a page means every change re-renders the whole page and every reader has to trace it down through props.
The rule: put state as low as possible, and lift it only as high as the closest component that really needs it.
Habit 6: avoid prop drilling, in the right order
Prop drilling is passing a prop through components that don't use it, just to reach one that does. The first fix isn't context. It's composition: let the parent pass the finished element instead of the raw data.
// Drilling: Layout never uses user, it only passes it on
<Layout user={user} />
// Composition: Layout just places what it's given
<Layout header={<UserMenu user={user} />}>
<Dashboard />
</Layout>
Reach for context when data is genuinely needed in many places at different depths: the current user, theme, locale, permissions. Not for everything, because every context consumer re-renders when the value changes, and "where does this come from?" gets harder to answer.
A quick readability check: can someone understand what a component renders by reading only its return statement and the names above it? If they need to read the effects to know what's on screen, something can be named, derived or extracted.
The checklist
- Names that say what things are;
is/has/canfor booleans,onfor callbacks. - Derive values during render instead of syncing them with effects.
- Logic in custom hooks, rendering in components.
- One responsibility per component; early returns for loading and errors.
- State as low as possible.
- Composition before context; context before a global store.
The React docs have a great page on this, You Might Not Need an Effect, which I'd recommend to anyone who writes React daily.
What's the first thing you refactor when you open a messy component? For me it's always the effects that sync state.

Be first to comment it...