Graduate Program KB

React.gg

  1. The Big Picture
  2. Describing UI
  3. Bringing React to Life
    1. Complete List of React Events
  4. Escaping React
  5. Optimizing React

The Big Picture

  • Previous practices for building for the web:
    • jQuery: Update application state by imperatively traversing the DOM until reaching the relevant node. Resulted in spaghetti code
    • Backbone: State is kept inside of Models. Whenever a Model changes, all subscribed Views re-render to reflect the new state. User had to define implementation for rendering
    • Angular: Two-way data binding (changes in Models would update bound Views, changes in Views would update bound Models). Resulted in code that was hard to follow and hard to debug and performance issues, as Angular had to constantly scan for state changes.
  • How does React do it?
    • Views are a function of the application's state. (v=f(s))
    • Compose functional components together to create UI
    • React, as opposed to traditional frameworks, concerns itself with anything that has to do with rendering the view, including the state, UI and styling (JS, HTML, CSS).
    • Can be managed by a metaframework like Next.js, Remix, Astro
  • (..Section continues by describing functional principles. Notes on FP can be found here.)

Describing UI

  • React Components: Functions that return a description of the UI (A React Element) in a declarative format almost identical to HTML. Components must have capitalized names to be acknowledged by React as a React component, otherwise it will be interpreted as a DOM element
  • Example of component usage:
function Authors() {
    return (
        <aside>
            <h2>Authors</h2>
            <ul>
                <li>Tyler McGinnis</li>
                <li>Ben Adam</li>
                <li>Lynn Fisher</li>
            </ul>
        </aside>
    );
}

export default function About() {
    return (
        <> // Shorthand for <React.Fragment/>
            <h1>About Us</h1>
            <p>We write JavaScript and words about JavaScript.</p>
            <Authors />
        </>
    );
}

  • Like with functions, components should be created following the single responsibility principle
  • If you have a component that's being reused somewhere else, make it its own file. If not, create it inside of whatever file you need it
  • Notes on JSX syntax:
    • Functions can only ever return one top-level element from a component. To return multiple adjacent elements, wrap them in a <React.Fragment> block (or use shorthand )
    • Self-closing tags must be explicitly closed (e.g. <br/>, <img/>, etc.)
    • Used className where class would be used in HTML to avoid collisions with JS reserved keywords
    • Variable names cannot have a dash in them and should be converted to camelCase.
    • JavaScript expressions can be used by wrapping them in {curly braces}
    • nulls can be returned from components if you need the component to conditionally not render
  • To convert an array into a JSX list use .map:
<ul id="tweets">
  {tweets.map((tweet) => (
    <li key={tweet.id}>{tweet.text}</li> // adding a unique key property helps React know which items change throughout different renders of a component
  ))}
</ul>
  • Ternary operators can be used for clean conditional rendering:
export default function App() {
    const isLactoseTolerant = getIsLactoseTolerance();

    return isLactoseTolerant
        ? <LactoseTolerant/> // returns if isLactoseTolerant is true
        : <LactoseIntolerant/> // else this returns
}
  • Props: Data passed into components. Syntax is the same as setting attributes on HTML (<Hello name="Bob" authed={true}>)
    • Object literals can be passed using {{two sets of curly braces}} (one that shows we're not passing in a string and one because of JS object syntax)
    • If you pass a prop without a value, the value is set to true
    • Props can be destructured
  • Example of using passed in props:
function Hello(props) {
    return {
        <h1>Hello, {props.name}. (auth {props.authed ? "succeeded" : "failed"}) </h1>
    }
}
  • Data can also be passed in via content inside a component's tags, e.g.:
function Layout (props) {
  return (
    <div className="layout">
      <Sidebar />
      {props.children} // children of <Layout/> tag
      <Footer />
    </div>
  )
}

// ...later...
<Layout>
    <Pizza/> // pass Pizza component to Layout
</Layout>

  • Example of creating a React Element using just JavaScript:
import { jsx } from "react/jsx-runtime";

const element = jsx("h1", {
    className: "header",
    children: "Profile"
});

Bringing React to Life

  • To attach an event handler to an element, pass a function to a prop of the element, e.g. <button onClick={() => alert("Hello!")}>
  • Whenever an event handler is called by React it will pass an object containing information about the triggered event as the first argument to the event handler. Example format for a trigger of onCopy:
SyntheticBaseEvent {
  bubbles: true,
  cancelable: true,
  clipboardData: DataTransfer {},
  currentTarget: null,
  defaultPrevented: false,
  eventPhase: 3,
  target: li,
  timeStamp: 52607.59999999404,
  type: "copy",
}
  • Hooks: Declarations about special functionality a component needs from React. Unconditional as they can't be called inside conditions, loops or nested functions
const [state, setState] = React.useState({ // Hook used to preserve a value across component renders
  id = 0, // object syntax can be used to represent multiple state values of one logical object
  email = "initialStateValue@mail.com",
  name = "Initial State, M.D."
})

// as setState will set the state value to whatever you pass in, object state values require extra handling if you'd like to update just one value
const handleUpdateEmail = (email) => {
  const updatedUser = {
    ...state, // spread operator spreads all existing key/value pairs from 'state' into a new object
    email: "updatedStateValue@mail.com"
  }

  setState(updatedUser);
};
  • State can be lifted up (i.e. definition moved up component hierarchy) and then managed by parent components, e.g. a ToDoList component can manage the state of its ToDo child components. This involves creating an updater function in the parent component that is passed down as a prop to its children, which will be invoked by event handlers in the child components.
  • Canonical methods of updating array state, which intentionally avoid modifying the original state array:
const [highScores, setHighScores] = React.useState(/*...*/)

const handleNewHighScore = (session) => { // adding element
  const newHighScores = [...highScores, session] // spread all existing elements
  setHighScores(newHighScores);
}

const handleRemoveCheater = (id) => { // Removing elements
  const newHighScores = highScores.filter((session)) => { // filter out elements to remove
    session.id !== id;
  }
  setHighScores(newHighScores);
}

const handleUpdateHighScore = (updatedSession) => { // Updating elements
  const newHighScores = highScores.map((session) => { // map elements to update
    return session.id === updatedSession.id
      ? updatedSession
      : session
  })
  setHighScores(newHighScores);
}
  • Rendering: When React calls your component with the intent of updating the View. React will create a snapshot of your component which captures everything React needs to update the view at that particular moment (props, state, event handlers and description of the UI) which is used to render the updated View.
  • Initial rendering is done via React Hook const root = createRoot(document.getElementByID("root")) followed by root.render(<App/>)
  • React will only re-render when the state of a component changes if useState's updater function is invoked and React notices that state has changed from the last snapshot. Re-rendering only begins once every invocation of useState's updater has been called in an event handler
  • NOTE! Event handlers only have access to the props and state as they were in the most recent snapshot. If a handler uses useState's updater and then tries to access the state value it will still hold the old state value.
  • By default, whenever React re-renders a component that owns an updated state it will also re-render all of its child components.
    • export React.memo(MyComponent): React will only re-render MyComponent if its props change
  • <StrictMode>, when wrapped around a component, will enable special behaviours in development builds:
    • Components will re-render an extra time to find bugs caused by impure rendering (if your function is pure it should produce the same result every time. Rendering twice will make it more obvious if impurity is causing bugs).
    • Components will re-run Effects an extra time to find bugs caused by missing Effect cleanup (same as above)
    • Components will be checked for usage of deprecated APIs (findDOMNode, UNSAFE_ class lifecycle methods, this.refs, childContextTypes, contextTypes, getChildContext)

Complete List of React Events

  • Clipboard Events: onCopy, onCut, onPaste
  • Composition (Text Input) Events: onCompositionEnd, onCompositionStart, onCompositionUpdate
  • Keyboard Events: onKeyDown, onKeyPress, onKeyUp
  • Focus Events: onFocus, onBlur
  • Form Events: onChange, onInput, onInvalid, onReset, onSubmit
  • Generic Events: onError, onLoad
  • Mouse Events: onClick, onContextMenu, onDoubleClick, onDrag, onDragEnd, onDragEnter, onDragExit, onDragLeave, onDragOver, onDragStart, onDrop, onMouseDown, onMouseEnter, onMouseLeave, onMouseMove, onMouseOut, onMouseOver, onMouseUp
  • Pointer Events: onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onGotPointerCapture, onLostPointerCapture, onPointerEnter, onPointerLeave, onPointerOver, onPointerOut, onPointerHover
  • Selection Events: onSelect
  • Touch Events: onTouchCancel, onTouchEnd, onTouchMove, onTouchStart
  • UI Events: onScroll
  • Wheel Events: onWheel
  • Media Events: onAbort, onCanPlay, onCanPlayThrough, onDurationChange, onEmptied, onEncrypted, onEnded, onLoadedData, onLoadedMetadata, onLoadStart, onPause, onPlay, onPlaying, onProgress, onRateChange, onSeeked, onSeeking, onStalled, onSuspend, onTimeUpdate, onVolumeChange, onWaiting
  • Image Events: onLoad, onError
  • Animation Events: onAnimationStart, onAnimationEnd, onAnimationIteration
  • Transition Events: onTransitionEnd
  • Other Events: onToggle

Escaping React

  • In the context of React, a pure component only takes input (props and state) and calculates output (the View), with no side effects (includes API calls, DOM manipulation, browser APIs, etc)
  • Rules for managing side effects:
    1. When a component renders, it should do so without running into any side effects
    2. If a side effect is triggered by an event, put that side effect in an event handler (so it happens prior to rendering)
    3. If a side effect is synchronizing your component with some external system put that side effect inside a useEffect hook (so it happens after rendering)
    4. If a side effect is synchronizing your component with some outside system and that side effect needs to run before the browser paints the screen, put that side effect inside useLayoutEffect
    5. If a side effect is subscribing to an external store, use the useSyncExternalStore hook
  • If event listeners were added in a useEffect function through document.addEventListener("eventName", handlerFunc) ensure that it is removed in the cleanup function using document.removeEventListener("eventName", handlerFunc) to avoid a memory leak.
  • useEffect syntax:
React.useEffect(func, [variable1, variable2,...]) // func will be invoked so it can resynchronize with an external system whenever its dependencies (variable1, variable2...) change
  // If useEffect returns a function, it will be run before every subsequent useEffect call and when the component is removed from the DOM
  • Handling multiple conflicting API calls with Promises and cleanup functions:
    1. Put API call in an async function and await it in component code
    2. In your cleanup function set an ignore boolean value to true
    3. After your API call, before continuing with rendering behaviour put a check to see if ignore has been set for this run-through of useEffect. This ensures that if the cleanup function has been called (meaning a new, potentially conflicting API call is being made) nothing more will be done with the old call.
    • To avoid needing to add and remove listeners every time a state dependency is changed, do this:
export default function TabAways () {
  const [count, setCount] = React.useState(0)

  React.useEffect(() => {
    const handleChange = () => {
      if (document.visibilityState !== "visible") {
        setCount((c) => c + 1) // passing a function to setState will make setState invoke the function with the current state as an argument, avoiding the need to bring 'count' into useEffect's dependency array
      }
    }

    console.log("addEventListener")
    document.addEventListener("visibilitychange", handleChange)

    return () => {
      console.log("removeEventListener")
      document.removeEventListener("visibilitychange", handleChange)
    }
  }, []) // Don't pass in any dependencies, so useEffect will only be invoked here once after the initial render

  return (
    <p>
      You've tabbed away <strong>{count}</strong> time{count !== 1 && "s"}.
    </p>
  )
}
  • Reactive values are values that can change between re-renders (includes props, state, variables defined inside a component). Anything that isn't guaranteed to remain in the same spot in memory
  • const ref = React.useRef("value") creates a value preserves across renders that doesn't trigger a re-render when it changes that can be accessed and mutated via ref.current
  • Context: React API that allows 'teleporting' of data anywhere in your component tree without needing to pass props. Does not have anything to do with state management. Best used in large applications with deeply nested component architectures where passing props becomes unreasonable. Example:
const delorean = React.createContext(); // 1. Create the context

function App() {
  const marty = {
    name: "Marty McFly",
    age: 17,
    occupation: "Student"
  }

  return ( // 2. Decide what you want to teleport and to where
    <delorean.Provider value={marty}>
      <TwoThousandFifteen />
      <NineteenEightyFive />
      <NineteenFiftyFive />
      <EighteenEightyFive />
    </delorean.Provider>
  )
}

function TwoThousandFifteen () {
  return (
    <div className="era">
      <h2>2015</h2>
      <Cafe80s />
      <CourthouseMall />
      <Holomax />
      <Texaco />
    </div>
  )
}

function Cafe80s () {
  // 3. Get what you teleported from inside a component
  const marty = React.useContext(delorean)
  /* Returns what was passed to the value prop of the nearest Provider component of the same Context OR from the value of the first argument that was passed to createContext when it was created (if it is in scope or imported) */

  return (
    <div className="location">
      <h3>Cafe80s</h3>
      <p>Step back in time and join us at Cafe 80's for a blast from the past! Enjoy a delicious meal while surrounded by iconic 80's memorabilia, relive the era of big hair, bright colors, and all your favorite 80's hits. Don't miss out on the ultimate retro dining experience!</p>
      <h4>Current Guests</h4>
      <ul>
        <li>{marty.name}: Age {marty.age}. {marty.occupation}.</li>
      </ul>
    </div>
  )
}

Optimizing React

  • useReducer allows you to add manageable state to a component that will be preserved across renders and trigger re-renders when it changes. Usage:
const [state, dispatch] = React.useReducer(
  reducer, // function of signature (state, value) -> nextState
  initialState
)

dispatch(value) // call reducer function with value argument
  • If different pieces of state update independently from each other use useState. If your state is updated together or if updating one piece of state is based on another piece of state, use useReducer.
  • useMemo allows you to cache the result of calculations between renders. Usage:
const cachedFunction = React.useMemo(
  originalFunction, // function that returns value to cache
  dependencies // array of dependencies function depends on (cached value will be recalculated if any dependencies change)
)
  • useCallback is like useMemo but caches the function itself as well. Think of it like this:
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
  • useLayoutEffect is almost identical to useEffect but runs before the browser repaints the screen, which makes it an ideal hook to use for side effects that synchronize layout information with your components
  • useSyncExternalStore is a React hook that can subscribe components to outside non-React state without needing to duplicate it as React state inside your component. Usage:
const getSnapshot = () => {
  /*... code to read external state ...*/
}

const subscribe = () => {
  /* ... code to subscribe to external store so React knows when it changes, e.g. event listeners */
}

const myExternalData = React.useSyncExternalStore(
  subscribe,
  getSnapshot
)
  • useEffectEvent allows us to use reactive but non-synchronizing values in useEffect without including them in the dependency array by extracting them out to another function, preventing unneeded re-renders. Usage:
const onMyEvent = React.useEffectEvent((syncValue) => {
  someFunc(syncValue, nonSyncValue)
})

React.useEffect(() => {
  onMyEvent(syncValue)
}, [syncValue])
  • Custom Hooks can be created to bring custom, re-usable non-visual logic to components. To create a custom hook, write and export a function whose name starts with use and at some point and use a React hook somewhere inside the function body.