Tag logo

Short notes about React

Aug 11, 2024/
#react
/-12 min

Living Article is a term I coined that refers to a piece of writing which can be useful right now, but also has the potential to be enriched with additional content in the future.

I recently went through the new React documentation and took notes on the important parts. I will share these with you along with examples.

Common

  • Strict Mode of React calls each component function twice during development and helps you catch impure components. Keep this in mind when counting render times.

  • Code that runs only once at the start of the application can be placed outside of the

    App
    component.

    1if (typeof window !== "undefined") {
    2  checkAuthToken();
    3  loadDataFromLocalStorage();
    4}
    5
    6function App() {
    7  // ...
    8}

Rendering

  • Component is a function that return HTML elements. They use a syntax extension called JSX that looks like HTML but is actually typescript. Since it's still typescript, it makes working with elements in JS easier. Ultimately, JSX is also transpiled into HTML.

    1function MyButton() {
    2  return <button>I'm a button</button>;
    3}
    4
    5export default function MyApp() {
    6  return (
    7    <div>
    8      <h1 className="heading-1">Welcome to my app</h1>
    9      <MyButton />
    10    </div>
    11  );
    12}
  • If there is no child item, always use the closing tag.

    1<CustomElement />
    2// not <CustomElement></CustomElement>
  • We can't return multiple elements in a component. We need to wrap them with

    <div></div>
    or
    <></>
    which is called a fragment. Second one avoids adding an extra element to the HTML. This is because a function can't return more than one value.

    1<>
    2  <Header />
    3  <Main />
    4  <Footer />
    5</>
  • We can use typescript variables and expressions in JSX with curly braces.

    1const user = {
    2  fullName: "Hedy Lamarr",
    3  imageUrl: "https://i.imgur.com/yXOvdOSs.jpg",
    4  imageSize: 90,
    5};
    6
    7export default function Profile() {
    8  return (
    9    <>
    10      <h1>{user.fullName}</h1>
    11      <img
    12        className="avatar"
    13        src={user.imageUrl}
    14        alt={`Photo of ${user.fullName}`}
    15        style={{
    16          width: user.imageSize,
    17          height: user.imageSize,
    18        }}
    19      />
    20    </>
    21  );
    22}
  • We can use three different approaches for conditional rendering.

    false
    ,
    null
    , and
    undefined
    values are ignored in JSX.

    1// first way
    2let content;
    3
    4if (isLoggedIn) {
    5  content = <AdminPanel />;
    6} else {
    7  content = <LoginForm />;
    8}
    9return <div>{content}</div>;
    10
    11// second way
    12<div>{isLoggedIn ? <AdminPanel /> : <LoginForm />}</div>
    13
    14// third way
    15<div>
    16  {isLoggedIn && <AdminPanel />}
    17  {!isLoggedIn && <LoginForm />}
    18</div>
  • We can use three different approaches for conditional rendering.

    false
    ,
    null
    and
    undefined
    values returned from conditions are ignored in JSX.

    1// first way: if statement
    2if (isPacked) {
    3  return <li className="item">{name}</li>;
    4}
    5
    6return <li className="item">{name}</li>;
    7
    8// second way: ternary operator
    9return <li className="item">{isPacked ? name + " ✔" : name}</li>;
    10
    11// third way: and operator
    12return (
    13  <li className="item">
    14    {name} {isPacked && "✔"}
    15  </li>
    16);
  • We can use JSX arrays to render multiple elements. Just make sure to give each element a

    key
    attribute to ensure that React renders the correct elements even if the array changes.

    1const products = [
    2  { title: "Cabbage", id: 1 },
    3  { title: "Garlic", id: 2 },
    4  { title: "Apple", id: 3 },
    5];
    6
    7const listItems = products.map((product) => (
    8  <li key={product.id}>{product.title}</li>
    9));
    10
    11return <ul>{listItems}</ul>;

If the key property of the values is the same, you will receive a warning message: "Warning: Each child in a list should have a unique 'key' prop.".

Even if the component does not change, it is rerendered when the key changes. Therefore, if you get the value from a function that produces a different value each time, such as Math.random, you will cause unnecessary renders.

  • In React, the DOM is updated after the code block completes when the state changes. If we need to access the new DOM immediately after a state change, we can use the

    flushSync
    method.

    1import { useState, useRef } from "react";
    2import { flushSync } from "react-dom";
    3
    4export default function TodoList() {
    5  const listRef = useRef(null);
    6  const [todos, setTodos] = useState(initialTodos);
    7
    8  function handleAdd() {
    9    flushSync(() => {
    10      setTodos([...todos, { id: nextId++, text: text }]);
    11    });
    12
    13    listRef.current.lastChild; // can access new todo dom element
    14  }
    15
    16  return (
    17    <>
    18      <button onClick={handleAdd}>Add</button>
    19      <input value={text} onChange={(e) => setText(e.target.value)} />
    20      <ul ref={listRef}>
    21        {todos.map((todo) => (
    22          <li key={todo.id}>{todo.text}</li>
    23        ))}
    24      </ul>
    25    </>
    26  );
    27}

Event Handler

  • Event handler is a function that will be executed when a user triggers an event. We can pass these functions as props to component. By default, the

    Event
    object is passed to the function. If we want to send extra data, we can use an arrow function.

    1function MyButton() {
    2  function handleClick() {
    3    alert("You clicked me!");
    4  }
    5
    6  return (
    7    // <button onClick={(e) => handleClick(e, customProp)}>
    8    <button onClick={handleClick}>Click me</button>
    9  );
    10}

As you can see, we are passing the reference of the function. If we had written

onClick={handleClick()}
, it would only have been triggered during every render.

  • It's common to start naming with

    on
    for event props and
    handle
    for handler. For example:
    onMouseEnter={handleMouseEnter}
    .

  • Event propagation is also works with components. Looking at the example code, first the button's

    onClick
    and then div's
    onClick
    is called. All events except
    onScroll
    propagate upwards. To prevent this,
    e.stopPropagation()
    is used.

    1export default function Toolbar() {
    2  return (
    3    <div
    4      className="Toolbar"
    5      onClick={() => {
    6        alert("You clicked on the toolbar!");
    7      }}
    8    >
    9      <button
    10        onClick={() => {
    11          e.stopPropagation();
    12          alert("Playing!");
    13        }}
    14      >
    15        Play Movie
    16      </button>
    17    </div>
    18  );
    19}
  • Some events, such as the

    onSubmit
    event of a form element, have default browser behavior and will execute their own code after your code. To prevent this, we can use
    e.preventDefault
    .

  • To execute event handlers from top to bottom (capturing), we can use

    onClickCapture
    instead of
    onClick
    .

    1<div
    2  onClick={() => {
    3    alert("Call 1");
    4  }}
    5>
    6  <div
    7    onClickCapture={() => {
    8      alert("Call 2");
    9    }}
    10  >
    11    <div
    12      onClickCapture={() => {
    13        alert("Call 3");
    14      }}
    15    >
    16      Click!
    17    </div>
    18  </div>
    19</div>
    Call 2
    Call 3
    Call 1

Props

  • We use props to pass data to a component. It's the same logic as passing params to a function.

    1function Avatar({ person, size = 100 }) {
    2  // ...
    3}
    4
    5function Profile() {
    6  return (
    7    <div className="card">
    8      <Avatar person="Enes" size={80} />
    9    </div>
    10  );
    11}
  • We can get children elements from the

    children
    props.

    1function Heading({ children }) {
    2  return <div className="heading">{children}</div>;
    3}
    4
    5export default function Text() {
    6  return <Heading>Hello</Heading>;
    7}

State

  • We use state for values that can change over time and cause the screen to be updated. It is like a component-based memory. The difference from classic variables is that value is preserved between renders.
  • State is associated with the location of the component in the DOM. Otherwise, it is preserve its own value. If we want to reset the state, we can change the
    key
    parameter.
  • To define state in React, we use the
    useState
    hook.
1import { useState } from "react";
2
3function MyButton() {
4  // count: current value
5  // setCount: setter function
6  // 0: initial value
7  const [count, setCount] = useState(0);
8
9  function handleClick() {
10    setCount(count + 1);
11  }
12
13  return <button onClick={handleClick}>Clicked {count} times</button>;
14}

Functions that start with "use" are called hooks in React. They essentially allow us to access React's features. They can only be used in the top scope of components and should not be used inside conditions or loops. If necessary, we can create a new component.

  • State setter çağırıldıktan sonra kod bloğu tamamlandığında rerender tetiklenir.

  • React states are stored in an array and should only be defined at the root of the component. They are read the value in the order they are called. That's why they should only be defined at the root of the component.

  • We should use the spread operator when updating it.

    1const [person, setPerson] = useState({
    2  name: "Niki de Saint Phalle",
    3  artwork: {
    4    title: "Blue Nana",
    5    city: "Hamburg",
    6    image: "https://i.imgur.com/Sd1AgUOm.jpg",
    7  },
    8});
    9
    10function handleTitleChange(e) {
    11  setPerson({
    12    ...person,
    13    artwork: {
    14      ...person.artwork,
    15      title: e.target.value,
    16    },
    17  });
    18}
  • The state value doesn't change immediately when we call the setter method. So, if we call the setNumber method three times, we won't get the final value of 3. Instead, each time the setter is called, the state value remains as the value read at the start of render (in this case 0). The state value is updated in memory only after the onClick block executed and a render is triggered. To access the current value of the component in memory, we can use the updater function.

    1import { useState } from "react";
    2
    3export default function Counter() {
    4  const [count, setCount] = useState(0);
    5
    6  return (
    7    <>
    8      <h1>{count}</h1>
    9      <button
    10        onClick={() => {
    11          setCount(count + 1); // setCount(0 + 1)
    12          setCount(count + 1); // setCount(0 + 1)
    13          setCount(count + 1); // setCount(0 + 1)
    14          // the value will be 1 in the next render.
    15        }}
    16      >
    17        +3
    18      </button>
    19      <button
    20        onClick={() => {
    21          setCount((count) => count + 1); // setCount(0 + 1)
    22          setCount((count) => count + 1); // setCount(1 + 1)
    23          setCount((count) => count + 1); // setCount(2 + 1)
    24          // the value will be 3 in the next render.
    25        }}
    26      >
    27        +3
    28      </button>
    29    </>
    30  );
    31}

Reducer

  • We may have difficulty managing complex states. Reducer establishes an action-based state update mechanism. We can use the

    useReducer
    hook to define a reducer.

    1import { useReducer } from "react";
    2
    3function tasksReducer(tasks, action) {
    4  switch (action.type) {
    5    case "add": {
    6      return [
    7        ...tasks,
    8        {
    9          id: action.id,
    10          text: action.text,
    11          done: false,
    12        },
    13      ];
    14    }
    15    case "change": {
    16      return tasks.map((t) => {
    17        if (t.id === action.task.id) {
    18          return action.task;
    19        } else {
    20          return t;
    21        }
    22      });
    23    }
    24    case "delete": {
    25      return tasks.filter((t) => t.id !== action.id);
    26    }
    27    default: {
    28      throw Error("Unknown action: " + action.type);
    29    }
    30  }
    31}
    32
    33const initialTasks = [
    34  { id: 0, text: "Visit Kafka Museum", done: true },
    35  { id: 1, text: "Watch a puppet show", done: false },
    36  { id: 2, text: "Lennon Wall pic", done: false },
    37];
    38
    39const useTasksReducer = () => useReducer(tasksReducer, initialTasks);
    40
    41export { tasksReducer, useTasksReducer };
    1import AddTask from "./AddTask.js";
    2import TaskList from "./TaskList.js";
    3import { useTasksReducer } from "./tasksReducer.js";
    4
    5export default function TaskApp() {
    6  const [tasks, dispatch] = useTasksReducer();
    7
    8  function handleAddTask(text) {
    9    dispatch({
    10      type: "add",
    11      id: nextId++,
    12      text,
    13    });
    14  }
    15
    16  function handleChangeTask(task) {
    17    dispatch({
    18      type: "change",
    19      task,
    20    });
    21  }
    22
    23  function handleDeleteTask(taskId) {
    24    dispatch({
    25      type: "delete",
    26      id: taskId,
    27    });
    28  }
    29
    30  return {
    31    // ...
    32  };
    33}

Ref

  • Ref is used to access HTML elements, so there is no need to deal with

    querySelector
    . We can use the
    useRef
    hook to define a reference.

    1import { useRef } from "react";
    2
    3export default function Form() {
    4  const inputRef = useRef(null);
    5
    6  function handleClick() {
    7    inputRef.current.focus();
    8  }
    9
    10  return (
    11    <>
    12      <input ref={inputRef} />
    13      <button onClick={handleClick}>Focus the input</button>
    14    </>
    15  );
    16}
  • The ref value cannot be read or modified in the component body because it is inaccessible during DOM rendering. It can only be accessed inside

    useEffect
    and event handler that we are sure will run after rendering.

    1function VideoPlayer({ src, isPlaying }) {
    2  const ref = useRef(null);
    3
    4  // WRONG! this will crash.
    5  if (isPlaying) {
    6    ref.current.play();
    7  } else {
    8    ref.current.pause();
    9  }
    10
    11  // CORRECT! instead we can use useEffect
    12  useEffect(() => {
    13    if (isPlaying) {
    14      ref.current.play();
    15    } else {
    16      ref.current.pause();
    17    }
    18  });
    19
    20  return <video ref={ref} src={src} loop playsInline />;
    21}
  • It can also be used to store a value because it is preserved during renders. Unlike state, it does not trigger a rerender when the value changes.

    1import { useRef } from "react";
    2
    3export default function Counter() {
    4  let count = useRef(0);
    5
    6  function handleClick() {
    7    count.current = count.current + 1;
    8    alert("You clicked " + count.current + " times!");
    9  }
    10
    11  return (
    12    <div>
    13      <p>Click count: {count.current}</p>
    14      <button onClick={handleClick}>Click me!</button>
    15    </div>
    16  );
    17}
  • We can use a single reference to keep track of the references of multiple elements. However, when an element is deleted, it also needs to be removed from this reference. We can use a ref callback for this.

    1import { useRef } from "react";
    2
    3export default function CatFriends() {
    4  const itemsRef = useRef(new Map());
    5
    6  return (
    7    <ul>
    8      {catList.map((cat) => (
    9        <li
    10          key={cat.id}
    11          ref={(node) => {
    12            if (node) {
    13              itemsRef.current.set(cat.id, node);
    14            } else {
    15              itemsRef.current.delete(cat.id);
    16            }
    17          }}
    18        >
    19          ...
    20        </li>
    21      ))}
    22    </ul>
    23  );
    24}
  • We may want to assign a ref to an HTML element inside a component. The ref we passed as a prop is not automatically added to the DOM. To pass this, we need to use

    forwardRef
    .

    1import { forwardRef, useRef } from "react";
    2
    3const MyInput = forwardRef((props, ref) => {
    4  return <input {...props} ref={ref} />;
    5});
    6
    7export default function Form() {
    8  const inputRef = useRef(null);
    9
    10  return <MyInput ref={inputRef} />;
    11}

Context

  • Context provides data flow from the component to sub components. Instead of passing the props 100 times down between components, we can define a context and read it with

    useContext
    hook.

    1import React, { createContext, useContext, useState } from "react";
    2
    3// firstly we create context
    4const LanguageContext = createContext();
    5
    6// the provider allows to access context for children
    7const LanguageProvider = ({ children }) => {
    8  const [language, setLanguage] = useState("en");
    9
    10  // value props store the provided context's values
    11  return (
    12    <LanguageContext.Provider value={{ language, setLanguage }}>
    13      {children}
    14    </LanguageContext.Provider>
    15  );
    16};
    17
    18// we can read the values with useContext hook
    19const useLanguageContext = () => useContext(LanguageContext);
    20
    21export { LanguageProvider, useLanguageContext };
    1import { LanguageProvider } from "./LanguageContext.tsx";
    2import { Navbar } from "./Navbar.tsx";
    3import { PageTitle } from "./PageTitle.tsx";
    4
    5export default function App() {
    6  return (
    7    <div className="App">
    8      <LanguageProvider>
    9        <Navbar />
    10        <PageTitle />
    11      </LanguageProvider>
    12    </div>
    13  );
    14}
    1import { useLanguageContext } from "./LanguageContext.tsx";
    2
    3export const PageTitle = () => {
    4  const { language } = useLanguageContext();
    5
    6  const titleByLang = {
    7    en: "Welcome to my App!",
    8    tr: "Uygulamama hoşgeldiniz!",
    9  };
    10
    11  return <h1>{titleByLang[language]}</h1>;
    12};
    1import { useLanguageContext } from "./LanguageContext.tsx";
    2
    3export const Navbar = () => {
    4  const { setLanguageHandler } = useLanguageContext();
    5
    6  return (
    7    <nav>
    8      <button onClick={() => setLanguageHandler("en")}>English</button>
    9      <button onClick={() => setLanguageHandler("tr")}>Türkçe</button>
    10    </nav>
    11  );
    12};

Effect

  • It is used to perform side effects caused by rendering. For example, we can use it to save analytic data when the page is loaded or to establish a connection with the server in a chat application. We use the useEffect hook.

    1useEffect(() => {
    2  // run after every render
    3});
    4
    5useEffect(() => {
    6  // run after every render when dependency changed (will compare with Object.is)
    7}, [someCompVariable, someProps, someState]);
    8
    9useEffect(() => {
    10  // run on mount (appears on the screen for the first time)
    11
    12  return () => {
    13    // run on unmount
    14  };
    15}, []);
  • It doesn't work during server-side.