The same app in React
The chapter Frameworks compared set Vue, React and Angular side by side. This page shows the catalog of Lorraine Commons with search and favorites in React, just as parts 3 to 5 built it in Vue. In React a component is a function that returns JSX, a syntax that looks like HTML but is JavaScript.
In your project
Section titled “In your project”According to the project assignment, you choose the framework of your project yourself. React pays off if you need it at work or already know it. You know components, props, state, derived values and routing from the course, only the names and some rules change. The table in the section Transfer maps the terms onto each other.
For a new project, react.dev recommends a framework such as Next.js or React Router. You can also build an SPA without a server of its own with Vite, as the example does. Some rules differ from Vue. You never change state directly but set a new value with the function from useState, a copy in the case of an array. You only call Hooks like useState at the top level of the component and never inside a condition, and you only need useEffect to keep the component in sync with something outside React.
A small example
Section titled “A small example”The example lives under optional/react/en and runs with Vite. useState returns the current value and a function that sets a new one. React then runs the whole function App again and draws what has changed. A derived value such as the hits of the search therefore needs no computed, it is simply computed on every run. The search field is controlled, its value comes from the state, and onChange reports every keystroke.
import { useState } from 'react'
import items from '../data/items.json'import { search } from './catalog.js'import ItemCard from './ItemCard.jsx'
export default function App() { // useState returns the current value and a function that sets a new one and draws again const [query, setQuery] = useState('') const [favorites, setFavorites] = useState([])
// A derived value is simply computed while drawing, where Vue uses computed const hits = search(items, query)
// The state lives here, the card only reports the click through its callback prop function toggleFavorite(id) { setFavorites((current) => (current.includes(id) ? current.filter((favorite) => favorite !== id) : [...current, id])) }
return ( <main> <h1>Catalog</h1> <label htmlFor="search">Search</label> <input id="search" type="search" value={query} onChange={(event) => setQuery(event.target.value)} /> <p role="status"> {hits.length} things found, {favorites.length} favorites </p> <ul className="grid"> {hits.map((item) => ( <li key={item.id}> <ItemCard item={item} favorite={favorites.includes(item.id)} onToggleFavorite={toggleFavorite} /> </li> ))} </ul> </main> )}The card gets its props as one object. Instead of emitting an event, it calls the function that the parent gave it as a prop. That way App keeps holding the state of the favorites, as in the chapter State in the right place.
// The props arrive as one object, a function prop takes the place of an emitted eventexport default function ItemCard({ item, favorite, onToggleFavorite }) { return ( <article className="card"> <h2>{item.name}</h2> <p> {item.category}, CHF {item.deposit} deposit </p> <button type="button" aria-pressed={favorite} onClick={() => onToggleFavorite(item.id)}> {favorite ? 'Remove favorite' : 'Save as favorite'} </button> </article> )}The test renders the app with createRoot into an element of happy-dom. act makes sure that React has finished all changes before the test checks anything. Larger projects usually use React Testing Library, which makes it easier to find elements and to type.
import { act } from 'react'import { createRoot } from 'react-dom/client'import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import items from '../data/items.json'import App from '../src/App.jsx'import { search } from '../src/catalog.js'
// React checks that every update in a test runs inside act(), the async form waits for all of themglobalThis.IS_REACT_ACT_ENVIRONMENT = true
let containerlet root
beforeEach(async () => { container = document.createElement('div') document.body.append(container) root = createRoot(container) await act(async () => { root.render(<App />) })})
afterEach(async () => { await act(async () => {Official links
Section titled “Official links”Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.