No history yet

React Fundamentals

Components and Props

Think of a web page like a complex LEGO structure. Instead of building the entire thing with tiny, individual bricks, you often build smaller, reusable sections first—like a wall, a window, or a door. In React, these sections are called components. They are independent, reusable pieces of code that describe a part of your user interface.

A component is essentially a JavaScript function that returns some special HTML-like code. Let's look at a simple example.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// This is how you would use the component:
<Welcome name="Sara" />

Here, Welcome is a React component. It accepts a single argument called props (short for properties), which is an object containing data. We can pass data to this component using attributes, just like with HTML tags. In the example, <Welcome name="Sara" /> passes the string "Sara" to the component through a prop named name.

Props are read-only. A component should never change its own props; it should treat them as immutable inputs. This makes components predictable and easier to debug.

Components are the fundamental building blocks of any React application.

You can also compose components to build more complex UIs. For instance, you could create an App component that renders the Welcome component multiple times.

function App() {
  return (
    <div>
      <Welcome name="Ada" />
      <Welcome name="Grace" />
      <Welcome name="Hedy" />
    </div>
  );
}

This ability to nest and reuse components is what makes React so powerful for building large, manageable applications.

JSX: A Syntax for UI

You might have noticed the HTML-like syntax inside the JavaScript code. This is called JSX, which stands for JavaScript XML. It's a syntax extension that lets you write what looks like HTML directly in your JavaScript files. While you don't have to use it, most React developers find it helpful for describing what the UI should look like.

Here's an example:

const element = <h1>Hello, world!</h1>;

This funny-looking tag syntax is neither a string nor HTML. It gets compiled into regular JavaScript objects by a tool like Babel. For example, the code above is compiled into something like this:

React.createElement('h1', null, 'Hello, world!');

You can embed any valid JavaScript expression inside JSX by wrapping it in curly braces. This is how we used props.name in our Welcome component earlier.

For example, 2 + 2, user.firstName, or formatName(user) are all valid JavaScript expressions you could put inside {} in JSX.

One key rule of JSX is that an expression must have a single outer element. If you want to return multiple elements, you need to wrap them in a parent tag, like a <div>, or use a special wrapper called a Fragment, which looks like <> and </>.

// Correct: Wrapped in a single div
function Greeting() {
  return (
    <div>
      <h1>Hello!</h1>
      <p>How are you?</p>
    </div>
  );
}

// Incorrect: No single root element
// function Greeting() {
//   return (
//     <h1>Hello!</h1>
//     <p>How are you?</p>
//   );
// }

State and Lifecycle

While props allow us to pass data from a parent to a child, what if a component needs to manage its own data that changes over time? For this, we use state.

State is similar to props, but it is private and fully controlled by the component. It's the component's own memory. To use state, you'll need to use a class component. (Functional components can also have state using a feature called Hooks, but we'll focus on class components for now).

Let's build a simple clock component that updates itself every second.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Right now, this clock will only show the time when it was first created. To make it update, we need to use special methods that are part of a component's lifecycle.

A component has several “lifecycle methods” that you can override to run code at particular times in the process. Think of it like a component's birth, life, and death.

  • componentDidMount() runs after the component has been rendered to the DOM for the first time.
  • componentWillUnmount() runs just before the component is removed from the DOM.

We can use these to set up and tear down a timer.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Notice the this.setState() call in the tick() method. This is the only correct way to update a component's state. Directly changing this.state will not re-render the component. When you call setState(), React knows the state has changed and calls the render() method again to update the UI.

Handling User Interactions

React elements can listen for events, like clicks or keyboard input, much like you would in plain HTML. However, there are a few syntax differences:

  • React events are named using camelCase, so onclick becomes onClick.
  • You pass a function as the event handler, rather than a string.

Here’s a simple button that toggles a message:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

In the example above, this.handleClick is bound to the component instance in the constructor. This is a common pattern in class components to ensure this refers to the component inside the event handler method.

Conditional Rendering

Often, you'll want to show or hide parts of your UI based on certain conditions. In React, you can use standard JavaScript if statements or conditional operators to control which components render.

In the Toggle example, we used a ternary operator {this.state.isToggleOn ? 'ON' : 'OFF'} to change the button's text. You can also use this to render different components entirely.

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <h1>Welcome back!</h1>;
  }
  return <h1>Please sign up.</h1>;
}

This Greeting component renders a different heading based on the value of its isLoggedIn prop.

Lists and Keys

Displaying lists of items is a common task. You can use JavaScript's map() function to transform an array of data into an array of React elements.

When you render a list, React needs a way to identify which items have changed, been added, or been removed. This is where the special key prop comes in. Keys should be stable, predictable, and unique strings or numbers that identify each item in the list.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

Using an array index as a key is generally discouraged, especially if the order of items may change. It can lead to performance issues and bugs with component state.

Managing Data Flow

In React, data flows in one direction: down from parent components to child components via props. This is called "top-down" or "unidirectional" data flow.

But what if a child component needs to change the state of a parent component? For example, what if an input field in a child needs to update a value held in the parent's state?

This is where you use a technique called lifting state up. The parent component defines a function that updates its state, and then passes that function down to the child as a prop. The child can then call this function whenever it needs to communicate back up to the parent.

This pattern keeps components more reusable and ensures that there is a single "source of truth" for any given piece of data. If multiple components need to reflect the same changing data, their common ancestor should own that state.

Forms in React

HTML form elements like <input>, <textarea>, and <select> work a bit differently in React because their state is naturally mutable. React can control these elements by making their value a piece of the component's state. This is called a "controlled component."

Here’s how you would create a simple controlled input field:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

In this example, the form input's value is always driven by this.state.value. The handleChange function is called every time the input changes, updating the state. This makes the React state the single source of truth.

Composition vs. Inheritance

React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components. Instead of creating complex class hierarchies, you build components that can be configured and nested within each other.

For example, if you have a Sidebar and a Dialog that are special cases of a generic Box component, you don't need to have Sidebar extend Box. Instead, you can have a Box component that accepts other components as props.

function Box(props) {
  return (
    <div className={'Box Box-' + props.color}>
      {props.children} // Renders whatever is inside the <Box> tags
    </div>
  );
}

function App() {
  return (
    <Box color="blue">
      <h1>Welcome</h1>
      <p>This is a composed component.</p>
    </Box>
  );
}

This approach is more flexible and makes it easier to understand how components are related. Props and composition give you all the flexibility you need to customize a component’s look and behavior in a clear and safe way.

Quiz Questions 1/6

What is the primary way to pass data from a parent component to a child component in React?

Quiz Questions 2/6

Which of the following is the correct way to update a class component's state and trigger a re-render?