Lesson 1 of 12

8% Complete

React Fundamentals: JSX, Components & Props

Master the foundational concepts of React: JSX syntax, component creation, and prop passing. Learn how to build reusable UI components and compose them together.

In this lesson, you'll learn how to build a simple <Tag> component – a small, reusable UI element for labeling content (like categories or keywords). Along the way, we will introduce core React concepts including JSX, function components, props, default prop values, component composition with children, and even basic prop type checking. By the end, you'll understand how to create and use your own components in a React app, following modern practices from the latest React documentation.

JSX: Writing Markup in JavaScript

React uses a syntax extension called JSX to let you write HTML-like markup directly in your JavaScript code. Instead of separating HTML and JS in different files, React encourages mixing them in components – which keeps the rendering logic and the markup together. For example, here's a simple JSX snippet inside a React component:

Loading syntax highlighting...

In this code, <span className="tag">New!</span> is JSX. It looks like an HTML <span> element, but it's actually JavaScript under the hood. Each component in React is essentially a JavaScript function that returns JSX markup. JSX lets you write markup conveniently, and React will render it to real DOM elements in the browser.

JSX vs HTML: There are a few important differences between JSX and plain HTML:

  • Closing Tags: JSX requires every tag to be closed properly. Self-closing tags like <br> or <img> must be written as <br /> or <img />. If you forget to close a tag, React's compiler will error.
  • Attributes: In JSX, HTML attributes are written in camelCase or slightly different names. For example, use className instead of class for CSS classes, and onClick (camelCase) for event handlers instead of lowercase onclick.
  • JavaScript in JSX: You can embed JavaScript expressions inside JSX by wrapping them in curly braces { }. For instance: const name = 'React'; then <h1>Hello, {name}!</h1> will insert the value of the name variable into the heading. This is how you can display dynamic data in your markup.

One Root Element: A React component's JSX must return a single root element. If you need to return multiple siblings, wrap them in a parent element (like a <div>) or a React Fragment (<> ... </>). This rule keeps JSX tidy and unambiguous. For our <Tag> example above, the JSX has one <span> as the root, so it's valid. (Returning two <span> siblings side by side without a wrapper would error out.)

Now that we have seen JSX in action, let's turn this into a proper component that we can reuse.

Defining a Function Component

A React component is simply a JavaScript function (typically using ES6 function or arrow syntax) that returns JSX. Let's formally define our Tag component as a function and use it inside another component:

Loading syntax highlighting...

Here we created a Tag component that returns a span element with some text. We export it so that it can be imported and used in other files (like App.tsx). Notice how we use <Tag /> in JSX just like a normal HTML tag. In React, component names must begin with a capital letter (like Tag) so that React knows it's a custom component and not a built-in DOM tag. If you wrote <tag /> lowercase, React would look for a literal HTML <tag> element instead of your component. Always start your component function names with a capital letter – this is a key convention, and React will warn or ignore the component if you don't.

When the <App> component renders, it will output the following HTML to the browser:

Loading syntax highlighting...

As you can see, using <Tag /> inside App resulted in an actual <span> element in the DOM. This shows the power of components: you define a piece of UI once, then you can reuse it by referencing <Tag /> wherever you need that labeled span. In fact, you could use <Tag /> multiple times (e.g. render several tags) and each would output a span. Components are the building blocks of React apps – you create small pieces like this and compose them to build complex UIs. React components are regular functions except that they return JSX and must be capitalized. Next, we'll make our Tag more flexible by providing it with data.

Passing Props to Your Component

Right now, our Tag always displays the fixed text "New!". But in real use, we want to label different things – e.g. "React", "JavaScript", "Tutorial", etc. React components achieve this flexibility through props (short for "properties"). Props are input data passed into components, similar to function arguments. They let a parent component configure a child component.

To pass props, you add them as attributes on the JSX tag, just like HTML attributes. For example, we can give our Tag a custom label via a prop:

Loading syntax highlighting...

In this updated Tag, we changed the function signature to function Tag({ label }). This uses JavaScript destructuring to grab the label property from the props object. (React will pass an object of props to your component, and we immediately unpack label out of it.) Then we use {label} inside the JSX to display its value. Now whenever a parent renders <Tag label="React" />, the label prop in the Tag component will be "React", and it will render a span containing "React".

Think of props as the way parents communicate with child components. Every parent component can pass information to its children by giving them props. In the example above, App is passing different text values into each <Tag> instance. Props might remind you of HTML attributes (and they look similar), but unlike static HTML attributes, props can be any JavaScript value – strings, numbers, arrays, objects, or even functions. This makes them very powerful.

How to read props: Inside the child component (Tag), you access props either via the function parameters (using destructuring as we did) or by using the function's first argument (conventionally called props). For example, we could define function Tag(props) { return <span>{props.label}</span>; } – this works the same, but then you'd write props.label inside. Using destructuring function Tag({ label }) is cleaner, especially when you have multiple props.

One thing to note is that if you pass no props at all, the destructured variables will be undefined. For instance, <Tag /> without a label would render an empty span because label is undefined. We usually want to guard against that by providing default values, which we'll cover next.

Setting Default Prop Values

Sometimes a prop may not be provided by the parent, and you want the component to use a reasonable default value in that case. In React, the recommended way to specify default props is by using default parameters in the function definition. We can enhance our Tag component to have a default color for the tag, for example:

Loading syntax highlighting...

Here we added a new prop color and wrote it as color = "gray" in the destructuring. This means if a parent doesn't pass a color prop, it will default to "gray". Now our Tag can be color-coded, but it won't break if no color is given – it just falls back to gray.

For example:

Loading syntax highlighting...

In the first case, no color prop is provided, so inside Tag the variable color will be set to "gray". In the second case, we pass color="blue", so inside the component color is "blue". We then use that value in the style object to set the span's background. (We also set a text color to white for contrast in this example.)

How default values work: The default value only applies if the prop is missing or explicitly undefined. If you pass color={null} or color="" (empty string), those values will be used (and in our case null or empty string would result in no background color). So default parameters are used for "no value" scenarios, but they won't override an actual null or falsy value that's passed in. Keep this in mind to avoid confusion – e.g., a 0 or false prop will not trigger the default because those are legitimate values (not undefined).

Default props help make your components more robust by handling missing data. You could also use this for something like a label: for instance, function Tag({ label = "Default", color = "gray" }) could provide a fallback label. However, in most cases you'll want important props like a label to be required (it's better to catch the mistake of not passing it). For optional visual props like our color, providing a default makes the component easier to use.

(Aside: In older React code, you might see Tag.defaultProps = { color: "gray" } used for default values. The modern approach is to use default function parameters as shown above, which is more concise and doesn't require an extra property on the component.)

Composing Components with Children

Thus far, our Tag component can display text via a label prop and has a customizable color. But what if we want our Tag to include more complex content – for example, an icon alongside the text, or perhaps we want a wrapper component that can contain any elements inside a styled box? This is where React's composition model shines, using a special prop called children.

In React, you can nest JSX inside other JSX tags, just like you nest HTML tags. When you write something inside a component's opening and closing tags, that content is passed to the component via the children prop. For example:

Loading syntax highlighting...

Here, we are using Tag not as a self-closing tag, but with an opening <Tag> and closing </Tag>, wrapping a <strong>Success</strong> element. React will pass that JSX (<strong>Success</strong>) to our Tag component as a prop named children. To handle this, we can update the Tag component to use children instead of a label prop:

Loading syntax highlighting...

Now Tag will simply render whatever content is passed between its tags, wrapped in the styled <span>. For example, in the usage above, children will be the <strong>Success</strong> element, so the output HTML becomes <span style="background-color:green; ..."><strong>Success</strong></span>. We could also pass plain text as children (e.g. <Tag>Success</Tag> without a <strong>), or more complex JSX like an icon image plus text:

Loading syntax highlighting...

In this case, the entire <img> Error content is received as children and rendered inside the span.

Understanding children: The children prop is how you achieve component composition – meaning you can build bigger components out of smaller ones by nesting. One component can serve as a container or wrapper for any child content. Our Tag is effectively acting as a wrapper that adds styling (background color, padding, etc.) around whatever is inside it. The key is that Tag doesn't need to know what its children are; it just renders {children} in the output. This makes it very flexible. As the official docs say, when you nest content inside a JSX tag, the parent component receives that content via a children prop and can render it wherever appropriate. You can think of children like a "hole" or placeholder in your component's output that can be filled with any JSX by the parent.

Using children like this is common for layout or UI elements that just decorate or frame some content. For example, you might have a <Card> component that renders a div with some styling and inside it outputs {children} – allowing you to wrap arbitrary content in a card layout. In our case, Tag wraps children with a colored badge style.

Now that we've covered JSX, components, props, default values, and children, let's briefly touch on ensuring our component is used correctly with prop type checking.

Type Checking with TypeScript

Since we're using TypeScript, we get compile-time type checking for our component props, which is more powerful and efficient than runtime validation. TypeScript will catch type errors during development and provide excellent IntelliSense support.

Our Tag component already has proper TypeScript interfaces:

Loading syntax highlighting...

Benefits of TypeScript over PropTypes:

  • Compile-time checking: TypeScript catches errors before your code runs, not during development
  • Better IntelliSense: Your IDE provides autocomplete and inline documentation
  • Refactoring safety: TypeScript ensures you don't break component contracts when refactoring
  • No runtime overhead: Type checking happens at compile time, so there's no performance impact
  • More precise types: TypeScript can express complex types that PropTypes cannot

TypeScript will catch errors like:

  • Passing a number instead of a string: <Tag color={123}>Hello</Tag>
  • Missing required props: <Tag></Tag> ❌ (if children were required)
  • Passing invalid prop names: <Tag invalidProp="value">Hello</Tag>

This is much more robust than the runtime warnings that PropTypes provide. TypeScript is the modern standard for React applications and provides a superior development experience.

Best Practices & Common Mistakes

Building React components is straightforward, but there are some best practices and gotchas to be aware of. Here's a list of tips and common mistakes to avoid when working with JSX, components, and props:

  • Name Components with a Capital Letter: Always start your component names with a capital letter (e.g. Tag, not tag). React differentiates between DOM tags and components by this convention. If you use lowercase, it will treat it as a built-in tag or ignore it.
  • Export Your Components: If you create a component in its own file, remember to export it (e.g. export default function Tag() { ... }). Forgetting to export will result in import errors or a blank render.
  • Return JSX (One Parent Element): Make sure your component's return statement returns JSX with a single root element. If you have adjacent JSX elements, wrap them in a <div> or <> fragment. Also, include parentheses around multi-line JSX returns. A common pitfall is writing:
Loading syntax highlighting...

In JavaScript, a line break after return can terminate the statement unexpectedly. Using return ( on one line and ) after the JSX block helps avoid this automatic semicolon insertion. In short, always return exactly one JSX element (or fragment), and wrap it in parentheses if it spans multiple lines.

  • Don't Call Components Like Functions: You might wonder, since components are functions, can you call Tag() directly? Do not do this. Always use the JSX syntax (<Tag />) to "call" a component. Calling it as a regular function will not trigger React's rendering and will likely cause errors. Let React handle invoking your components via JSX.
  • Props are Read-Only: Inside a component, never attempt to modify your props. Props should be considered immutable (unchangeable) data. If you think a component needs to change some value, that should be done via state in the parent and passed down again as new props. For example, you wouldn't do something like props.color = 'blue' inside Tag – that doesn't actually update the parent, and it's conceptually wrong. Always treat props as read-only snapshots of data.
  • Use Default Props for Optional Values: If a prop can be omitted, provide a default value (using destructuring as we did with color). This way your component won't end up with undefined values that could cause errors. It makes the component easier to use (the parent can omit safe-to-assume props).
  • Validate Props in Development: Consider using PropTypes (or TypeScript) to catch mistakes early. This is a best practice especially in larger apps – it can save you from passing wrong types (like a number when a string is expected) or forgetting required props. PropTypes will warn you of such issues during development.
  • Compose Instead of Duplicating: Leverage composition with children rather than duplicating code. For instance, instead of writing a nearly identical wrapper component for different content, create one that uses children. Similarly, don't define components inside other components. Define each component at the top level (module scope) and use props to pass data. Nesting component definitions (writing a new function inside another) is an anti-pattern that leads to bugs and performance issues. Always compose via JSX, not by nesting function definitions.

By following these practices, you'll avoid the most common React hurdles and your components will be easier to maintain and reuse.

Recap

You've built a reusable <Tag> component and learned several core React skills. Now you can confidently do the following:

  • Create React components as JavaScript functions that return JSX (markup in JavaScript).
  • Use those components in JSX just like HTML elements, to build your UI out of reusable pieces.
  • Pass data via props to customize components (e.g. giving each Tag its own label or color).
  • Provide default prop values to make components robust to missing props (using destructuring with =).
  • Compose components by nesting them and using the special children prop to inject content or other components inside a wrapper.
  • Apply basic prop type checking with PropTypes to catch mistakes during development (optional but useful for growing codebases).

With these fundamentals, you can create many kinds of UI elements and compose them into complex interfaces. Our Tag component is just one example – try building other components (like buttons, alerts, cards) using the same patterns. You should now have the confidence to write modern React code following the best practices from the official docs. Happy coding! 😊


Next: State and Lifecycle with Hooks →