React

JSX

<h1>Hello, {user.name}</h1>

Attributes & Props

<img tabIndex="0" src={user.avatarUrl}></img>
const selectedPerson = {
    name: "Bob",
    age: 23,
    job: "Developer"
}

<Person {...selectedPerson} />

Classes and styles

<img className="avatar" style={{ height: 10 }} />

Conditional rendering

const button = <LoginButton />;
return (
    <div>
        {button}
    </div>
)
return (
    <div>
        <h1>Hello!</h1>
        {unreadMessages.length > 0 &&
            <h2>
                You have {unreadMessages.length} unread messages.
            </h2>
        }
    </div>
)
return (
    <div>
        {isLoggedIn
            ? <LogoutButton />
            : <LoginButton />
        }
    </div>
)
function WarningBanner(props) {
    {/* will not be rendered if props.warn is falsy */}
    if (!props.warn) {
        return null;
    }

    return (
        <div className="warning">
        Warning!
        </div>
    );
}

Lists & keys

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(number => 
    <li key={number.toString()}>
        {number}
    </li>
);

ReactDOM.render(
    <ul>{listItems}<ul>,
    document.getElementById('root')
);
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <ul>
        {numbers.map(number => 
            <ListItem key={number.toString()} value={number} />
        )}
    </ul>
);

Force a component to re-mount

Components

function Cafe() {
  return (
    <>
      <Cat name="Munkustrap" />
      <Cat name="Spot" />
    </>
  );
};

Component purity

Component rendering should be pure, meaning the same inputs will produce the same output. Any data the component depends on should be stored as props or state.

You can wrap your app in <React.StrictMode> to call each component twice during development to find impure components.

Controlled vs. uncontrolled components

Components that are primarily driven through props are sometimes referred to as controlled components, because their parent controls their behavior. Components that keep their primary information in local state are called uncontrolled components.

Example of a controlled <input>:

const [firstName, setFirstName] = useState('')

return <input value={firstName} onChange={e => setFirstName(e.target.value)} />

Server Components vs. Client Components

'use client'

import { Button } from 'ui-library'
export default Button

cache

import { cache } from 'react';
const getMetrics = cache(calculateMetrics);

getMetrics(user) // will calculate and cache the result
getMetrics(user) // will return the cached result without calling calculateMetrics again
const getUser = cache(async (id) => {
  return await db.user.query(id);
});

async function Profile({id}) {
  // if the getUser call from Page finished, will use the cache
  const user = await getUser(id);
  return (
    <section>
      <img src={user.profilePic} />
      <h2>{user.name}</h2>
    </section>
  );
}

function Page({id}) {
  // start fetching the user data, even though the result isn't used here
  getUser(id);
  // ... some computational work
  return (
    <>
      <Profile id={id} />
    </>
  );
}

Props

function Welcome({ firstName, lastName }) {
  return <h1>Hello, {firstName} {lastName}!</h1>;
}

const firstName = "Sara"
const element = <Welcome firstName={firstName} lastName="Smith" />;

children (slots)

// inside Card.js
function Card({ children }) {
    return (
        <div className="card">
            {children}
        </div>
    )
}

// inside another component
<Card>
    <Avatar />
</Card>

<Card>
    <p>Hello world!</p>
</Card>

Pass a component as a prop

import type { ComponentType } from 'react'
import type { LucideProps } from 'lucide-react'

export interface CardProps {
    // Icon must be a component that accepts LucideProps
    // capitalized name tells React to treat it as a component
    Icon?: ComponentType<LucideProps>
}

export default function Card({ Icon }: CardProps) {
    return (
        <div>
            {Icon && <Icon size={50} />}
            <!-- other stuff -->
        </div>
    )
}

// usage
import { Database } from 'lucide-react'

<Card Icon={Database} />

Render a tag name from a prop

export interface ComponentProps {
    // Tag must be a valid HTML tag name
    // capitalized name tells React to treat it as a component
    Tag?: keyof JSX.IntrinsicElements
}

export default function Component({ Tag = 'div' }: ComponentProps) {
    return <Tag>Hello!</Tag>
}

// usage

<Component /> // renders <div>Hello!</div>
<Component Tag="article" /> // renders <article>Hello!</article>

Events

function MyButton() {
    function activateLasers() {
        alert('Lasers activated!')
    }

    return (
        <button onClick={activateLasers}>
            Activate Lasers
        </button>
    )
}
render() {
    return (
        <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
        <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
    )
}

Actions

async function createUser(formData) {
    await fetch('/user', {
        method: 'POST',
        body: {
            name: formData.get('name'),
            email: formData.get('email'),
        }  
    })
}

<form action={createUser}>
    <input name="name" placeholder="name" />
    <input name="email" placeholder="email" />
    <button type="submit">Submit</button>
</form>
const [userId, setUserId] = useState(123)

async function createUser(userId, formData) { /* ... */ }

const createUserWithId = createUser.bind(null, userId)

Server Actions

export async function createUser(formData) {
    'use server'
    await db.createUser({
        name: formData.get('name'),
        email: formData.get('email'),
    })
}
import { createUser } from '@/util/actions'

export default function UserForm() {
    return (
        <form action={createUser}>
            <input name="name" placeholder="Name" />
            <input name="email" placeholder="Email" />
            <button type="submit">Submit</button>
        </form>
    )
}

Hooks

Rules of Hooks

useState

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
// bad - createTodosExpensive is called on every render, but only used on the first render
const [todos, setTodos] = useState(createTodosExpensive(todoData))

// good - initializeTodos is called only once
const initializeTodos = () => createTodosExpensive(todoData)
const [todos, setTodos] = useState(initializeTodos)
const [number, setNumber] = useState(0);

// incorrect
setNumber(number + 1) // 0 + 1
setNumber(number + 1) // 0 + 1
setNumber(number + 1) // 0 + 1

// the final value of `number` will be 1
const [number, setNumber] = useState(0);

// correct
setNumber(number => number + 1) // 0 + 1
setNumber(number => number + 1) // 1 + 1
setNumber(number => number + 1) // 2 + 1

// the final value of `number` will be 3
// contrived example - Counter will keep its state when isFancy changes
return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
    </div>
)

useEffect

const [port, setPort] = useState(3000);

// when port changes, the component will disconnect from the old port and connect to the new one
useEffect(() => {
    connectToPort(3000)
    return () => { disconnectFromPort(3000) };
}, [port])

useMemo

const sortTodos = useMemo(
    () => todos.sort(sortFunction),
    [todos, sortFunction]
)

memo

const Greeting = memo(function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>
})
function GroupsLanding({ person }) {
  const hasGroups = person.groups !== null
  return <CallToAction hasGroups={hasGroups} />
}

useCallback

const handleSubmit = useCallback((orderDetails) => {
    // ... function body here
}, [productId, referrer])

Higher-order functions

// incorrect
useCallback(_.debounce(myFunction, 100), [myFunction])
// correct
useMemo(() => _.debounce(myFunction, 100), [myFunction])

useRef

const ref = useRef(0) // can be any value, like useState

// ref looks like this
{
    current: 0
}

useEffect(() => {
    // ref.current is mutable, but should only be read and written
    // in effects or handlers
    ref.current = ref.current + 1
})
// instead of this
const ref = useRef(new ExpensiveClass())

// do this
const ref = useRef(null)
if (playerRef.current === null) {
    playerRef.current = new ExpensiveClass()
}

DOM element refs

const myRef = useRef(null)

<div ref={myRef}>

Refs for multiple DOM elements

const rows = useRef(null)
function getRowMap() {
    if (!rows.current) {
        // initialize the map only once
        rows.current = new Map()
    }
    return rows.current
}

return (
    <tbody>
        {data.map((row) => (
            <tr key={row.id} ref={(node) => {
                const map = getRowMap()
                node ? map.set(row.id, node) : map.delete(index)
            }}></tr>
        ))}
    </tbody>
)
return (
    <tbody>
        {data.map((row) => (
            <tr key={row.id} ref={(node) => {
                const map = getRowMap()
                map.set(row.id, node)
                return () => map.delete(row.id)
            }}></tr>
        ))}
    </tbody>
)

forwardRef for component refs

const MyInput = ({ ref, ...props }) => {
    return <input {...props} ref={ref} />
}

// inside another component
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus Input</button>
const MyInput = forwardRef((props, ref) => {
    return <input {...props} ref={ref} />
})

// inside another component
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus Input</button>

useImperativeHandle

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);

  function getValue() {
    return inputRef.current?.value;
  }

  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus();
      },
      getValue
    };
  }, []);

  return <input {...props} ref={inputRef} />;
});

Cleanup

<input ref={(ref) => {
    // ref creation logic here
    return () => {
        // cleanup here
    }
}}

useId

import { useId } from 'react'

export default function checkboxWithLabel() {
    const id = useId()

    return (
        <input type="checkbox" id={id}></input>
        <label htmlFor={id}>Checkbox</label>
    )
}

useFormStatus

import { useFormStatus } from 'react-dom'

export default function SubmitButton() {
    const { pending } = useFormStatus()

    return (
        <button type="submit" disabled={pending}>
            {pending ? 'Please wait...' : 'Submit'}
        </button>
    )
}

useActionState/useFormState

/* actions.js */
export async function createUser(currentState, formData) {
    'use server'
    const user = await db.createUser({
        name: formData.get('name'),
        email: formData.get('email'),
    })
    return user
}
import { useActionState } from 'react'
import { createUser } from '@/util/actions'

export default function UserForm() {
    const [user, createUserForm, pending] = useActionState(createUser, null)

    return (
        <h2>Sign Up</h2>
        <form action={createUserForm}>
            <input name="name" placeholder="Name" />
            <input name="email" placeholder="Email" />
            <button type="submit" disabled={pending}>
                {pending ? 'Please wait...' : 'Submit'}
            </button>
        </form>

        { user ? (
            <h2>Your Profile</h2>
            <UserInfo user={user} />
        ) : null}
    )
}

useReducer

function tasksReducer(tasks, action) {
    // return next state
    switch (action.type) {
        case 'added':
            return [
                // tasks.push won't work because you can't mutate state
                ...tasks,
                {
                    id: action.id,
                    text: action.text
                }
            ]
        case 'deleted':
            return tasks.filter(t => t.id !== action.id)
    }
}
import { useReducer } from 'react'

// instead of
// const [tasks, setTasks] = useState(initialTasks)
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}

useOptimistic

import { useOptimistic } from 'react'

const updateNameAction = async (formData) => {
    const newName = formData.get('name')
    // start showing the optimistic state
    setOptimisticName(newName)
    const updatedName = await updateNameInDatabase(newName)
    // if updateNameInDatabase was successful,
    // update the current state
    onUpdateName(updatedName)
}

export default function changeName({ currentName, onUpdateName }) {
    const [optimisticName, setOptimisticName] = useOptimistic(currentName)

    return (
        <form action={updateNameAction}>
            <p>Your name is: {optimisticName}</p>
            <label>
                <span>Change Name:</span>
                <input name="name" disabled={currentName !== optimisticName} />
            </label>
        </form>
    )
}

Custom Hooks

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    /* ... */
  });

  return isOnline;
}

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
}

useDebugValue

useDebugValue(isOnline ? 'Online' : 'Offline');

Suspense

import { Suspense } from 'react';

{/* LoadingSpinner will be shown until both async components finish loading */}
<Suspense fallback={<LoadingSpinner />}>
    <AsyncComponentOne />
    <AsyncComponentTwo />
</Suspense>

lazy

import { lazy } from 'react'

const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'))

/* the result is an async component */
<Suspense fallback="Loading...">
    <MarkdownPreview />
</Suspense>

startTransition and useTransition

import { useState, startTransition } from 'react'

function PageView() {
    const [page, setPage] = useState(0)

    function switchPage() {
        startTransition(() => setPage(page === 0 ? 1 : 0))
    }

    // assume FirstPage and SecondPage are async components
    return (
        <div>
            <Button onClick={switchPage}>Switch Page</button>
            page === 0 ? <FirstPage /> : <SecondPage />
        </div>
    )
}

import { useState, useTransition } from 'react'

function PageView() {
    const [page, setPage] = useState(0)
    const [isPending, startTransition] = useTransition()

    function switchPage() {
        startTransition(() => setPage(page === 0 ? 1 : 0))
    }

    // assume FirstPage and SecondPage are async components
    return (
        <div>
            <Button disabled={isPending} onClick={switchPage}>
                {isPending ? 'Loading...' : 'Switch Page'}
            </button>
            page === 0 ? <FirstPage /> : <SecondPage />
        </div>
    )
}

useDeferredValue

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

use

'use client'
import { use } from 'react'

export default function UserInfo({ userPromise }) {
    const userData = use(userPromise)

    return (
        <div>
            <span>{user.name}</span>
            {/* etc. */}
        </div>
    )
}
/* this is a Server Component */
export default function ProfilePage() {
    const userDataPromise = db.getUserById(123) // not awaited

    return (
        {/* promise is passed from server to client as a prop */}
        <Suspense fallback="Loading user data...">
            <UserInfo userPromise={userDataPromise} />
        </Suspense>
    )
}

Error Boundaries

Context

import { createContext } from 'react'

export const LevelContext = createContext(1)
import { LevelContext } from './LevelContext'

/* here the context value (`level`) is passed as a prop to the higher level component, but it could come from state or anywhere */
export default function Section({ level, children }) {
    return (
        <section>
            {/* if anything inside the context provider asks for `LevelContext`, it will get the value of `level` */}
            <LevelContext.Provider value={level}>
                {children}
            </LevelContext.Provider>
        </section>
    )
}
<LevelContext value={level}>
    {children}
</LevelContext>
import { useContext } from 'react'
import { LevelContext } from './LevelContext'

export default function Heading({ children }) {
    const level = useContext(LevelContext)

    return (/* ... */)
}
const level = use(LevelContext)

Metadata (\<head\> tags)

export default function BlogPost({ post }) {
    return (
        <article>
            <title>{post.title}</title>
            <meta name="author" content={post.author} />
            <link rel="author" href={post.authorUrl} />
            ...
        </article>
    )
}

TypeScript

import type { PropsWithChildren } from 'react'

interface PersonProps extends PropsWithChildren {
  name: string
  age?: number
}
function Person({ name, age, children }: PersonProps) {
  /* ... */
}

// PropsWithChildren can also accept a type argument
interface CardProps {
  title: string
}
function CardWithChildren = (
  { title, children }: PropsWithChildren<CardProps>
) {
  /* ... */
}
type PageProps = ComponentProps<typeof Page>
type ButtonProps extends HTMLProps<HTMLButtonElement> {
    primary?: boolean
}

function Button({ className, children, primary }: ButtonProps) {
    /* ... */
}
const buttonStyle: React.CSSProperties = {
    backgroundColor: Colors.blue;
}

return <button style={buttonStyle}></button>

Component and element types

Prop types with generics

interface DataGridProps<T extends { id: any }> {
    /* data is an array of objects with an `id` property of any type */
    data: T[]
    selectedIds: Set<T>
    onSelect: (id: T['id']) => void
}

function DataGrid<T extends { id: any }>(props: DataGridProps<T>) {
    /* ... */
}

forwardRef

const ButtonWrapper = forwardRef<HTMLButtonElement, ButtonProps>(
    { label, children }, // type ButtonProps
    ref // type ForwardedRef<HTMLButtonElement>
) {
    return <button ref={ref}>...</button>
}

Type HOC using forwardRef

https://codesandbox.io/p/sandbox/sleepy-kilby-1zyof?file=%2Fsrc%2FwithStatusMessages.tsx

function withStatusMessages<P extends object>(
  WrappedComponent: React.ComponentType<P>
): React.FunctionComponent<P & withStatusMessagesProps> {
  return ({ errorText, successText, ...props }) => {
    return (
      <>
        <WrappedComponent {...props as P} />
        {errorText ? <div className="errorText">{errorText}</div> : null}
        {successText ? <div className="successText">{successText}</div> : null}
      </>
    );
  };
}

Creating a project

create-react-app

Warning

create-react-app is deprecated and not as performant as other solutions. Try #Vite instead for a simple app, or Next.js for a full-featured app with routing.

Vite

npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template react-swc-ts

Next.js

Next.js

Libraries

React Query/TanStack Query

React Query

Flip Move

const Item = forwardRef((props, ref) => (
    <div ref={ref}>{props.name}</div>
))

<FlipMove>
    {listItems.map(item => {
        <Item key={item.id} {...item} />
    })}
</FlipMove>

See also