Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🎁 What are Props?📤 Passing Props to Components🔍 Reading Props in Components🎯 Props for Customization✅ Prop Types & Validation🔄 Props vs State⬇️ Passing Functions as Props🚀 Building Reusable Components
Reactjs/Props/Props Vs State

🔄 Advanced Data Architecture — Props, State, Context, and Beyond

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.


🎯 Data Flow Patterns: Props Down, Events Up

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 for Dependency Injection

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>

🔧 External State Management for Complex Applications

Libraries like Redux, Zustand, and Jotai manage complex global state. Use external state management when:

  • Multiple components need the same data
  • State changes frequently
  • Complex update logic exists
  • Time-travel debugging is needed
// 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>

🎨 Decision Matrix: Where Should Data Live?

┌─────────────────┬──────────────────────────────────────────────┐
│ 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                    │
└─────────────────┴──────────────────────────────────────────────┘

🔑 Key Takeaways

  • ✅ Unidirectional data flow prevents circular dependencies and bugs
  • ✅ Props should be immutable, deterministic configuration
  • ✅ Local state manages component-specific UI state
  • ✅ Context solves prop drilling for dependencies and configuration
  • ✅ External state management handles complex global state
  • ✅ Choose data location based on access patterns and update frequency
  • ✅ Combine approaches for scalable, maintainable architectures

Ready to practice? Challenges | Next: Callback Props & Events


Resources

Python Docs

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

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn