Nathan Lamont

Notes to Self

React

WHEREAS I am out of work and vuejobs.com only lists 1 or 2 jobs I'm potentially suited for a month, and whereas I see many many more jobs of interest that require React knowledge, I am reluctantly going to learn it.

Notes

Components are called components. Exports function with object argument, the keys of which are "props."

Refs are part of "state"

import { useState } from 'react';

export default function SomeComponent() {
  const [name, setName] = useState('nathan')

  function changeName(newName: string) {
    setName(newName)
  }
}

What about objects? You can't mutate them. You have to copy them, e.g.

import { useState } from 'react';

export default function SomeComponent() {
  const [bio, setBio] = useState({
    name: 'nathan',
    address: {
      steet: '123 Fancy St.',
      town: 'Lovelywood'
    }
  })

  function changeTown(newTown: string) {
    setBio({
      ...bio,
      address: {
        ...bio.address,
        town: newTown
      }
    })
  }
}

Which is a PITA. Immer is a package that makes it easier with magic.

import { useImmer } from 'use-immer';

export default function SomeComponent() {
  const [bio, updateBio] = useImmer({
    name: 'nathan',
    address: {
      steet: '123 Fancy St.',
      town: 'Lovelywood'
    }
  })

  function changeTown(newTown: string) {
    updateBio( draft => {
      draft.address.town = newTown
    })
  }
}

Note the convention of using update[Type] instead of set[Type]

Works with arrays as well, e.g. you can mutate an object in the array.

You can also pass the object instead of the function, which appears to work like standard useState

Also: you can apparently call the set function with a function parameter (instead of a value), which gets passed the current value. Like this:

const [messages, setMessages] = useState([]);
// ... later
setMessages(msgs => [...msgs, receivedMessage]);

Reducer

Organizational tool to handle state. Uses "dispatch" and "action" but that does not imply a promise/asynchronous function. Convention is "what happened" (past tense) not "do this" (imperative). Action has no predefined structure?

!note

Yes you were correct, no aync: "Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects…"

Reducer takes two arguments: the thing to change, and the action to perform. The thing to change is the only thing that should be changed. Same rules apply as to calling setState

Why "reducer" (hate it)? Named after array's reduce function. "React reducers are an example of the same idea: they take the state so far and the action, and return the next state." Lame. Encapsulates why you don't like react. Non-ergonomic. Non-thoughtful naming.

import { useReducer } from 'react';

export default function SomeComponent() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  )
  // e.g.
  function handleAddTask(name) {
    dispatch({
      type: 'added', id: nextId++, text: name
    })
  }
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    // ... etc.
}

And yes there is an "Immer" flavor:

import { useImmerReducer } from 'use-immer';

So you can mutate objects/arrays/etc. (but only if it's the first argument!)

Context

To provide state across component hierarchy.

e.g.


// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(0);

// Section.js

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}

// App.js
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
        </Section>
      </Section>
    </Section>
  );

Reducer and Contexts are used in tandem

What about routes? What about onMounted beforeUnmouted.

Redux = store, pinia

I don't think there's a computed equivalent. Since the whole function of a component is called with each update, "computed" values are basically every var that's not part of state.

e.g.

const [firstName, setFirstName] = useState('Cosmo')
const [lastName, setLastName] = useState('Brown')
const fullName = `${firstName} ${lastName}` // set every render

"children" = "slot" -- as a magic parameter, e.g.

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        First panel content. This is "children."
      </Panel>
      <Panel title="Etymology">
        Second panel content. This is "children."
      </Panel>
    </>
  );
}

You can pass... handler functions? As properties.

export default function ParentComponent() {
  function 
  return (
    <ChildComponent onTest={() => console.log("hello world")} />
  )
}

function ChildComponent({ onTest }) {
  return (
    <button onClick={ onTest }>Test</button>
  )
}

This is legal

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

Where each instance is independent (they do not share state)

Normally state is attached to place in tree. Use key to be more specific.

functions that start with use are magic Custom Hooks and let you use other hooks, like useContext inside of them.

The idea is that you combine contexts and reducers into a single file (for a single concern), like this:


// TasksContext.js

import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

// App.js

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

// AddTask.js, e.g.

import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useTasksDispatch();
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;

Refs

Refs are like state but do not trigger re-renders. Used like Vue refs, with this syntax:

const someRef = useRef(123)

// ...later

if (someRef.current === 123) {
  // ...
}

Typical use cases: timeout IDs, DOM element. You think, vars that persist over rerenders.

Also, like vue, refs can magically refer to DOM elements.

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Note that you should not use such a ref during a render - that is, during the top-level stuff in the function. The dom does not exist yet.

No free array of refs for dom. You can do it, but it looks like a PITA:

One possible way around this is to get a single ref to their parent element, and then use DOM manipulation methods like querySelectorAll to “find” the individual child nodes from it. However, this is brittle and can break if your DOM structure changes.

Another solution is to pass a function to the ref attribute. This is called a ref callback. React will call your ref callback with the DOM node when it’s time to set the ref, and with null when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID.

import { useRef } from 'react';

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}


If you want to get a ref to a child component's dom element, you must use forwardRef. Looks like this:

import { forwardRef, useRef } from 'react';

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

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

For more control over what gets exposed, you can use useImperitiveHandle?

If you have an action that updates the dom, and you want to immediately act on it you can use flushSync

flushSync(() => {  
  setTodos([ ...todos, newTodo]);  
});  

listRef.current.lastChild.scrollIntoView();

useEffect

More later, but appears to be sort of like a watcher? Or beforeMount() + beforeUnmount()?

Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time.

So, it's like a watcher that: if it references a property that changes (what about a state?), and the property changes, then first the "unmount" function gets called, then the main function gets called with the new values.

Effects re-synchronize if any of the values they read, like props or state, are different than during last render. Sometimes, you want a mix of both behaviors: an Effect that re-runs in response to some values but not others.

All code inside Effects is reactive. It will run again if some reactive value it reads has changed due to a re-render.

But if you call a function from within the effect function, it will NOT be magically watched.

But yes it does appear to be where you add/remove event listeners as well.

import { useState, useEffect } from 'react';

export function usePointerPosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
  }, []);
  return position;
}

There is some way to specify dependencies? Yes in array argument.

useEffect is run after every render.

So, one use case, is to use useEffect to delay until after rendering, like onMount

!warning You shouldn't change state inside a useEffect?

// infinite loop
const [count, setCount] = useState(0);  
useEffect(() => {  
  setCount(count + 1);  
});

What if you had a mounted flag?

Declaring Dependency

The second argument is an array of dependencies.

useEffect(() => {
    // isPlaying is a property
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  }, []); // lint error: missing dependency: 'isPlaying'

Ugh. Look:

useEffect(() => {
  // This runs after every render
});

useEffect(() => {
  // This runs only on mount (when the component appears)
}, []);

useEffect(() => {
  // This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);

As implied before, the return value of the function passed to useEffect is the "cleanup" function. It gets called before each call to useEffect (after the first) and once on unmount.

In development, useEffect is immediately called twice (this is to surface unmounting problems).

For network requests, you need a way to ignore results of aborted requests, like this:

useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

Note that the scope of ignore is within the returned function.

Docs state that you should not use an effect "if you want to update a component’s state when some props or state change" — so what do you do?

Here’s an example of reacting to a prop change.

// DO NOT DO THIS
function List({ items }) {  
  const [isReverse, setIsReverse] = useState(false);  
  const [selection, setSelection] = useState(null);  

  // 🔴 Avoid: Adjusting state on prop change in an Effect  
  // this will cause 2 unnessary renders
  useEffect(() => {  
    setSelection(null);  
  }, [items]);  
// ...  
}

// DO THIS INSTEAD
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
 
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

** Why that works ** — state changes at the top, before any output, don’t trigger re-renders, same as initial assignment.

But, even better, would be not to need it at all:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

Later, docs do offer this pattern for e.g. app init:

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

// PROBABLY BETTER
// Check if we're running in the browser.  
if (typeof window !== 'undefined') { 
  // ✅ Only runs once per app load
  loadDataFromLocalStorage();
  checkAuthToken();  
}  

function App() {  
// ...  
}

Code at the top level runs once when your component is imported—even if it doesn’t end up being rendered. To avoid slowdown or surprising behavior when importing arbitrary components, don’t overuse this pattern. Keep app-wide initialization logic to root component modules like App.js or in your application’s entry point.

Later…

Effects in your application should eventually be replaced by custom Hooks, whether written by you or by the community. Custom Hooks hide the synchronization logic, so the calling component doesn’t know about the Effect. As you keep working on your app, you’ll develop a palette of Hooks to choose from, and eventually you won’t need to write Effects in your components very often.

useMemo

useMemo is react’s built-in memoizer that you can use for expensive calculations. Looks like this:

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // immediately run during render
  const visibleTodos = useMemo(() => {
      return getFilteredTodos(todos, filter);
    },
    [todos, filter] // getFilteredTodos only gets called if either
                    // of these change
  );
  // ...
}

useSyncExternalStore

You may be tempted to use an external store like this:

// note the magical `use` function, making it a hook.
function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  // run once after component initial mount
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

Although it’s common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead.

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ Good: Subscribing to an external store with a built-in Hook
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

It is sanctioned to use useEffect for updating a query, because the query may change without user action.

If you don’t use a framework (and don’t want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting your fetching logic into a custom Hook like in this example:

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

Props, state, and variables declared inside your component’s body are called reactive values.

Effect Events - Not Yet Stable

It’s a function that gets access to local vars but that won’t be considered reactive. For example:


// this won't work
  function onConnected() {
    showNotification('Connected!', theme);
  }
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // linter complains that onConnected is ommitted

// this will work
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

they can only be called from within an effect, never passed to hooks or components.

TODO: review https://react.dev/learn/removing-effect-dependencies - first thing you had trouble on, partly because the written part was a mix of twice repeated info and new info

Custom Hooks

Magically named function starting with use. Only hooks and components can call hooks.

  1. React component names must start with a capital letter, like StatusBar and SaveButton. React components also need to return something that React knows how to display, like a piece of JSX.
  2. Hook names must start with use followed by a capital letter, like useState (built-in) or useOnlineStatus (custom, like earlier on the page). Hooks may return arbitrary values.