React is built around a unidirectional data flow, which makes component interactions predictable and easier to debug. Understanding how data travels through your application—from parent components to children via props, and how it can be shared globally via context—is essential for building scalable, maintainable frontend applications. With the introduction of hooks in React 16.8 and improvements in React 18, developers now have a more declarative and functional approach to managing state, effects, and context, replacing many of the patterns that previously required class components.
This article dives deep into React props, state management, hooks, and context, demonstrating practical patterns that can be used in real-world projects. You will learn not only how to pass data efficiently but also how to structure your components, share global state without prop drilling, and optimize your application for performance and maintainability. By the end, you’ll have a clearer understanding of how data flows in React applications and how to leverage modern hooks and context to write cleaner, more robust code. React hooks are the foundation of modern React development. They allow you to use state, lifecycle methods, and other features without writing class components. Let’s explore how to apply hooks effectively.
1. useState – Local State Management
useState lets you add state to functional components.
1import { useState } from 'react';
2
3export default function Toggle() {
4 const [isOn, setIsOn] = useState(false);
5
6 return (
7 <button onClick={() => setIsOn(!isOn)}>
8 {isOn ? 'ON' : 'OFF'}
9 </button>
10 );
11}
12
13Each component has its own isolated state.
2. useEffect – Side Effects
useEffect handles side effects like data fetching or subscriptions.
1
2import { useState, useEffect } from 'react';
3
4export default function Timer() {
5 const [seconds, setSeconds] = useState(0);
6
7 useEffect(() => {
8 const interval = setInterval(() => setSeconds(s => s + 1), 1000);
9 return () => clearInterval(interval); // cleanup
10 }, []);
11
12 return <p>Seconds elapsed: {seconds}</p>;
13}
14The empty dependency array [] ensures the effect runs once on mount.
3. Custom Hooks – Reusable Logic
You can extract common logic into custom hooks.
1import { useState, useEffect } from 'react';
2
3function useWindowWidth() {
4 const [width, setWidth] = useState(window.innerWidth);
5
6 useEffect(() => {
7 const handleResize = () => setWidth(window.innerWidth);
8 window.addEventListener('resize', handleResize);
9 return () => window.removeEventListener('resize', handleResize);
10 }, []);
11
12 return width;
13}
14
15// Usage
16export default function App() {
17 const width = useWindowWidth();
18
19 return <p>Window width: {width}px</p>;
20}
21Custom hooks start with use and let you share logic across components.
4. useReducer – Complex State Logic
For state that involves multiple values or complex updates, useReducer is helpful.
1import { useReducer } from 'react';
2
3const initialState = { count: 0 };
4
5function reducer(state, action) {
6 switch(action.type) {
7 case 'increment': return { count: state.count + 1 };
8 case 'decrement': return { count: state.count - 1 };
9 default: return state;
10 }
11}
12
13export default function Counter() {
14 const [state, dispatch] = useReducer(reducer, initialState);
15
16 return (
17 <div>
18 <p>Count: {state.count}</p>
19 <button onClick={() => dispatch({ type: 'increment' })}>+</button>
20 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
21 </div>
22 );
23}
24
25useReduceris similar to Redux but built into React.
Summary
In this post, we explored the foundational concepts of React’s data flow, starting with props as the simplest method of passing data down the component tree. We examined how useState allows components to maintain internal state, and how useEffect can handle side effects such as data fetching or subscriptions. Moving beyond local state, we looked at React Context, which enables global state sharing and eliminates cumbersome prop drilling, particularly in deep component trees.
We also highlighted the importance of combining hooks and context for scalable applications. Using useReducer with context, developers can manage complex state transitions in a predictable manner. Additionally, the post covered best practices for reusable and maintainable code, including custom hooks, clean component structure, and performance optimization.
Ultimately, mastering React’s data flow is about understanding how data moves, where it should live, and how to share it efficiently. By leveraging props, hooks, and context thoughtfully, developers can build applications that are not only functional but also scalable, maintainable, and performant, harnessing the full power of React 18 and modern frontend development patterns.
Happy coding!