
ReactJS
As applications scale, choosing where data lives becomes increasingly important. Advanced architectures leverage props for config, local state for UI, context for shared dependencies, and external stores for complex global state.
Unidirectional data flow is React's core principle. Data flows down through props, and changes flow up through callbacks. This pattern scales beautifully when applied consistently.
At scale, maintaining unidirectional data flow prevents circular dependencies and makes state mutations predictable. Every component knows where its data comes from and has a clear way to communicate changes upward.
// Multi-level component hierarchy with clear data flow
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
title: string;
content: string;
authorId: string;
}
// Top-level container manages all state
function BlogContainer() {
const [posts, setPosts] = useState<Post[]>([]);
const [users, setUsers] = useState<Map<string, User>>(new Map());
const [selectedPostId, setSelectedPostId] = useState<string | null>(null);
const selectedPost = selectedPostId
? posts.find((p) => p.id === selectedPostId)
: null;
const handleAddPost = async (post: Omit<Post, "id">) => {
const newPost = { ...post, id: Date.now().toString() };
setPosts((p) => [...p, newPost]);
};
const handleDeletePost = async (postId: string) => {
setPosts((p) => p.filter((post) => post.id !== postId));
if (selectedPostId === postId) setSelectedPostId(null);
};
const handleSelectPost = (postId: string) => {
setSelectedPostId(postId);
};
// Data flows down, events flow up
return (
<div>
<PostList
posts={posts}
selectedId={selectedPostId}
onSelect={handleSelectPost}
onDelete={handleDeletePost}
onAddPost={handleAddPost}
/>
{selectedPost && (
<PostDetail
post={selectedPost}
onDelete={() => handleDeletePost(selectedPost.id)}
/>
)}
</div>
);
}
interface PostListProps {
posts: Post[];
selectedId: string | null;
onSelect: (postId: string) => void;
onDelete: (postId: string) => Promise<void>;
onAddPost: (post: Omit<Post, "id">) => Promise<void>;
}
function PostList({
posts,
selectedId,
onSelect,
onDelete,
}: PostListProps) {
return (
<div>
{posts.map((post) => (
<PostItem
key={post.id}
post={post}
isSelected={selectedId === post.id}
onSelect={() => onSelect(post.id)}
onDelete={() => onDelete(post.id)}
/>
))}
</div>
);
}
interface PostItemProps {
post: Post;
isSelected: boolean;
onSelect: () => void;
onDelete: () => Promise<void>;
}
function PostItem({ post, isSelected, onSelect, onDelete }: PostItemProps) {
const [deleting, setDeleting] = useState(false);
const handleDelete = async () => {
setDeleting(true);
try {
await onDelete();
} finally {
setDeleting(false);
}
};
return (
<div
onClick={onSelect}
style={{
padding: "12px",
backgroundColor: isSelected ? "#e0e7ff" : "white",
}}
>
<h3>{post.title}</h3>
<button onClick={handleDelete} disabled={deleting}>
{deleting ? "Deleting..." : "Delete"}
</button>
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Form state management at different levels
// Component owns form state, parent owns submission logic
function FormContainer() {
const [submitted, setSubmitted] = useState(false);
const handleFormSubmit = async (data: FormData) => {
try {
await api.submitForm(data);
setSubmitted(true);
} catch (error) {
// Handle error
}
};
return (
<Form
onSubmit={handleFormSubmit}
onSuccess={() => setSubmitted(true)}
/>
);
}
interface FormProps {
onSubmit: (data: FormData) => Promise<void>;
onSuccess: () => void;
}
function Form({ onSubmit, onSuccess }: FormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setErrors({});
try {
await onSubmit({ email, password });
onSuccess();
} catch (error: any) {
setErrors({ submit: error.message });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isSubmitting}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isSubmitting}
/>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
{errors.submit && <div>{errors.submit}</div>}
</form>
);
}</details>
Context should pass configuration and dependencies, not frequently-changing data. Using context for state that changes frequently causes unnecessary re-renders.
Context excels at solving the "prop drilling" problem for configuration and dependencies that don't change often. Theme, authentication, locale, and feature flags are ideal use cases. Frequently-changing state belongs in local state or external stores instead.
// Create contexts for dependencies
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = React.createContext<AuthContextType | undefined>(
undefined
);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await api.login(email, password);
setUser(response.user);
localStorage.setItem("token", response.token);
} finally {
setIsLoading(false);
}
};
const logout = async () => {
setIsLoading(true);
try {
await api.logout();
setUser(null);
localStorage.removeItem("token");
} finally {
setIsLoading(false);
}
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = React.useContext(AuthContext);
if (!context) throw new Error("useAuth must be used inside AuthProvider");
return context;
}
// Application-wide theme context for configuration
interface ThemeContextType {
theme: "light" | "dark";
setTheme: (theme: "light" | "dark") => void;
}
const ThemeContext = React.createContext<ThemeContextType | undefined>(
undefined
);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = React.useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used inside ThemeProvider");
return context;
}
// Usage: inject dependencies without prop drilling
function App() {
return (
<AuthProvider>
<ThemeProvider>
<MainApp />
</ThemeProvider>
</AuthProvider>
);
}
function Dashboard() {
const { user } = useAuth();
const { theme } = useTheme();
if (!user) return <div>Not authenticated</div>;
return (
<div style={{ background: theme === "dark" ? "#000" : "#fff" }}>
Welcome, {user.name}
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Feature flags context for configuration
type FeatureFlag = "new_ui" | "beta_features" | "dark_mode";
interface FeatureFlagsContextType {
enabled: (flag: FeatureFlag) => boolean;
getVariant: (flag: FeatureFlag) => string | null;
}
const FeatureFlagsContext = React.createContext<
FeatureFlagsContextType | undefined
>(undefined);
function FeatureFlagsProvider({
flags,
children,
}: {
flags: Record<FeatureFlag, boolean | string>;
children: React.ReactNode;
}) {
return (
<FeatureFlagsContext.Provider
value={{
enabled: (flag) => !!flags[flag],
getVariant: (flag) => (typeof flags[flag] === "string" ? (flags[flag] as string) : null),
}}
>
{children}
</FeatureFlagsContext.Provider>
);
}
function useFeatureFlags() {
const context = React.useContext(FeatureFlagsContext);
if (!context) throw new Error("Must be inside FeatureFlagsProvider");
return context;
}
function Dashboard() {
const { enabled } = useFeatureFlags();
return (
<div>
{enabled("new_ui") && <NewUI />}
{enabled("dark_mode") && <DarkModeToggle />}
</div>
);
}</details>
Libraries like Redux, Zustand, and Jotai manage complex global state. Use external state management when:
// Zustand: lightweight state management
import create from "zustand";
interface TodoState {
todos: Array<{ id: string; text: string; done: boolean }>;
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
removeTodo: (id: string) => void;
}
const useTodoStore = create<TodoState>((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [
...state.todos,
{ id: Date.now().toString(), text, done: false },
],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
),
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
}));
// Components subscribe to store
function TodoList() {
const todos = useTodoStore((state) => state.todos);
const addTodo = useTodoStore((state) => state.addTodo);
return (
<div>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
<button onClick={() => addTodo("New task")}>Add</button>
</div>
);
}
function TodoItem({ todo }: { todo: typeof useTodoStore.getState().todos[0] }) {
const toggleTodo = useTodoStore((state) => state.toggleTodo);
return (
<div
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.done ? "line-through" : "none" }}
>
{todo.text}
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Redux with Redux Toolkit
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";
const todoSlice = createSlice({
name: "todos",
initialState: [] as typeof useTodoStore.getState().todos,
reducers: {
addTodo: (state, action) => {
state.push({
id: Date.now().toString(),
text: action.payload,
done: false,
});
},
toggleTodo: (state, action) => {
const todo = state.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
},
});
const store = configureStore({
reducer: {
todos: todoSlice.reducer,
},
});
function TodoListRedux() {
const todos = useSelector((state: any) => state.todos);
const dispatch = useDispatch();
return (
<div>
{todos.map((todo: any) => (
<div key={todo.id} onClick={() => dispatch(todoSlice.actions.toggleTodo(todo.id))}>
{todo.text}
</div>
))}
</div>
);
}</details>
┌─────────────────┬──────────────────────────────────────────────┐
│ Data Location │ Use Cases │
├─────────────────┼──────────────────────────────────────────────┤
│ Props │ Config, read-only data from parent │
│ Local State │ UI state, form input, temporary values │
│ Context │ Theme, auth, locale, feature flags │
│ External Store │ Complex global state, multiple subscribers │
│ External APIs │ Remote data with caching │
└─────────────────┴──────────────────────────────────────────────┘Ready to practice? Challenges | Next: Callback Props & Events
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward