const name = 'World';
export function Greeting() {
return (
<div>
<h1>Hello, {name}</h1>
<p>This is your first component!</p>
</div>
);
}
Multi-line JSX should always be wrapped in parentheses.
function SingleLineComponent() { return <p>Single-line without enclosing ()</p>; } function MultiLineComponent() { return ( <div> <h1>Heading</h1> <p>This is a multi-line component enclosed by ().</p> </div> ); }
function SingleLineComponent() { return <p>Single-line without enclosing ()</p>; } function MultiLineComponent() { return ( <div> <h1>Heading</h1> <p>This is a multi-line component enclosed by ().</p> </div> ); }
Importing another component
WelcomePage.tsximport { Greeting } from './Greeting'; export function WelcomePage() { return ( <div> <Greeting /> <p>Other content</p> </div> ); }
import { Greeting } from './Greeting'; export function WelcomePage() { return ( <div> <Greeting /> <p>Other content</p> </div> ); }
Components always return a single node .
If you want to return multiple nodes, wrap them in a Fragment to avoid rendering an unnecessary <div> .
function MyComponent() { return ( <> <ChildComponent /> <p>Other content</p> </> ); }
function MyComponent() { return ( <> <ChildComponent /> <p>Other content</p> </> ); }
function MyComponent() { return ( <> <ChildComponent /> <p>Other content</p> </> ); }
import { Fragment } from "react"; function MyComponent() { return ( <Fragment> <ChildComponent /> <p>Other content</p> </Fragment> ); }
Using the long-form
Fragment
is equivalent to the shorter
We will see in a bit in which situation the long-form <Fragment> is useful.
$ npm create vite@latest pokedex-vite
> React
> Typescript + SWC
$ cd ./pokedex-vite
$ npm install
$ npm run dev
const name = 'World';
export function PokeListEntry() {
return (
<div>{name}</div>
);
}
import { PokeListEntry } from "./PokeListEntry"
export function App() {
return (
<PokeListEntry />
);
}
We can pass data to child components by setting their props (properties)
A prop can have any type, e.g. it can be a JavaScript primitive, an object, or a function
Props can be defined as required or optional
Pass functions as props to listen to events emitted by child components
type MyComponentProps = {
name: string; // required prop
age?: number; // optional prop
wearsHat?: boolean; // optional prop with default value
};
export function MyComponent({
name,
age,
wearsHat = false,
}: MyComponentProps) {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age !== undefined ? age : "unknown"}</p>
<p>Wears hat: {wearsHat ? "yes" : "no"}</p>
</div>
);
}
type PokeListEntryProps = {
name?: string;
};
export function PokeListEntry({
name = 'Bulbasaur'
}: PokeListEntryProps) {
return (
<div>{name}</div>
);
}
Our components can be tested using the Vitest and React Testing Library
Add the library
npm install vitest @testing-library/react @testing-library/jest-dom --save-dev
Add in the package.json in the scripts section
"test": "vitest",
import { expect, afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; import matchers from "@testing-library/jest-dom/matchers"; /* extends Vitest's expect method with methods from react-testing-library */ expect.extend(matchers); /* runs a cleanup after each test case (e.g. clearing jsdom) */ afterEach(() => { cleanup(); });
import { expect, afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; import matchers from "@testing-library/jest-dom/matchers"; /* extends Vitest's expect method with methods from react-testing-library */ expect.extend(matchers); /* runs a cleanup after each test case (e.g. clearing jsdom) */ afterEach(() => { cleanup(); });
import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react-swc"; export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom", setupFiles: "./tests/setup.ts", }, });
import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react-swc"; export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom", setupFiles: "./tests/setup.ts", }, });
import { render, screen } from '@testing-library/react'; import { Greeting } from './Greeting'; describe('Greeting', () => { it('should render "Hello World"', () => { render(<Greeting />); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); });
import { render, screen } from '@testing-library/react'; import { Greeting } from './Greeting'; describe('Greeting', () => { it('should render "Hello World"', () => { render(<Greeting />); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); });
import { render, screen } from '@testing-library/react'; import { Greeting } from './Greeting'; describe('Greeting', () => { it('should render "Hello World"', () => { render(<Greeting />); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); });
import { render, screen } from '@testing-library/react'; import { Greeting } from './Greeting'; describe('Greeting', () => { it('should render "Hello World"', () => { render(<Greeting />); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); });
npm run test
It is a good practice to conduct TDD wherever possible
Let us amend the component such that we can set the name through an name property
it('should render correct name', () => {
render(<Greeting name="React Master" />);
expect(screen.getByText("Hello React Master"))
.toBeInTheDocument();
});
type Props = {
name?: string;
};
export function Greeting({ name = 'World' }: Props) {
return <div>Hello {name}</div>;
}
Refactor your code such that it is extensible and easy to understand
it('should render correct name', () => {
const name = 'React Master';
render(<Greeting name={name} />);
expect(screen.getByText(`Hello ${name}`))
.toBeInTheDocument();
});
it('should render Bulbasaur if no name is passed', () => {
const name = 'Bulbasaur';
render(<PokeListEntry />);
expect(screen.getByText(name)).toBeInTheDocument();
});
it('should render the passed pokémon name', () => {
const name = 'Pickachu';
render(<PokeListEntry name={name} />);
expect(screen.getByText(name)).toBeInTheDocument();
});
it.each(['Bulbasaur', 'Eevee', 'Pickachu'])
('should render the passed pokémon %s', (name) => {
render(<PokeListEntry name={name} />);
expect(screen.getByText(name)).toBeInTheDocument();
});
On your CI pipeline you typically want to ensure the quality of the solution
To render a list of items, we call the
map
function on an array.
In the callback we map the array item to a JSX element.
We can map to an HTML element or to a custom component.
const items = ['Apple', 'Orange', 'Mango']; export function List() { return ( <ul> {items.map( item => <li key={item}>{item}</li> )} </ul> ); }
const items = ['Apple', 'Orange', 'Mango']; export function List() { return ( <ul> {items.map( item => <li key={item}>{item}</li> )} </ul> ); }
const items = ['Apple', 'Orange', 'Mango']; export function List() { return ( <ul> {items.map( item => <li key={item}>{item}</li> )} </ul> ); }
The JSX element returned from map must have a key prop.
This is a special prop used by React internally to distinguish the elements. It must be unique within the list (e.g. an entity ID).
Use a Fragment to return multiple elements from map .
import { Fragment } from "react"; const items = [{ id: 1, … }, { id: 2, … }, { id: 3, … }]; export function List() { return ( <table> <tbody> {items.map( item => ( <Fragment key={item.id}> <tr>…</tr> <tr>…</tr> </Fragment> ) )} <tbody> </table> ); }
import { Fragment } from "react"; const items = [{ id: 1, … }, { id: 2, … }, { id: 3, … }]; export function List() { return ( <table> <tbody> {items.map( item => ( <Fragment key={item.id}> <tr>…</tr> <tr>…</tr> </Fragment> ) )} <tbody> </table> ); }
Here we can't use the shorter <>…</> syntax. Only Fragment can take the key prop.
Like HTML elements, React components can be nested:
export function App() {
return (
<ContentContainer title="Profile">
<ProfilePicture />
<Bio />
<ScrollableSection>
<ProjectList />
</ScrollableSection>
</ContentContainer>
);
}
The parent component receives the child components through the children prop.
import { PropsWithChildren } from "react" type ContentContainerProps = PropsWithChildren<{ title: string }>; function ContentContainer({ title, children, }: ContentContainerProps) { return ( <div className="content-container"> … </div> ); }
function ContentContainer({ title, children, }: ContentContainerProps) { return ( <div className="content-container"> <div className="heading"> <h1>{title}</h1> </div> <div className="content"> {children} </div> </div> ); }
The children prop can be rendered in the parent component's JSX.
import PokeListEntry from "./PokeListEntry";
const pokemons = […];
export function PokeList() {
return (
<ul>
{pokemons.map((pokemon) => (
<PokeListEntry
key={pokemon.name}
name={pokemon.name}
/>
))}
</ul>
);
}
type PokeListEntryProps = {
name: string;
};
export function PokeListEntry({
name
}: PokeListEntryProps) {
return <li>{name}</li>;
}
type LayoutProps = PropsWithChildren;
export function Layout({ children }: LayoutProps) {
return (
<>
<header>
<nav>
<a href="#">Home</a> | <a href="#">Profile</a>
</nav>
</header>
<main>{children}</main>
</>
);
}
import Layout from "./Layout";
import PokeList from "./PokeList";
export function App() {
return (
<Layout>
<PokeList />
</Layout>
);
}
We learned…