JavaScript and React course Β· Module 3: State and Events

Data Flow

9 min read
In this lesson7

On a cosmic journey through React, understanding data flow is like learning the star maps of systems - crucial for safe and efficient navigation. React relies on unidirectional data flow, which ensures predictability and makes debugging applications easier.

Unidirectional data flow

React uses the concept of unidirectional (or one-way downward) data flow. This means that data flows in only one direction: from parent components to child components.

Imagine your application structure like a command hierarchy in a space mission:

  • Mission command center (top-level component) sends instructions and data to various modules
  • Each module can pass that data further to its submodules
  • Submodules cannot directly modify data received from parent modules
  • To change data, submodules must "report" changes to parent modules, which can update the data and pass it back

Advantages of unidirectional data flow

  1. Predictability - easier to understand where data comes from and how it changes
  2. Easier debugging - data issues can be traced "upstream" to the source
  3. Better performance - React can optimize component updates
  4. Less complexity - each component has a clearly defined role in the data flow

Props as the mechanism for passing data down

The main mechanism for passing data from parent to children is props (properties):

1// Parent component
2function MissionControl() {
3  const missionData = {
4    name: "Apollo 11",
5    destination: "Moon",
6    crew: ["Neil Armstrong", "Buzz Aldrin", "Michael Collins"],
7    status: "In Progress"
8  };
9
10  return (
11    <div className="mission-control">
12      <h1>Mission Control Center</h1>
13      <MissionInfo
14        name={missionData.name}
15        destination={missionData.destination}
16        status={missionData.status}
17      />
18      <CrewDisplay crew={missionData.crew} />
19    </div>
20  );
21}
22
23// Child component
24function MissionInfo({ name, destination, status }) {
25  return (
26    <div className="mission-info">
27      <h2>{name}</h2>
28      <p>Destination: {destination}</p>
29      <p>Status: {status}</p>
30    </div>
31  );
32}
33
34// Another child component
35function CrewDisplay({ crew }) {
36  return (
37    <div className="crew-display">
38      <h2>Crew</h2>
39      <ul>
40        {crew.map((member, index) => (
41          <li key={index}>{member}</li>
42        ))}
43      </ul>
44    </div>
45  );
46}

In this example:

  • MissionControl is the parent component that contains mission data
  • It passes that data down to the MissionInfo and CrewDisplay components via props
  • The child components receive that data and render it, but they cannot directly modify it

Upward communication - how children communicate with parents

Since data only flows downward, how can lower-level components communicate with higher-level components? The solution is a technique of passing functions down as props, which child components can call to "report" changes to the parent.

1function MissionControl() {
2  const [missionStatus, setMissionStatus] = useState("Preparation");
3
4  // This function will be passed to the child
5  const updateStatus = (newStatus) => {
6    setMissionStatus(newStatus);
7    console.log(`Mission status updated to: ${newStatus}`);
8  };
9
10  return (
11    <div className="mission-control">
12      <h1>Mission Control Center</h1>
13      <p>Current Status: {missionStatus}</p>
14
15      {/* We pass the updateStatus function as a prop to StatusControls */}
16      <StatusControls
17        currentStatus={missionStatus}
18        onStatusChange={updateStatus}
19      />
20    </div>
21  );
22}
23
24function StatusControls({ currentStatus, onStatusChange }) {
25  return (
26    <div className="status-controls">
27      <h2>Status Controls</h2>
28
29      <button
30        onClick={() => onStatusChange("Countdown")}
31        disabled={currentStatus === "Countdown"}
32      >
33        Start Countdown
34      </button>
35
36      <button
37        onClick={() => onStatusChange("Launch")}
38        disabled={currentStatus === "Launch" || currentStatus !== "Countdown"}
39      >
40        Initiate Launch
41      </button>
42
43      <button
44        onClick={() => onStatusChange("Orbit")}
45        disabled={currentStatus === "Orbit" || currentStatus !== "Launch"}
46      >
47        Confirm Orbit Achieved
48      </button>
49
50      <button
51        onClick={() => onStatusChange("Aborted")}
52        disabled={currentStatus === "Aborted" || currentStatus === "Orbit"}
53      >
54        Abort Mission
55      </button>
56    </div>
57  );
58}

In this example:

  1. MissionControl (parent) defines the updateStatus function
  2. It passes this function as the onStatusChange prop to the StatusControls component (child)
  3. StatusControls calls this function with a new value when the user clicks a button
  4. Once called, the function updates the state in the parent component, triggering a re-render
  5. The updated data (new status) flows back down to children via props

This pattern preserves unidirectional data flow, even though lower-level components can influence data in higher-level components.

Lifting state up

"Lifting state up" is an important concept in React, involving moving state from child components to the nearest common ancestor that needs access to that data. This technique is crucial for maintaining unidirectional data flow.

Consider an example with spaceship modules that need to share data:

1function SpaceshipDashboard() {
2  // State has been "lifted up" to the common parent
3  const [power, setPower] = useState(100);
4  const [shields, setShields] = useState(75);
5  const [weapons, setWeapons] = useState(0);
6
7  // Functions to update state
8  const adjustPower = (system, amount) => {
9    if (system === 'shields') {
10      setShields(prev => Math.min(100, Math.max(0, prev + amount)));
11      setPower(prev => Math.max(0, prev - amount));
12    } else if (system === 'weapons') {
13      setWeapons(prev => Math.min(100, Math.max(0, prev + amount)));
14      setPower(prev => Math.max(0, prev - amount));
15    }
16  };
17
18  return (
19    <div className="spaceship-dashboard">
20      <h1>Spaceship Control Panel</h1>
21
22      <PowerDisplay powerLevel={power} />
23
24      <div className="systems">
25        <ShieldSystem
26          shieldLevel={shields}
27          onAdjustPower={amount => adjustPower('shields', amount)}
28        />
29
30        <WeaponSystem
31          weaponCharge={weapons}
32          onAdjustPower={amount => adjustPower('weapons', amount)}
33        />
34      </div>
35    </div>
36  );
37}
38
39function PowerDisplay({ powerLevel }) {
40  return (
41    <div className="power-display">
42      <h2>Power Reserve: {powerLevel}%</h2>
43      <div className="power-bar" style={{ width: `${powerLevel}%` }}></div>
44    </div>
45  );
46}
47
48function ShieldSystem({ shieldLevel, onAdjustPower }) {
49  return (
50    <div className="system-panel shields">
51      <h2>Shields: {shieldLevel}%</h2>
52      <div className="shield-bar" style={{ height: `${shieldLevel}%` }}></div>
53
54      <div className="controls">
55        <button onClick={() => onAdjustPower(10)} disabled={shieldLevel >= 100}>
56          + Increase Shield Power
57        </button>
58        <button onClick={() => onAdjustPower(-10)} disabled={shieldLevel <= 0}>
59          - Decrease Shield Power
60        </button>
61      </div>
62    </div>
63  );
64}
65
66function WeaponSystem({ weaponCharge, onAdjustPower }) {
67  return (
68    <div className="system-panel weapons">
69      <h2>Weapon Charge: {weaponCharge}%</h2>
70      <div className="weapon-bar" style={{ height: `${weaponCharge}%` }}></div>
71
72      <div className="controls">
73        <button onClick={() => onAdjustPower(25)} disabled={weaponCharge >= 100}>
74          Charge Weapons
75        </button>
76        <button onClick={() => onAdjustPower(-25)} disabled={weaponCharge <= 0}>
77          Discharge Weapons
78        </button>
79      </div>
80    </div>
81  );
82}

In this example:

  1. State for power, shields, and weapons is stored in the parent component SpaceshipDashboard
  2. The ShieldSystem and WeaponSystem components receive only the data they need and functions to update that data
  3. When one system uses power, it affects the available power for other systems
  4. Resource sharing logic is managed centrally by the parent component

This technique is particularly useful when:

  • Multiple components need the same data
  • Data in one component affects data in other components
  • Components need to be synchronized

Controlled and uncontrolled components

In React, especially in the context of forms, there are two approaches to managing input data: controlled and uncontrolled components.

Controlled components

In controlled components, form data is managed by React state. Every change made by the user goes through React:

1function ControlledMissionForm() {
2  const [formData, setFormData] = useState({
3    missionName: '',
4    duration: '',
5    crew: ''
6  });
7
8  const handleChange = (e) => {
9    setFormData({
10      ...formData,
11      [e.target.name]: e.target.value
12    });
13  };
14
15  const handleSubmit = (e) => {
16    e.preventDefault();
17    console.log('Mission data:', formData);
18    // Data submission logic...
19  };
20
21  return (
22    <form onSubmit={handleSubmit}>
23      <div>
24        <label>
25          Mission Name:
26          <input
27            type="text"
28            name="missionName"
29            value={formData.missionName}
30            onChange={handleChange}
31          />
32        </label>
33      </div>
34
35      <div>
36        <label>
37          Duration (days):
38          <input
39            type="number"
40            name="duration"
41            value={formData.duration}
42            onChange={handleChange}
43          />
44        </label>
45      </div>
46
47      <div>
48        <label>
49          Number of Crew Members:
50          <input
51            type="number"
52            name="crew"
53            value={formData.crew}
54            onChange={handleChange}
55          />
56        </label>
57      </div>
58
59      <button type="submit">Plan Mission</button>
60    </form>
61  );
62}

Advantages of controlled components:

  • Full control over state and real-time validation
  • Ability to react immediately to changes (e.g., dynamic validation)
  • Ease of manipulating and transforming input data

Uncontrolled components

Uncontrolled components let the DOM itself manage form state:

1function UncontrolledMissionForm() {
2  // References to DOM elements
3  const missionNameRef = useRef();
4  const durationRef = useRef();
5  const crewRef = useRef();
6
7  const handleSubmit = (e) => {
8    e.preventDefault();
9
10    const formData = {
11      missionName: missionNameRef.current.value,
12      duration: durationRef.current.value,
13      crew: crewRef.current.value
14    };
15
16    console.log('Mission data:', formData);
17    // Data submission logic...
18  };
19
20  return (
21    <form onSubmit={handleSubmit}>
22      <div>
23        <label>
24          Mission Name:
25          <input
26            type="text"
27            name="missionName"
28            ref={missionNameRef}
29            defaultValue="Mars Mission"
30          />
31        </label>
32      </div>
33
34      <div>
35        <label>
36          Duration (days):
37          <input
38            type="number"
39            name="duration"
40            ref={durationRef}
41            defaultValue="90"
42          />
43        </label>
44      </div>
45
46      <div>
47        <label>
48          Number of Crew Members:
49          <input
50            type="number"
51            name="crew"
52            ref={crewRef}
53            defaultValue="4"
54          />
55        </label>
56      </div>
57
58      <button type="submit">Plan Mission</button>
59    </form>
60  );
61}

Advantages of uncontrolled components:

  • Less code for simple forms
  • Fewer renders (better performance in some cases)
  • Easier integration with external libraries

In most cases, it is recommended to use controlled components because they provide greater control and are more aligned with React's philosophy. However, in some scenarios (e.g., file uploads, integration with external libraries), uncontrolled components may be more practical.

Data flow in larger applications

As an application grows, data flow can become more complex. In such cases, it is worth considering:

1. Context (Context API)

For data needed in many places in the application, you can use the Context API to avoid so-called "props drilling" (passing props through many levels of components):

1// Creating a context
2const MissionContext = React.createContext();
3
4// Provider component at a high level
5function MissionProvider({ children }) {
6  const [missionData, setMissionData] = useState({
7    name: "Artemis",
8    phase: "Preparation",
9    crew: ["Anna", "Jan", "Maria", "Piotr"],
10    launchDate: "2025-07-20"
11  });
12
13  const updateMission = (updates) => {
14    setMissionData(prev => ({
15      ...prev,
16      ...updates
17    }));
18  };
19
20  return (
21    <MissionContext.Provider value={{ missionData, updateMission }}>
22      {children}
23    </MissionContext.Provider>
24  );
25}
26
27// Usage in a deeply nested component
28function DeepNestedComponent() {
29  const { missionData, updateMission } = useContext(MissionContext);
30
31  return (
32    <div>
33      <h3>Mission Phase: {missionData.phase}</h3>
34      <button onClick={() => updateMission({ phase: "Countdown" })}>
35        Start Countdown
36      </button>
37    </div>
38  );
39}
40
41// Application structure
42function App() {
43  return (
44    <MissionProvider>
45      <div className="app">
46        <Header />
47        <MainDashboard />
48        <Footer />
49      </div>
50    </MissionProvider>
51  );
52}

2. Global state management

For even more complex applications, you can consider state management libraries like Redux Toolkit, Zustand or MobX. They offer more advanced mechanisms for managing global state.

Summary

Understanding data flow in React is the foundation of creating predictable, maintainable applications:

  1. Unidirectional data flow - data flows from top to bottom, from parents to children
  2. Props - the primary mechanism for passing data down
  3. Functions as props - the mechanism for upward communication
  4. Lifting state up - the technique of moving shared state to a common ancestor
  5. Controlled vs uncontrolled components - different approaches to managing form data
  6. Context API and state management libraries - solutions for more complex data flows

By following these principles, you create applications that are easier to understand, debug, and develop - just like a well-planned space mission has clearly defined communication protocols and data flow.

Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3// Child component - receives data via props
4function MissionInfo({ mission, onStatusChange }) {
5  return (
6    <div className="mission-info">
7      <h3>{mission.name}</h3>
8      <p>Status: <span className={`status ${mission.status}`}>{mission.status}</span></p>
9      <p>Destination: {mission.destination}</p>
10      <p>Crew: {mission.crew.join(', ')}</p>
11
12      <div className="controls">
13        <button
14          onClick={() => onStatusChange(mission.id, 'in-progress')}
15          disabled={mission.status === 'in-progress'}
16        >
17          Start mission
18        </button>
19        <button
20          onClick={() => onStatusChange(mission.id, 'completed')}
21          disabled={mission.status === 'completed'}
22        >
23          Complete mission
24        </button>
25        <button
26          onClick={() => onStatusChange(mission.id, 'cancelled')}
27          disabled={mission.status === 'cancelled'}
28        >
29          Cancel mission
30        </button>
31      </div>
32    </div>
33  );
34}
35
36// Parent component - manages state and passes data down
37function MissionControl() {
38  const [missions, setMissions] = useState([
39    {
40      id: 1,
41      name: 'Apollo 21',
42      status: 'planned',
43      destination: 'Moon',
44      crew: ['Anna Kowalska', 'Jan Nowak']
45    },
46    {
47      id: 2,
48      name: 'Mars Explorer',
49      status: 'planned',
50      destination: 'Mars',
51      crew: ['Maria Wisniewska', 'Piotr Zielinski', 'Katarzyna Dabrowska']
52    },
53    {
54      id: 3,
55      name: 'Titan Survey',
56      status: 'in-progress',
57      destination: 'Titan (moon of Saturn)',
58      crew: ['Tomasz Kowalczyk', 'Agnieszka Nowak']
59    }
60  ]);
61
62  // Function passed down - communication going up
63  const handleStatusChange = (missionId, newStatus) => {
64    setMissions(prevMissions =>
65      prevMissions.map(mission =>
66        mission.id === missionId
67          ? { ...mission, status: newStatus }
68          : mission
69      )
70    );
71  };
72
73  const getStatusCount = (status) => {
74    return missions.filter(mission => mission.status === status).length;
75  };
76
77  return (
78    <div className="mission-control">
79      <h1>Mission Control Center</h1>
80
81      <div className="statistics">
82        <div className="stat">
83          <span className="count">{getStatusCount('planned')}</span>
84          <span className="label">Planned</span>
85        </div>
86        <div className="stat">
87          <span className="count">{getStatusCount('in-progress')}</span>
88          <span className="label">In Progress</span>
89        </div>
90        <div className="stat">
91          <span className="count">{getStatusCount('completed')}</span>
92          <span className="label">Completed</span>
93        </div>
94        <div className="stat">
95          <span className="count">{getStatusCount('cancelled')}</span>
96          <span className="label">Cancelled</span>
97        </div>
98      </div>
99
100      <div className="missions-grid">
101        {missions.map(mission => (
102          <MissionInfo
103            key={mission.id}
104            mission={mission}
105            onStatusChange={handleStatusChange}
106          />
107        ))}
108      </div>
109    </div>
110  );
111}
112
113function App() {
114  return (
115    <div className="app">
116      <MissionControl />
117    </div>
118  );
119}
120
121export default App;

Spotted a mistake in this lesson?

Hands-on tasks in the game

  • Code editor

    Create a login form with the states [username, setUsername] and [password, setPassword]. Both fields must be controlled, e.g. <input value={username} onChange={e => setUsername(e.target.value)} />, and the password field has type="password". The form has onSubmit={handleLogin}, and handleLogin calls e.preventDefault(). After submitting, display the greeting Welcome {username} (store in state that the form has been submitted).

Useful articles