Introduction #

This handbook and its accompanying TypeScript code is a reference guide on how to use React Hooks with TypeScript (and write tests for them using react-testing-library).

  1. This is not a React primer, and is primarily aimed at developers who know React class components and want to learn to use Hooks to create function components using TypeScript.
  2. You can jump directly to any topic in the table of contents that you are curious about in this handbook, you don’t have to read it from start to finish.

⚡ The source code for this handbook can be found in this github repo.

💡 The source code project accompanying this handbook created using create-react-app or CRA.

React #

To learn more about how React actually works and generates Virtual DOM objects which are then rendered to the actual DOM, please refer to this great video on how to create a custom React renderer. Here’s the github repo for the toy React DOM Mini.

React Hooks #

React hooks exist because there were severe limitations in versions before React 16 on what function components could do. And there were some problems w/ how class components function. By adding hooks to function components, it basically eliminates these 2 groups of problems.

Why? #

Here are the details on the problems w/ class components mentioned above. It is difficult to reuse stateful logic between components, which results in people doing things like:

  • render props: Passing functions down the component hierarchy as via props is cumbersome. However, there are situations where this can’t be avoided.
  • higher-order components: State can be stored at the top most component in the hierarchy in order to be shared between children components, which makes it necessary to add “artificial” components at the top most level (to have a shared scope in the children) just to wrap everything else underneath it, which is not optimal.

With hooks, it is now possible to reuse stateful logic between function components! Thru the use of useState hook and others, you can create your own more complex hooks. The official docs have Hooks at a Glance which is a great overview of hooks along w/ an example of how you can make your own.

Limitations when using hooks (must follow these rules) #

Read this deep dive on how hooks are implemented and follow these rules. Hooks in functional components get turned into objects that are stored in a linked list inside of a fiber (which is an object representation of the DOM that each React component has).

💡 More info on React fibers.

  1. By following these rules:
  2. You ensure that Hooks are called in the same order each time a component renders.
  3. You ensure that all stateful logic in a component is clearly visible from its source code.
  4. Hooks should not be called in loops, nested functions or conditions. Instead, always use Hooks at the top level of your React function, before any early returns.
  5. Don’t call Hooks from regular JavaScript functions.
  6. React function components can call Hooks.
  7. Custom Hooks can call other Hooks.
  8. If we want to run an effect conditionally, we can put that condition inside our Hook.
    useEffect(() => (name !== "" ? null : localStorage.setItem("name_field", name)))
    

Example w/ explanation of React memory model #

Here’s an example from the React official docs.

import React, { useState, useEffect } from "react"

/** Custom hook. */
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)

  function handleStatusChange(status) {
    setIsOnline(status.isOnline)
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
    }
  })

  return isOnline // This comes from `useState()`.
}

// FC.
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)

  if (isOnline === null) {
    return "Loading..."
  }
  return isOnline ? "Online" : "Offline"
}

// FC.
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)

  return <li style={getColor(isOnline)}>{props.friend.name}</li>

  function getColor(isOnline) {
    return "color: " + isOnline ? "green" : "black"
  }
}

Here are some notes on the code to understand what is happening here and how to think about state in a function component, and how that maps to React’s memory model.

  1. With classes, it was easier to understand React’s memory model, because a stateful class component has a constructor, which then allocates the state up front. ReactDOM.render(virtualDom, actualDom) actually takes the JSX.Element that this class component represents, and mounts and renders it. Then there are subsequent re-renders which is where state actually comes into play. In hooks, this part is a little bit confusing, since there is no constructor. Here’s one way to think about it.

  2. The first time the function component is rendered (after being mounted), React will allocate any state variables that are declared via calls to useState(). This is stored in memory by React and the initial value is the argument passed to useState(initialValue).
  3. This is also why React cares that these calls should not be wrapped in conditional logic or loops, because it relies on the lexical order in which these useState() calls are declared to do internal bookkeeping to figure out what the values of each stateful variable is (in its internal memory model).
  4. When the setter returned by useState<T>(initialValue:T) which is of type Dispatch<SetStateAction<T>>, is called w/ a new value, it will trigger a re-render. A function can also be passed to useState<T>(initialValue:(T)=>T) which will compute the subsequent value based on the previous state’s value. When a re-render is triggered then the new value of this state will be used to generate the UI.

  5. Note that when stateful logic is reused, the state of each component is completely independent. Hooks are a way to reuse stateful logic, not state itself. In fact, each call to a Hook has a completely isolated state — so you can even use the same custom Hook twice in one component.

  6. Custom Hooks are more of a convention than a feature. If a function’s name starts with ”use” and it calls other Hooks, we say it is a custom Hook. The “useSomething” naming convention is how the linter plugin is able to find bugs in the code using Hooks.

useEffect #

This hook is kind of componentDidMount, componentDidUpdate, and componentWillUnmount combined!

It allows you to register a function that is called whenever anything is rendered to the DOM on the page. And it can be scoped to an array of dependencies (which are state variables that can change in order to trigger this effect to be run).

  1. Read the official docs to get the details.
  2. This SO thread also has some good insight on how to use this hook.

Here is an example that mimics componentDidMount.

export const ReactReplayFunctionComponent: FC<AnimationFramesProps> = (
  props
): ReactElement => {
  /** State: animator (immutable). */
  const [animator] = useState<Animator>(
    new Animator(MyConstants.delayMs, tick, "[FunctionalComponentAnimator]")
  )

  useEffect(runAnimatorAtStart, [animator])

  /** Starts the animator. */
  function runAnimatorAtStart() {
    animator.start()

    // Cleanup.
    return () => {
      if (animator.isStarted) animator.stop()
    }
  }

  /* snip */
}

This useEffect hook is scoped to the animator variable, which is returned by useState.

⚠ This is similar to componentDidMount, unmount, update, etc. However, there are some big differences.

  • To mimic the functionality of componentDidMount you have to pass a 2nd argument to useEffect which is either an empty array [] or some state variable that doesn’t really change.
  • To watch for changes in some specific variables, you can pass them in that array.

💡 Read more about the difference between passing an empty deps array and passing nothing on this SO thread.

If you want a hook to run on every single DOM render, then you can write something like this.

import { ReactElement } from "react"

export const ReactReplayFunctionComponent: FC<AnimationFramesProps> = (
  props
): ReactElement => {
  useEffect(runForEveryDomChange)

  function runForEveryDomChange() {
    /* code */
  }

  /* snip */
}

⚠ Beware the issue of “stale closures” when using useEffect(). The stale closure problem occurs when a closure captures outdated variables.

First render and subsequent re-renders using useRef and useEffect #

Out of the box the callback that you pass to useEffect() can’t tell the difference between first render, and subsequent re-renders. This is where useRef can be very useful (we have seen it used for keyboard focus in this section.

You can set whatever value you want in the current property of the ref (which is returned by React.useRef(initialValue)). This value will be stable when the component is re-rendered. This can be a simple way of detecting when the first render occurs, and when subsequent re-renders occur.

💡 Note that storing a value in an object returned by useRef is different than simply using setState because React isn’t watching for changes in the state in order to trigger a re-render.

Here’s an example of a custom hook that allows you to use localStorage in order to get and set key-value pairs.

export type MyLocalStorageHook = [string, Dispatch<SetStateAction<string>>]
const useMyLocalStorageHook = (key: string): MyLocalStorageHook => {
  const isMounted = React.useRef(false)

  const [value, setValue] = React.useState(localStorage.getItem(key) || "N/A")

  React.useEffect(() => {
    if (!isMounted.current) {
      console.log("First render")
      isMounted.current = true
      return
    }

    console.log("Subsequent re-render")
    localStorage.setItem(key, value)
  }, [key, value])

  return [value, setValue]
}

⚠ Note that the value of the object returned by useRef (the current property of ref) is only set in the useEffect callback. It isn’t set in the code that is doing the rendering for example.

  1. This hook can tell the difference between the first render and subsequent re-renders since it is using useRef(boolean) in order to set the current property to true on first render (and then doing an early return).
  2. Subsequent re-renders will skip over this early return condition check and will actually the work it is intended to.
  3. When the setValue dispatch is used to assign a new value to the key, then it will actually save the key-value pair to local storage.

💡 Note that ref.current can also be used as a place to store the results of an expensive computation that are local to a function component, but isn’t part of the state. This can be a cache that is local to the component. Perhaps this cache can be populated on first render and then re-used for subsequent renders.

useState #

Reusing stateful logic isn’t the same as sharing state between function components. For the latter, useContext or Redux might be more appropriate.

Example #

The following code uses the useState hook in a function component.

import { ReactElement } from "react"

type IndexStateHookType = [number, Dispatch<SetStateAction<number>>]

export const ReactReplayFunctionComponent: FC<AnimationFramesProps> = (
  props
): ReactElement => {
  /** State: currentAnimationFrameIndex (mutable). */
  const [frameIndex, setFrameIndex]: IndexStateHookType = useState<number>(0)

  /** State: animator (immutable). */
  const [animator] = useState<Animator>(
    new Animator(MyConstants.delayMs, tick, "[FunctionalComponentAnimator]")
  )

  /* snip */
}

⚡ Here’s the complete source file ReactReplayFunctionComponent.tsx.

Here’s the anatomy of each call to useState.

  1. On the 👈 left hand side of the call to useState hook, it returns an array (of type IndexStateHookType).
  2. The first item is a reference to the state variable (of type T).
  3. The second item is the setter function for it (of type Dispatch<SetStateAction<T>>). This mutates the state and triggers are render of the function component.
  4. It has to be initialized w/ whatever value is on the right hand side 👉 expression. So it is a pretty simple way of making function components have initial state.

⚠ Beware the issue of “stale closures” when using useState(). The stale closure problem occurs when a closure captures outdated variables.

Shared stateful logic vs shared state #

Here’s a SO question about this that goes into quite a bit of detail about the differences between sharing state between components and stateful logic that is shared. Here are some tips from this SO answer.

  • Stateful logic is different from state. Stateful logic is stuff that you do that modifies state. For e.g., a component subscribing to a store in componentDidMount() and unsubscribing in componentWillUnmount(). This subscribing/unsubscribing behavior can be implemented in a hook and components which need this behavior can just use the hook.

  • Here are some ways to share state between components (each w/ its pros and cons):

    1. Lift state up to a common ancestor component of the two components. This is the same approach taken with class components, the only difference w/ hook is how we declare the state.

      const Parent: FC = () => {
        const [count, setCount] = useState(1)
        return (
          <>
            <ChildA count={count} onCountChange={setCount} />
            <ChildB count={count} onCountChange={setCount} />
          </>
        )
      }
      
    2. If the child components are deep in the hierarchy to be passed down at every level, then useContext hook might be the way to go.

    3. External state management libraries like Redux might be the way to go. Your state will then live in a store outside of React and components can subscribe to the store to receive updates.

    ⚠ Note - Avoid the open source custom hook called useBetween since it is a useless hack.

Resources #

Here are some great resources on learning about useState.

  1. Official docs
  2. TypeScript and useState

useReducer #

This hook is very similar to the useState hook. You can use both in a component as well. The main difference between them is unlike setState, a reducer function must be provided that deals w/ generating a new state when employing useReducer.

💡 This is very similar to Redux! And this might just be the simplest way to learn Redux patterns. Jump to this section to learn all about how to use React and Redux (using the modern redux-toolkit and not old-school redux-react).

It is possible to replace code that employs useState w/ code that employs useReducer. Here’s an example.

Code that employs useState hook.

type StoriesStateHookType = [Story[], Dispatch<SetStateAction<Story[]>>]
export const ListOfStoriesComponent: FC<ListOfStoriesProps> = (props) => {
  // Store Story[] in the function component's state.
  const [myStories, setMyStories]: StoriesStateHookType = React.useState<Story[]>([])

  // Simply call `setMyStories` to set a value to the function component's state.
  React.useEffect(
    () => {
      getAsyncStoriesWithSimulatedNetworkLag().then((value) => {
        setMyStories(value)
      })
    },
    [] /* Only run this effect once; akin to componentDidMount. */
  )
}

// Function to remove a story by simply using `setMyStories`.
const handleRemoveItem = (id: number): void => {
  setMyStories(myStories.filter((it) => it.objectID !== id))
  console.log(`handleRemoveItem('${id}) called'`)
}

Equivalent code that employs useReducer hook.

export const ListOfStoriesComponent: FC<ListOfStoriesProps> = (props) => {
  // Create a reducer to manage Story[] in the state.
  const [myStories, dispatchMyStories]: ReducerHookType = React.useReducer<ReducerType>(
    storiesReducer,
    new Array<Story>()
  )

  // To make changes to the state managed by the reducer, you have to dispatch an action w/
  // a payload. In this example, we have 2 actions: "setState" and "removeItem".

  React.useEffect(
    () => {
      getAsyncStoriesWithSimulatedNetworkLag().then((value) => {
        dispatchMyStories({
          type: "setState",
          payload: value,
        })
      })
    },
    [] /* Only run this effect once; akin to componentDidMount. */
  )

  const handleRemoveItem: FnWithSingleArg<Story> = (objectToRemove: Story) => {
    console.log("handleRemoveItem called w/", objectToRemove)
    dispatchMyStories({
      type: "removeItem",
      payload: objectToRemove,
    })
  }
}

// This is the reducer function.

export type StateType = Story[]
interface ActionSetState {
  type: "setState"
  payload: Story[]
}
interface ActionRemoveItem {
  type: "removeItem"
  payload: Story
}
export type ActionType = ActionSetState | ActionRemoveItem
export type ReducerType = Reducer<StateType, ActionType>
export type ReducerHookType = [StateType, Dispatch<ActionType>]

export const storiesReducer = (
  currentState: StateType,
  action: ActionType
): StateType => {
  console.log("storiesReducer -> \ncurrentState:", currentState, "\n-> action:", action)
  let newState: StateType
  switch (action.type) {
    case "removeItem":
      const itemToRemove = action?.payload
      newState = currentState.filter((story) => story.objectID !== itemToRemove.objectID)
      break
    case "setState":
      newState = action.payload
      break
    default:
      throw new Error(`Invalid action: ${action}`)
  }
  console.log("return newState", newState)
  return _.clone(newState)
}

Two examples (one w/out network, and one w/ network) #

  1. For a simple example that doesn’t use any network, and that is decomposed into many files, check out the ListOfStoriesComponent which utilizes both useState and useReducer hooks.
  2. For an example that has network calls, and does everything in one file, check out the CatApiComponent which only uses useReducer and no useState.

useCallback and useMemo #

These two hooks are very tricky, since they cache objects that are tightly bound to React’s lifecycle, and might have memory and CPU impacts that you’re not able to predict using your intuition. Most of the time, its best not to use them.

These articles go into great details about why they should not be used unless you have a very specific use case that warrants their use as.

  1. Article - useMemo and useCallback
  2. Article - Don’t overuse React useCallback

Reference docs

Here are some acceptable use cases where these can be used.

  • When functions that are declared inside of a hook have some internal state, such as when the function is debounced or throttled.
  • When the function object is a dependency on other hooks (eg: passing the function returned by useCallback to useEffect).
  • When the function component wrapped inside of useMemo accepts a function object prop.

How to write complex function components #

If the FCs (ItemComponent, ListComponent, SearchComponent ) are defined inside the ListOfStoriesComponent they behave differently with keyboard focus, than if they are defined outside it!

  1. If they are declared outside, the keyboard focus inside the input element is retained as you type different search terms.
  2. If they are declared inside, the keyboard focus is lost every time you generate a single keyboard event.

    ⚠ This might have something to do with this, function components being stateless. If things are declared inside of a function, then they will be recreated everytime that function is run, vs if they are declared outside, then they won’t be recreated on every (render) call.

Custom hooks #

When writing custom hooks, that relay external async events (eg, happening via a listener function that is attached to the DOM, or Node.js, or a database) please note that this listener function, which is passed to useEffect(), can not directly call into the React function component 🧨! If you have functions defined inside of your React function component, even though they look and feel like regular TypeScript / JavaScript functions they don’t work the same way if code outside the component invokes them vs. code that is inside.

🤔 This is a very subtle but important point to remember when figuring how to “relay” the non-React async event into the React function component world (via a custom hook that you’re writing). When using “normal” React (host components in a browser) we don’t get to see this because the code that interfaces with the browser’s DOM is provided to us. So we are used to attaching a callback function to an onclick props of a button in React, and it would just work 🧙. However, under the covers this host component is taking care of interfacing w/ a DOM async event emitter to invoke callback function via addEventListener and doing something similar to this hook.

It might be tempting to simply pass a React-function-component-callback function to a hook, which then passes it on to an external event emitter listener 😈. When the listener is then executed by the external event emitter (which is not a React function component) then problems will arise! The state information that this React-function-component-callback will get will be wrong.

The way to get around this issue is to do the following:

  1. Introduce an intermediate state variable (via useState() hook).
  2. Have your external event listener use the setter for this state variable to let your hook know that some external event just came in.
  3. Use useEffect() and pass the getter for this state variable as a dependency to a function that gets called when this state variable changes. This function will be able to then run the React-function-component-callback that is passed into the hook to begin with.

Here’s a diagram of this.

For a real world example, please take a look at this commit in r3bl-ts-utils, this hook, and this other hook for more information on how the hook “binds” the two disparate worlds of Node.js process.stdin’s keypress events, and the React function component that ends up using this hook to get async events that are generated by an end user who’s typing in a terminal.

🤔 When using useEffect() and your own custom hooks, it is important to be mindful of the sequence of steps that React takes in order to clean up your hooks, when a reference in the dependency array changes. Here’s an example of this actually plays out for a scenario where you have useEffect() with [ foo ] as a dependency (from the diagram above). After the first time the effect runs, all subsequent changes in foo trigger the cleanup function to be executed prior to the effect callback function.

foo function calls
“a” callbackFn
undefined cleanupFn, then callbackFn
“b” cleanupFn, then callbackFn

Here are some examples:

  • useEventEmitter
    • Real example of the diagram shown above.
  • useNodeKeypress
    • Real example of the diagram shown above.
  • useLocalStorage
    • useLocalStorage shows how to use localStorage to back a state variable.
    • LocalStorageEvents shows how to deal w/ localStorage limitations when setting the value in the same document. localStorage changes can be initiated in 3 ways:
      1. from this hook (calls to the setter / dispatcher returned by this hook).
      2. from calls to MyLocalStorageEvents.storage.setValue().
      3. from other document(s) (running the same app in other tabs in the same browser instance) changing the key value pairs in localStorage.
  • useAnimator

References about hooks #

To learn about hooks, here are some important resources.

[h-5]: https://www.infoworld.com/article/3603276/how-to-use-react-functional-components.html

Keyboard focus and React #

We can do this declaratively (which is relatively simple, but extremely limiting) or imperatively, which is probably more realistic (and more complex).

Declarative #

Here’s a way to get keyboard focus into a single element declaratively. The autoFocus attribute on the input element is set to a boolean value that is passed as a props.

type SearchProps = {
  searchTerm: string
  onSearchFn: OnSearchFn
  initialKeybFocus: boolean
}

const ListOfStoriesComponent: FC = () => {
  /** snip */
  return (
    <div className={"Container"}>
      <strong>My Searchable Stories</strong>
      <SearchComponent
        searchTerm={searchTerm}
        onSearchFn={onSearchFn}
        initialKeybFocus={true}
      >
        <strong>Search: </strong>
      </SearchComponent>
      <ListComponent list={filteredStories} />
    </div>
  )
}

const SearchComponent: FC<SearchProps> = ({
  searchTerm,
  onSearchFn,
  children,
  initialKeybFocus,
}) => {
  return (
    <section>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={searchTerm}
        onChange={onSearchFn}
        autoFocus={initialKeybFocus}
      />
    </section>
  )
}

Imperative using useRef #

Here’s another way to do the same thing imperatively (programmatically) using useRef() and useEffect().

💡 Using ref (DOM element of a React element) and hooks:

  1. In order to use a ref, you must first create it using the useRef() hook, and then pass it to use the ref attribute in a React component.
  2. When this component is rendered to DOM, it will “set” the current property of this ref (which is the DOM element corresponding to this React component). This is used later in the useEffect() hook to actually call focus() on the DOM element.
const SearchComponent: FC<SearchProps> = ({
  takeInitialKeyboardFocus,
  searchTerm,
  onSearchFn,
  children,
}) => {
  // useEffect hook for initial keyboard focus on input element.
  const inputRef: React.MutableRefObject<any> = React.useRef()
  React.useEffect(() => {
    if (inputRef.current && takeInitialKeyboardFocus) {
      inputRef.current.focus()
    }
  })

  // Run this effect to log inputRef.
  const [showUi, setShowUi] = React.useState(true)
  function onButtonClicked() {
    setShowUi((prevState) => !prevState)
    console.log("showUi", showUi)
  }
  React.useEffect(() => {
    console.log("✨ Creating inputRef related effect")
    const logInputRef = (msg: string) => {
      console.log(
        `${msg}\n`,
        inputRef.current ? "🎉 has DOM element" : "🧨 does not have DOM element"
      )
    }
    logInputRef("🎹✨ inputRef")
    return () => {
      logInputRef("🎹🗑 Do something here to unregister the DOM element, inputRef")
    }
  }, [showUi])

  return (
    <section>
      {showUi && (
        <>
          <label htmlFor="search">{children}</label>
          <input
            id="search"
            type="text"
            value={searchTerm}
            onChange={onSearchFn}
            ref={inputRef}
          />
        </>
      )}
      <button onClick={onButtonClicked}>mount/unmount</button>
    </section>
  )
}

💡 You can also utilize useRef() in order to detect the first render vs subsequent re-render by pairing it w/ useEffect() hook; more in this section.

React and CSS #

At a high level there are 2 strategies to consider when using CSS in React (which are both bundled when you use create-react-app (CRA).

  1. Styled components (CSS in JS) - This is where you simply use the className prop to specify which CSS rules to use.
  2. CSS Modules (CSS in CSS) - This is where you leverage CSS modules which locally scope the styles to prevent collisions w/ synonymous styles declared in other files. Look below for more details.

CSS Modules example #

The following are the main differences that you have to be aware of when compared to styled components:

  1. Instead of using XXX.css, make XXX.module.css files. This tells CRA to use CSS modules.
  2. Instead of importing the CSS file like you would for a styled component, do this instead import YYY from './XXX.module.css', where YYY is what you choose to call the variable holding all the imported styles.

    When using styled components you only have to import the CSS file once (eg: in App.tsx) and these are available (implicitly) to all the React components that are nested in it. But using modules you have to import them in each file that uses the style.

  3. Then use these styles in your JSX like this: <Component className{YYY.XYZ}/>.

Here’s a simple example that shows how to use CSS modules or CSS in CSS approach.

⚡ Read the CRA official docs on using CSS modules.

Step 1 - Create a XYZ.module.css file, eg: App.module.css and not a App.css file.

.container {
  height: 100vw;
  padding: 20px;
  background: linear-gradient(to left, #b6fbff, #83a4d4);
  color: #171212;
}

Step 2 - In your JSX component, you have to do the following import to use the style.

import React from "react"
import myStyles from "./App.module.css"

const App: FC = () => <div className={myStyles.container}>Content</div>

⚠ Note how this is different from the usual <div className="container">...</div>.

Styled component example #

TooltipOverlay w/ just CSS #

It is actually very simple to implement a tooltip using just CSS. The key is using the :hover pseudo selector on the tooltip’s parent container to display a tooltip.

Here’s an overview of what this CSS does:

  • An empty parent container (span) is created so that its position can be set to relative, so that any child elements inside of it (other span elements) can be positioned as absolute w/ a z-index of 1000.
  • The :hover pseudo selector on the .tooltipContainer takes care of showing the .tooltip by setting its display to block.

TooltipOverlay.module.css

.tooltipContainer {
  position: relative;
}

.tooltip {
  --radius: 5px;
  --alpha: 0.8;
  --fillColor: rgba(26, 31, 48, var(--alpha));
  --borderColor: rgba(100, 149, 237, var(--alpha));
}

.tooltip {
  display: none;
  position: absolute;
  z-index: 1000;

  padding: calc(var(--defaultPadding) * 0.5);
  background: var(--fillColor);
  border-radius: var(--radius);
  border: 2px solid var(--borderColor);

  /* This ensures that the tooltip does not obstruct the element that is being hovered */
  top: 100%;
  width: auto;
}

.tooltipContainer:hover .tooltip {
  display: block;
}

The (minimal) React component below uses the CSS above.

TooltipOverlay.tsx

import React, { FC, PropsWithChildren } from "react"
import componentStyles from "./TooltipOverlay.module.css"

export type TooltipOverlayProps = {
  tooltipText: string
}

export const TooltipOverlay: FC<PropsWithChildren<TooltipOverlayProps>> = ({
  children,
  tooltipText,
}) => {
  return (
    <span className={componentStyles.tooltipContainer}>
      <span className={componentStyles.tooltip}>{tooltipText}</span>
      {children}
    </span>
  )
}

⚡ React bootstrap package has an example using this pattern here.

TooltipOverlay w/ just React #

Here is an example of implementing a tooltip that uses mostly React, and not much CSS as shown in the previous example.

Here’s the React code - The useState hook is essentially used to apply a “hover” effect on any React elements that are children of a TooltipOverlay component.

import componentStyles from "./TooltipOverlay.module.css"

export type TooltipOverlayProps = {
  tooltipText: string
}

export const TooltipOverlay: FC<PropsWithChildren<TooltipOverlayProps>> = ({
  children,
  tooltipText,
}) => {
  const [showTooltip, setShowTooltip] = React.useState(false)
  const onMouseEnter = () => setShowTooltip(true)
  const onMouseLeave = () => setShowTooltip(false)

  const styleVisible = componentStyles.tooltip + " " + componentStyles.visible
  const styleInvisible = componentStyles.tooltip

  return (
    <span
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      className={componentStyles.tooltipContainer}
    >
      <span className={showTooltip ? styleVisible : styleInvisible}>{tooltipText}</span>
      {children}
    </span>
  )
}

The CSS is the same as in the previous example, with the only following difference.

.tooltip.visible {
  display: block;
}

React and SVG #

It is very easy to use SVG w/ React. There are various ways of importing the SVG asset into a React component itself:

  1. You can import it from a file (static asset). The imported file is actually a React component itself. And you can style the SVG directly by passing className or inline styles to this component. Here’s an example of importing the SVG file.

    import { ReactComponent as Car } from "./car.svg"
    export const SvgExample: FC = (props) => <Car className={"SvgImage"} />
    

    Here’s an example of styling it in CSS.

    .SvgImage {
      height: 100px;
      width: 100px;
    }
    
    .SvgImage:hover > g {
      stroke: yellow;
      stroke-width: 8px;
    }
    
  2. You can declare it in JSX. Here’s an example.

    const BasicSvg = () => (
      <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
        <circle cx="50" cy="50" r="40" stroke="blue" strokeWidth="4" fill="lightblue" />
      </svg>
    )
    

    You can then just use it as a React component.

    export const SvgExample: FC = (props) => <BasicSvg />
    

    Or you can use it inside an img tag like so.

    export const SvgExample: FC = (props) => <img src={logo} className="logo" alt="logo" />
    
  3. You can load it as a string using the data:image/svg+xml pattern. Here’s an example of defining the SVG in CSS.

    .topography-pattern {
      background-color: #ffffff;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' ... ");
    }
    

    And then using it in a React component.

    export const SvgExample: FC = (props) => <div className={"topography-pattern"}></div>
    

References on SVG and React #

Redux #

Redux is a great way to share state between components. The redux-toolkit uses the underlying react-redux module, but is easier to use. Here’s how you can install it in your project.

npm install @reduxjs/toolkit react-redux

💡 To understand Redux it is best to start w/ ‘useReducer’ hook as show in this section. It is an easy way to understand the fundamentals of Redux which is actions, state, reducer functions, and immutable state.

While Redux helps you deal with shared state management, it has tradeoffs. You have to buy into its mental model, and structure your code and thoughts in a Redux-compliant way. As a developer you are used to modifying state imperatively, and the notion of having to request changes to be made declaratively might be something you’ve not seen before. It also adds some indirection to your code, and asks you to follow certain restrictions. It’s a trade-off between short term and long term productivity.

Here’s a simple example (no async, middleware, thunks, or splitting reducers, etc). There are the 5 steps to using Redux in your React component(s).

Step 1 - Define the types for the state, action, then use them to create a reducer function #

We will use a TypeScript feature called discriminated unions in order to make our actions typesafe.

  1. The type of the reducer function is Reducer<State | undefined, Action>. The undefined is to cover the case when the initial state must be created by the reducer.
  2. If you try and replace the Reducer<State ... , Action> w/ a type variable that holds the same thing, then it will not work for some reason w/ TypeScript.

💡 The reducer function is called that because it is meant to be analogous to Array.reduce() which takes all the items in an array and returns a single “thing”.

Reducer functions take the state and current action and produce a single “thing”, which is the new state.

Now, imagine putting all the actions that would be dispatched over the lifetime of an app and then put them into an array and then “apply” them to the reducer function (taking the resulting state from dispatching an action and passing it to the next action and so on). Ultimately you end up w/ a single result which is the final state of the app. Thinking of it this way, it sort of fits how the Array.reduce() function works 🤷.

// Types.
type Text = {
  id: string
  content: string
}
type State = {
  textArray: Array<Text>
}
interface ActionAdd {
  type: "add"
  content: string
}
interface ActionRemove {
  type: "remove"
  id: string
}
type Action = ActionAdd | ActionRemove

// Reducer function.
const reducerFn: Reducer<State | undefined, Action> = (state, action): State => {
  if (!state)
    return {
      textArray: [
        { id: _.uniqueId("id"), content: "fffff" },
        { id: _.uniqueId("id"), content: "gggg" },
      ],
    }

  switch (action.type) {
    case "add": {
      const newText: Text = { id: _.uniqueId("id"), content: action.content }
      console.log("add", newText)
      const copyOfArray = new Array<Text>().concat(state.textArray)
      copyOfArray.push(newText)
      return Object.assign({} as State, state, { textArray: copyOfArray })
    }
    case "remove": {
      // Get the index (the string we want to remove).
      const id: string = action.id
      // Make a copy of the old state and remove the element w/ the given id
      const copyOfArray = new Array<Text>()
        .concat(state.textArray)
        .filter((it) => it.id !== id)
      console.log("remove", id)
      return Object.assign({} as State, state, { textArray: copyOfArray })
    }
  }
  return state
}

No side effects, pure functions only #

Reducer functions can’t have side effects, and must be pure functions; so there can’t be things like fetch calls, async timers, console logging, random number generator use, or even Date.now() in them.

In order to do these important things, please use enhancers and / or middleware.

Here are a list of rules that reducers must follow.

💡 One of the benefits of using pure functions here is easy implementation of undo history.

Step 2 - Create a store that uses this reducer function #

💡 If you split reducers, then this is where you would declare all the ones that comprise the root reducer function.

// Redux store.
export const store = configureStore({
  reducer: reducerFn,
})

Step 3 - Wrap the component(s) that will share this state with a Provider tag #

Wrap the component(s) that will share this state with <Provider store={store}>...</Provider> where store is what you created and exported in this step.

💡 Wrapping your component in this tag and passing the store allows hooks like useDispatch() and useSelector() to work their “magic” (ie, not having to explicitly import and use the store).

<Provider store={store}>
  <SimpleReduxComponent />
</Provider>

💡 Here’s the official Redux documentation on using a Provider.

Step 4 - Subscribe your component(s) to the store with useSelector hook #

In your component, make sure to subscribe to the store, using the useSelector() hook.

💡 This is used instead of the old mapStateToProps / ‘connect’ mechanism. When the state changes in the store then, the new state will be passed to this component (by the hook), and it will be re-rendered.

This is somewhat equivalent to the useReducer() hook, in the sense that you will get the state from this hook.

If you split reducers, then you can select just a subset of the state that you are interested in (in the function that you pass to the useSelector() hook).

import { _also } from "r3bl-ts-utils"

// function component.
export const SimpleReduxComponent: FC = () => {
  const state: DefaultRootState = useSelector((state) => state)
  const myState = state as State
  /* snip */
}

⚡ Note the _also is a scope function (that is inspired by Kotlin) which you can get from r3bl-ts-utils npm module.

Step 5 - Make sure to dispatch actions in your component(s) to change shared state #

In your component, make sure to call store.dispatch({/* action */}) in order to request changes to happen to your store. This will trigger re-renders of the components that are subscribed to the store.

💡 Instead of importing the store in order to dispatch actions to it, you can also use the useDispatch() hook, which simply returns the store (that your React component has access to, since there’s a Provider in its hierarchy).

import { _also } from "r3bl-ts-utils"

// function component.
export const SimpleReduxComponent: FC = () => {
  const state: DefaultRootState = useSelector((state) => state)
  const myState = state as State

  const addListItem = () =>
    _also(
      {
        type: "add",
        content: "Nazmulnadiaidris".substring(Math.floor(Math.random() * 15)),
      } as ActionAdd,
      (it) => {
        store.dispatch(it)
      }
    )

  const removeListItem = (it: string) =>
    _also({ type: "remove", id: it } as ActionRemove, (it) => {
      store.dispatch(it)
    })

  const render = () => (
    <div className={"Container"}>
      <button onClick={addListItem}>Add</button>
      <ol>
        {myState.textArray.map((text) => (
          <li key={text.id} onClick={() => removeListItem(text.id)}>
            {text.content}
          </li>
        ))}
      </ol>
    </div>
  )

  return render()
}

⚡ Here’s the full example in a single source file SimpleReduxComponent.tsx.

Immutability #

One of the core concepts in Redux is that the state is immutable. There are a few approaches to make this happen.

This is a great article on reference equality, value equality, shallow and deep copy, and how to achieve immutability w/out using any external libraries using the following idioms.

  • Objects: Object.assign({}, originalObject, {propertyToChange:'value'})
  • Arrays: [].concat(oldArray)

Since Redux and React compare references to determine equality, it isn’t necessary to perform a deep copy of all the things, and copying objects and arrays that are contained in the state should suffice for most situations.

💡 Here are other approaches you can take instead of the idioms shown above.

  1. You can use lodash clone.
  2. Yet another approach is to use the immer library.

Selectors #

Selectors are simply functions that know how to extract specific pieces of information from a store’s state value. As an application grows larger, this is a way to keep from repeating this logic which would provide slices of state data needed by different parts of the app. Here’s an example:

type State = { foo: Foo }
type Foo = object

const selectFooValue: Foo = (state: State) => state.foo

const fooValue: Foo = selectFooValue(store.getState())

useSelector hook and React #

The useSelector() hook that comes w/ react-redux takes a selector and automatically subscribes it to the store, all in a single step. It also automatically unsubscribes from the store.

💡 When you wrap your component in a Provider tag, this allows the hook to get access to the store and do its thing. This is similar to how the useDispatch() hook gets access to the store.

Split and combine reducers #

Instead of having a single “root” reducer function you can split them up based on the shape of your store’s state value. For example if your state has two top level properties like this { foo:{}, bar:{} } then you could have two reducer functions: fooReducer() and barReducer().

⚠ The state parameter is different for every reducer, and corresponds to the part of the state it manages.

You can then join them together like this:

type State = { foo: Foo; bar: Bar }
type Foo = object
type Bar = object

function fooReducer(stateFragment: Foo, action) {}

function barReducer(stateFragment: Bar, action) {}

export const rootReducer = (state: State, action) => ({
  foo: fooReducer(state.foo, action),
  bar: barReducer(state.bar, action),
})

Or even more concisely using the following:

import { combineReducers } from "redux"

export const rootReducer = combineReducers({
  foo: fooReducer,
  bar: barReducer,
})

💡 Here’s more information on splitting and combining reducers.

Enhancer #

A store enhancer is a higher-order function that allows you to override the following store methods, and allow you to write code that hooks into the store’s lifecycle events.

  1. dispatch() - You can modify the original store.dispatch() method, and do things like log actions and states.

    💡 You can generate side effects, by making async calls here (timers and REST APIs), do logging, crash reporting, routing, modify actions, pause or even stop the action entirely, etc. If you just want to override this and nothing else, you can use middleware instead of enhancers.

  2. getState() - You can modify the original store.getState() method, and modify the state that gets returned.
  3. subscribe() - You can modify the original store.subscribe() method.

⚡ Here’s a source code example from the official Redux docs.

  1. codesandbox.io.
  2. github repo.

Intercept action dispatch #

Here’s an example of how to create an enhancer that just calls console.log when actions are dispatched.

💡 Note that the original store is enhanced w/ a new dispatch property which has the myDispatchFn function (which simply adds to the original store.dispatch() function).

import { createStore } from "redux"

export const consoleLogOnDispatch = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    function myDispatchFn(action) {
      const result = store.dispatch(action)
      console.log("action dispatched", action)
      console.log("new state", result)
      return result
    }

    return { ...store, dispatch: myDispatchFn }
  }
}

export const store = createStore(rootReducer, undefined, consoleLogOnDispatch)

Intercept state creation #

You can also intercept new state creation. This is a really powerful way of modifying the state. This is an example of state modification that adds timestampMs which is a property that holds the current time in ms when the state was created.

💡 Note that the original store is enhanced w/ a new getState property which has the myGetStateFn function (which simply adds to the original store.getState() function).

export const includeTimestamp = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    function myGetStateFn() {
      return {
        ...store.getState(),
        timestampMs: Date.now(),
      }
    }

    return { ...store, getState: myGetStateFn }
  }
}

Combining both overrides #

In order to use both of these enhancers shown above, you can use the compose function.

import { createStore, compose } from "redux"

export const store = createStore(
  rootReducer,
  undefined,
  compose(includeTimestamp, consoleLogOnDispatch)
)

Middleware #

Unlike enhancers middleware restrict store enhancements to just changing the dispatch() function. Redux middleware provides an extension point, where we can attach our code, between Redux dispatching an action to the store, and the moment it reaches the reducer.

💡 You can generate side effects, by making async calls here (timers and REST APIs), do logging, crash reporting, routing, modify actions, pause or even stop the action entirely, etc. If you just want to override this and nothing else, you can use middleware instead of enhancers.

⚡ Here is the official Redux middleware documentation.

Here’s an example of a single middleware function. You can actually have many of them chained together.

import { createStore, applyMiddleware } from "redux"
import { composeWithDevTools } from "redux-devtools-extension"

function myMiddlewareWithResultOnDispatch(store) {
  return function wrapDispatch(next) {
    return function handleAction(action) {
      // Do anything here, then pass the action onwards with next(action),
      // or restart the pipeline with store.dispatch(action).
      console.log("dispatching", action)
      let result = next(action)

      // Can also use store.getState() here.
      console.log("next state", store.getState())
      // Will be returned by store.dispatch(action).
      return result
    }
  }
}

export const store = createStore(
  rootReducer,
  undefined,
  composeWithDevTools(applyMiddleware(myMiddlewareWithResultOnDispatch))
)
const result = store.dispatch({ type: "foo", payload: "bar" })

💡 composeWithDevTools wraps the middleware so that you can debug it in Chrome DevTools after you install the Redux DevTools Chrome extension.

Notes on this code.

  1. This middleware actually returns something when store.dispatch(action) is called. You don’t have to use this but its there if you need this behavior. Typically the last line would be return next(action).
  2. The call to next(action) is a continuation of the journey of the action to the next middleware in the chain of middleware that may be in the pipeline.
  3. You can use store.getState() to access the store’s state object at any point.

Here’s a chain of simpler middleware that don’t return a result and use more concise arrow functions syntax.

import { createStore, applyMiddleware } from "redux"

export const middlewareConsoleLog = (store) => (next) => (action) => {
  console.log("action", action, "state", store.getState())
  return next(action)
}

export const middlewareConsoleError = (store) => (next) => (action) => {
  console.error("action", action, "state", store.getState())
  return next(action)
}

export const store = createStore(
  rootReducer,
  undefined,
  applyMiddleware(middlewareConsoleLog, middlewareConsoleError)
)
store.dispatch({ type: "foo", payload: "bar" })

Notes on this code.

  1. Middleware form a pipeline around the store’s dispatch method. When we call store.dispatch(action) we are actually just calling the first middleware in the pipeline (which is middlewareConsoleLog function). Typically, a middleware will check to see if the action is a specific type that it cares about, much like a reducer would. If it’s the right type, the middleware might run some custom logic. Otherwise, it passes the action to the next middleware in the pipeline.
  2. Unlike a reducer, middleware can have side effects, including timeouts and other async logic.
  3. When an action is dispatched to the store, the middlewareConsoleLog middleware is the first to run and the last to finish. Here are the functions that the action is passed thru, when store.dispatch(action) is called.
  4. middlewareConsoleLog (due to store.dispatch(action) call)
  5. middlewareConsoleError
  6. Original call to store.dispatch(action)
  7. The root reducer inside store

Async logic and data fetching #

Here is an example of running async code in middleware:

  1. delayedActionMiddleware uses setTimeout() to delay execution of an action.
  2. fetchTodosMiddleware uses axios to make a REST API call.
import { createStore, applyMiddleware } from "redux"

const delayedActionMiddleware = (store) => (next) => (action) => {
  if (action.type === "todos/todoAdded") {
    // Delay this action by one second.
    setTimeout(() => {
      store.dispatch(action)
    }, 1000)
    return
  }

  return next(action)
}

const fetchTodosMiddleware = (store) => (next) => (action) => {
  if (action.type === "todos/fetchTodos") {
    // Make an async API call to fetch todos from the server.
    _also("https://myapi.com/endpoint", async (it) => {
      const { data: payload } = await axios.get(it)
      // Dispatch an action with the todos we received.
      store.dispatch({ type: "todos/todosLoaded", payload })
    })
  }

  return next(action)
}

export const store = createStore(
  rootReducer,
  undefined,
  applyMiddleware(delayedActionMiddleware, fetchTodosMiddleware)
)
store.dispatch({ type: "todos/todoAdded", payload: [] })
store.dispatch({ type: "todos/fetchTodos" })

Redux “Thunk” middleware #

Additionally, Redux provides a redux-thunk middleware which allows you to dispatch functions to the store rather than actions.

⚡ This is available via npm (npm i redux-thunk) if you want to use it.

  • Pros:
    1. You can re-use business logic that isn’t tied to a specific store.
  • Cons:
    1. You lose some TypeScript protections and have to unknown type escape hatch.
    2. It is yet another pattern to learn / adopt, even though you can just make your own enhancer or middleware. And the code for thunk middleware is about 14 LOC.

💡 This article goes into a lot of detail about thunks and how to think about them, and use them.

This is how you can use the redux-thunk middleware.

💡 A thunk can also return a promise. And a component can await the result of that promise in a component. You can pass both async and “normal” functions to dispatch. Here are the official Redux docs on this.

import { createStore, applyMiddleware } from "redux"
import thunkMiddleware from "redux-thunk"
import { composeWithDevTools } from "redux-devtools-extension"

// The store now has the ability to accept thunk functions in `dispatch`.
const store = createStore(
  rootReducer,
  undefined,
  composeWithDevTools(applyMiddleware(thunkMiddleware))
)

// This function returns a function which returns a promise.
// More info: https://stackoverflow.com/q/54426176/2085356
function loadDataReturnsPromise() {
  return (dispatch) => {
    return axios
      .get("https://myapi.com/endpoint")
      .then((res) => res.json())
      .then((data) => store.dispatch({ type: "todos/todosLoaded", data }))
      .catch((err) => console.error("error loading data", err))
  }
}

// function component that uses the async function (aka a promise is returned) above.
const Header = () => {
  const [text, setText] = useState("")
  const [status, setStatus] = useState("idle")
  const dispatch = useDispatch()

  const handleChange = (e) => setText(e.target.value)

  const handleKeyDown = async (e) => {
    // If the user pressed the Enter key:
    const trimmedText = text.trim()
    if (e.which === 13 && trimmedText) {
      // Create and dispatch the thunk function itself
      setStatus("loading")

      // Wait for the promise returned by loadDataReturnsPromise.
      await store.dispatch(loadDataReturnsPromise) // We are dispatching a function, not an action!

      // And clear out the text input
      setText("")
      setStatus("idle")
    }
  }

  let isLoading = status === "loading"
  let placeholder = isLoading ? "" : "Press enter to load data"
  let loader = isLoading ? <div className="loader" /> : null

  return (
    <header>
      <input
        placeholder={placeholder}
        autoFocus={true}
        value={text}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        disabled={isLoading}
      />
      {loader}
    </header>
  )
}

💡 composeWithDevTools wraps the middleware so that you can debug it in Chrome DevTools after you install the Redux DevTools Chrome extension.

The following is a simplistic example of how we could implement the “Thunk” middleware itself. Also, here is the actual implementation of redux-thunk.

💡 The core idea here is creating a function that takes dispatch and getState functions from the store, and then uses them in order to dispatch an action after executing some async logic. By passing the dispatch and getState to the lambda that you’re passing, it frees this logic from being tied to a specific store.

const asyncFunctionMiddleware = (store) => (next) => (action) => {
  // If the "action" is actually a function instead ...
  if (typeof action === "function") {
    // Then call the function and pass `dispatch` and `getState` as arguments.
    return action(store.dispatch, store.getState)
  }

  // Otherwise, it's a normal action - send it onwards.
  return next(action)
}

const fetchSomeData = (dispatch, getState) => {
  // Make an async API call to fetch todos from the server.
  _also("https://myapi.com/endpoint", async (it) => {
    const { data: payload } = await axios.get(it)
    // Dispatch an action with the todos we received.
    store.dispatch({ type: "todos/todosLoaded", payload })
    // Check the updated store state after dispatching
    console.log("State after loading: ", getState())
  })
}

const store = createStore(rootReducer, applyMiddleware(asyncFunctionMiddleware))
store.dispatch(fetchSomeData) // We are dispatching a function, not an action!

⚡ If you want to explore redux-thunk more deeply, here are the official Redux docs.

Memoized selectors and Reselect #

Selector functions accept the Redux state object as an argument and return a value. This is a great place where we can not only access parts of the state, but also derive data from it.

Here’s an example of a selector function that takes some “todo” items in the state and only returns an array of ids (for each todo item).

interface Todo {
  id: string
  text: string
  done: boolean
}
interface State {
  todos: Todo[]
}
const selectTodoIds = (state: State): string[] => state.todos.map((it: Todo) => it.id)

However, Array.map() will always return a new array reference. The useSelector() hook will be run after every action is dispatched. And since this will result in a new array, the component will re-render. Regardless of whether the state.todos has changed or not.

Clearly this is not desirable. We need to cache this array, so as not to force needless re-renders. But we do need this selector function to return a new array when the underlying state.todos actually changes.

What we need is a memoized selector function. Memoized selector functions are selectors that save the most recent result value, and if you call them multiple times with the same inputs, will return the same result value. If you call them with different inputs than last time, they will recalculate a new result value, cache it, and return the new result.

This is where the reselect npm package comes in. We do have to modify our selector function in order to use it w/ reselect.

⚡ You can install reselect via npm i reselect.

We have to split our logic into two pieces:

  1. Input selector - This function allows reselect to understand what the inputs are. It can cache the output for a given input, and if these don’t change, then it will be a cache hit. When the input changes (from what was calculated earlier) then this is a cache miss.
  2. Output selector - This function allows the output value that will be cached, to actually be generated. When there’s a cache hit, this won’t be run. When there’s a cache miss, this will be executed with the argument(s) passed from the input selector.
import { createSelector } from "reselect"
import React, { FC } from "react"
import { useSelector } from "react-redux"
import { _also } from "r3bl-ts-utils"

interface Todo {
  id: string
  text: string
  done: boolean
}
interface State {
  todos: Todo[]
}

const selectTodoIds = createSelector(
  /* Input selector : */ (state: State): Todo[] => state.todos,
  /* Output selector: */ (todos: Todo[]): string[] => todos.map((it: Todo) => it.id)
)

const TodoList: FC = () => {
  const todoIds: string[] = useSelector(selectTodoIds)

  return (
    <ul>
      {_also(new Array<ReactElement>(), (elem: ReactElement[]) => {
        todoIds.forEach((todoId: string) =>
          elem.push(<li key={todoId}>todoId={todoId}</li>)
        )
      })}
    </ul>
  )
}

Any time the state.todos array changes, we’re going to create a new todo IDs array as a result. That includes any immutable updates to todo items like toggling their done field, since we have to create a new array for the immutable update.

This example only showed how to deal w/ one argument. What if you have a selector that can handle multiple arguments? The createSelector() function can actually take any number of input selectors as long as there is a single output selector. The set of values for the input selectors and the output selector be cached. So let’s say that we want to extend the example above to create a selector that only returns the todo ids that are marked as done. Here’s what this might look like.

import { createSelector } from "reselect"

interface Todo {
  id: string
  text: string
  done: boolean
}
interface State {
  todos: Todo[]
  filterByDone: boolean
}

const selectDoneTodoIds = createSelector(
  /* Input selector #1 : */ (state: State): Todo[] => state.todos,
  /* Input selector #2 : */ (state: State): boolean => state.filterByDone,
  /* Output selector   : */ (todos: Todo[], filterByDone: boolean): string[] =>
    todos.filter((todo: Todo) => todo.done === true).map((todo: Todo) => todo.id)
)

Undo history #

With Redux implementing undo history is a breeze. Here are the official docs on this. Since reducers are pure functions w/ no side effects, state is immutable, it makes it possible to very easily switch to an older state in the stack of state histories!

Data structure and algorithm #

Here’s the basic shape of the undo history data structure (or stack). T is the type of the state object that’s in our Redux store.

interface UndoHistory<T> {
  past: T[]
  present: T
  future: T[]
}

We have to define two additional actions to operate on this state: UndoAction and RedoAction.

// Undo and redo actions.
interface ActionUndo {
  type: "UNDO"
}
interface ActionRedo {
  type: "REDO"
}

// Other actions.
interface ActionAdd {
  type: "add"
  content: string
}
interface ActionRemove {
  type: "remove"
  id: string
}

type Action = ActionAdd | ActionRemove | ActionUndo | ActionRedo

⚠ The undo and redo action types are both in all caps to preserve compatibility w/ the redux-undo package (in case you want to use that) which is shown in the next section.

Here’s our algorithm to implement undo and redo.

  • Handling Other Actions:
    1. Insert the present at the end of the past.
    2. Set the present to the new state after handling the action.
    3. Clear the future.
  • Handling Undo:
    1. Remove the last element from the past.
    2. Set the present to the element we removed in the previous step.
    3. Insert the old present state at the beginning of the future.
  • Handling Redo:
    1. Remove the first element from the future.
    2. Set the present to the element we removed in the previous step.
    3. Insert the old present state at the end of the past.

Naive implementation #

Here’s what the code for this might look like, by creating a higher-order reducer to handle the undo and redo actions.

import { createStore } from "redux"

/**
 * Define the undo reducer enhancer function, and the wrapped state shape.
 *
 * - S: the type of the actual state.
 * - A: the Action type of the discriminated union.
 */

interface UndoHistoryStateWrapper<S> {
  past: S[]
  present: S
  future: S[]
}

function undoable<S, A>(
  reducer: (wrappedState: UndoHistoryStateWrapper<S>, action: A) => S
) {
  // Call the reducer with empty action to populate the initial state.
  const initialState: UndoHistoryStateWrapper<S> = {
    past: new Array<S>(),
    present: reducer(undefined, {}),
    future: new Array<S>(),
  }

  // Return a reducer that handles undo and redo
  return function (
    wrappedState: UndoHistoryStateWrapper<S> = initialState,
    action: A
  ): UndoHistoryStateWrapper<S> {
    const { past, present, future } = wrappedState

    switch (action.type) {
      case "undo":
        const previous = past[past.length - 1]
        const newPast = past.slice(0, past.length - 1)
        return {
          past: newPast,
          present: previous,
          future: [present, ...future],
        }
      case "redo":
        const next = future[0]
        const newFuture = future.slice(1)
        return {
          past: [...past, present],
          present: next,
          future: newFuture,
        }
      default:
        // Delegate handling the action to the passed reducer.
        const newPresent: S = reducer(present, action)
        if (present === newPresent) {
          return wrappedState
        }
        return {
          past: [...past, present],
          present: newPresent,
          future: [],
        }
    }
  }
}

// Use the enhanced reducer in the code below.

interface ActionUndo {
  type: "UNDO" // All caps for compatibility w/ `redux-undo` package.
}
interface ActionRedo {
  type: "REDO" // All caps for compatibility w/ `redux-undo` package.
}
interface ActionAdd {
  type: "add"
  content: string
}
interface ActionRemove {
  type: "remove"
  id: string
}
type Action = ActionAdd | ActionRemove | ActionUndo | ActionRedo

interface ActualState {
  todos: Todo[]
}
interface Todo {
  text: string
  done: boolean
}

const reducerFn = (state: UndoHistoryStateWrapper<ActualState>, action: Action) => {}
const store = createStore(undoable<ActualState, Action>(reducerFn))
store.dispatch({
  type: "add",
  content: "Use Redux",
})
store.dispatch({
  type: "UNDO",
})

Using the redux-undo package #

Instead of implementing all this from scratch (like we did in previous section), we can simply use the redux-undo package.

⚡ You can install the redux-undo package using npm i redux-undo.

This package provides an undoable() function, that replaces ours (which is shown in the previous section). You can use it like so.

import undoable from "redux-undo"

const store = createStore(undoable(reducerFn))
store.dispatch({
  type: "add",
  content: "Use Redux",
})
store.dispatch({
  type: "UNDO",
})

If you were using your actual reducer function (reducerFn) in other parts of your code, then you can just continue doing so w/out any changes (eg: in any combineReducers() calls).

However, the way in which you access your state will change. Instead of using state.todos like you would in the past, you now have to use state.todos.present, since the ActualState is wrapped in UndoHistoryStateWrapper<ActualState>.

If you want to add buttons for undo and redo, you will need the following information.

  • state.todos.past.length > 0 means you can enable undo button.
  • state.todos.future.length > 0 means you can enable redo button.
  • In order to dispatch the undo action, you can write store.dispatch( {type: "UNDO"} ).
  • In order to dispatch the redo action, you can write store.dispatch( {type: "REDO"} ).

⚡ Here’s more information on undo redo in the official Redux documentation.

Testing with react-testing-library #

CRA comes w/ Jest and TypeScript support built-in, so writing tests using Jest, TypeScript, and react-testing-library (aka RTL or React Testing Library) is really straightforward.

⚡ You can get more info about what comes out of the box w/ CRA here.

Here are some of the top benefits of using Jest & RTL:

  1. You can test your components in isolation from the child components they render.
  2. You can use real DOM nodes because the tests assert actual behavior that a user would experience and not React Virtual DOM implementation / internals.
  3. A real browser isn’t used to run your tests. Jest will use JSDOM which implements the DOM API in pure Javascript, and runs in a Node.js instance on your machine (or whatever machine you run the tests on).
  4. You can do blackbox and integration testing by mocking web services.

⚡ More information on RTL:

  1. Introduction
  2. Example w/ explanation
  3. Example in codesandbox.io
  4. Cheatsheet

Use RTL instead of Enzyme #

  1. RTL is a replacement for Enzyme.
  • Both Enzyme and RTL are built on top of:
  • Enzyme encourages (and provides utilities for) testing implementation details with rendered instances of components, whereas RTL encourages testing only the “end result” by querying for and making assertions about actual DOM nodes.
  • Here is a list of differences between the two.
  1. Rather than dealing with (Virtual DOM) instances of rendered React components, your tests will work with actual DOM nodes.
  • The utilities RTL provides facilitate querying the DOM in the same way the user would.
  • Finding form elements by their label text (just like a user would), finding links and buttons from their text (like a user would).
  • It also exposes a recommended way to find elements by a data-testid as an “escape hatch” for elements where the text content and label do not make sense or is not practical.

💡 RTL is built on top of dom-testing-library which provides the underlying APIs for testing DOM nodes.

  • Jest runs RTL tests in a headless environment, since the entire DOM API is implemented in pure Javascript by JSDOM.
  • The DOM implementation runs in a Node.js environment on the machine in which the tests are running in and not a real browser.
  • This isn’t like Karma test runner which will spool up an actual browser to run the tests in. This makes these tests very fast.
  1. You can also mock web services for your tests (Node.js instances are spawned on the machine in which you are running the tests), using msw to test out fetch calls. Here is an example.

Install the required packages for RTL #

To get started you should install the RTL and jest-dom packages.

npm install --save @testing-library/react @testing-library/jest-dom

Next, modify your src/setupTests.ts file by adding this line.

// react-testing-library renders your components to document.body,
// this adds jest-dom's custom assertions
import "@testing-library/jest-dom"

Basic unit tests #

Writing simple unit tests are no different in Jest than they are using Jasmine. Here’s an example of a simple unit test that is self contained and doesn’t really have a system under test. The system under test is the MyMatcher class which is declared in the test itself. This class is meant to mimic a Jasmine matcher. And the tests assert that it works as expected.

import { _also } from "r3bl-ts-utils"

class MyMatcher {
  constructor(private _arg: boolean = false) {}

  set arg(value: boolean) {
    this._arg = value
  }
  get arg(): boolean {
    return this._arg
  }

  isFalse = (): boolean => !this.arg

  isTrue = (): boolean => this.arg

  get not(): MyMatcher {
    return new MyMatcher(!this.arg)
  }
}

describe("MyMatcher -> myMatcher(arg)", () => {
  const myMatcher: MyMatcher =
    /* new MyMatcher(true) */
    _also(new MyMatcher(), (it) => (it.arg = true))
  test("myMatcher.isTrue() is true", () => expect(myMatcher.isTrue()).toBe(true))
  test("myMatcher.isFalse() is false", () => expect(myMatcher.isFalse()).toBe(false))
  test("myMatcher.not.isTrue() is false", () =>
    expect(myMatcher.not.isTrue()).toBe(false))
})

💡 TypeScript has an awesome “escape hatch” to allow private variables to be accessed in tests. Here’s a detailed SO answer on this topic.

Here’s an example:

  • Let’s say you have a class with a private variable named _x like so: class Clazz {private _x:number = 0}
  • Then you can access it w/ type safety in a test when using the array access syntax, eg: new Clazz()[_x], which will be treated as a number.
  • So you can assert expect(typeof new Clazz()[_x]).toBe('number').

Complex unit tests #

Let’s write a test for the reducer function of SimpleReduxComponent.tsx. The tests will be pretty simple, w/ the only complication being the way in which the function and types themselves are exported. The test needs to know some internals about the component which users of this component don’t really need to know about (nor should they, since that would break encapsulation).

In order to implement this, one approach is to use a namespace that simply exports only the required symbols for testing (this is done in the SimpleReduxComponent.tsx file). Here are the fewest symbols that need to be accessible by the test.

// Export namespace for testing.
export namespace SimpleReduxComponentForTesting {
  export const _reducerFn: Reducer<State | undefined, Action> = reducerFn
  export type _State = State
  export type _Action = Action
}

💡 The symbols all have to be renamed for this namespace, otherwise, they would collide w/ their original declarations.

These symbols are then imported in the actual test, like this:

import { SimpleReduxComponentForTesting } from "../SimpleReduxComponent"
type State = SimpleReduxComponentForTesting._State | undefined
type Action = SimpleReduxComponentForTesting._Action
const reducerFn: Reducer<State, Action> = SimpleReduxComponentForTesting._reducerFn

Now that the exports are complete, the test itself is pretty simple. Here’s the SimpleReduxComponent.test.tsx. The test is pretty simple, it does the following things:

  1. Tests to see that the initial state is generated correctly by the reducer function.
  2. Tests to see whether dispatching an add action actually adds something to the state.
  3. Tests to see whether dispatching a remove action actually removes something from the state.
import { SimpleReduxComponentForTesting } from "../SimpleReduxComponent"
import { Reducer } from "@reduxjs/toolkit"

describe("SimpleReduxComponent reducer function", () => {
  type State = SimpleReduxComponentForTesting._State | undefined
  type Action = SimpleReduxComponentForTesting._Action
  const reducerFn: Reducer<State, Action> = SimpleReduxComponentForTesting._reducerFn

  test("sets initial state", () => {
    const ignoredAction: Action = { type: "add", content: "text" }
    const state: State = reducerFn(undefined, ignoredAction)
    console.log(state)
    expect(state!!.textArray).toHaveLength(2)
  })

  test("dispatch add action works", () => {
    const action: Action = { type: "add", content: "foo" }
    const initialState: State = {
      textArray: [
        { id: "id4", content: "fffff" },
        { id: "id5", content: "gggg" },
      ],
    }
    const newState: State = reducerFn(initialState, action)
    console.log(newState)
    expect(initialState).not.toEqual(newState)
    expect(newState!!.textArray).toHaveLength(3)
    expect(newState!!.textArray[2]).toMatchObject({ content: "foo" })
  })

  test("dispatch remove action works", () => {
    const action: Action = { type: "remove", id: "id4" }
    const initialState: State = {
      textArray: [
        { id: "id4", content: "fffff" },
        { id: "id5", content: "gggg" },
      ],
    }
    const newState: State = reducerFn(initialState, action)
    console.log(newState)
    expect(initialState).not.toEqual(newState)
    expect(newState!!.textArray).toHaveLength(1)
    expect(newState!!.textArray[0]).toMatchObject({ content: "gggg" })
  })
})

Basic UI tests #

In order to write UI tests we will be using a combination of APIs from:

  1. RTL - APIs like render() will render your component into a container that is appended to document.body. This container is automatically removed from when the test completes, so make sure to call render() at the start of each it or test block.
  2. dom-testing-library - APIs like getByRole() will allow you to interrogate the JSDOM and assert things about it. Here’s a list of HTML ARIA roles that are used to match DOM elements.

⚡ Here’s an example of a simple UI tests SimpleReduxCompoennt.test.tsx.

The basic structure of the UI tests are very similar.

  1. Render some component that you are testing (the system under test).
  2. Wait for HTML element to be added to the DOM (via call to screen).
  3. Assert that the DOM looks the way you expect. Or fire an event here, and then check to see that the DOM is what you expect.

Here’s the simplest test case.

test("displays few list items at start", async () => {
  render(
    <Provider store={store}>
      <SimpleReduxComponent />
    </Provider>
  )
  await waitFor(() => screen.getByRole("list"))
  expect(screen.getByRole("list").children).toHaveLength(2)
})

Here’s a test case where an event is fired before the DOM is checked.

test("clicking item removes it", async () => {
  render(
    <Provider store={store}>
      <SimpleReduxComponent />
    </Provider>
  )
  await waitFor(() => screen.getByRole("list"))
  fireEvent.click(screen.getByRole("list").children[0])
  expect(screen.getByRole("list").children).toHaveLength(2)
})

Complex UI tests #

In this section we will write a test that deals with resizing a browser window (such as what you would need to do to test media queries). Here are the key things that we will cover in this example:

  1. How to resize a JSDOM browser window (since an actual browser is not used).
  2. How to wrap React updates in act().

Here’s the system under test.

ComponentWithoutState.tsx.

import { MessagePropsWithChildren } from "../types"
import styles from "../../styles/App.module.css"
import React, { Dispatch, SetStateAction, useEffect, useState } from "react"
import { _let } from "r3bl-ts-utils"

export const TestIdWindowSize = "test-id-window-size"

const setWindowSize = (
  setWidth: Dispatch<SetStateAction<number>>,
  setHeight: Dispatch<SetStateAction<number>>
) => {
  setWidth(window.innerWidth)
  setHeight(window.innerHeight)
}

type SizeStateHookType = [number, Dispatch<SetStateAction<number>>]

export const ComponentWithoutState = (props: MessagePropsWithChildren) => {
  const [width, setWidth]: SizeStateHookType = useState(0)
  const [height, setHeight]: SizeStateHookType = useState(0)

  useEffect(
    () =>
      _let(
        (event: UIEvent) => setWindowSize(setWidth, setHeight),
        (it) => {
          window.addEventListener("resize", it)
          return () => window.removeEventListener("resize", it)
        }
      ),
    [] /* Run once, like componentDidMount. */
  )

  useEffect(
    () => setWindowSize(setWidth, setHeight),
    [] /* Run once, like componentDidMount. */
  )

  const render = () => (
    <section className={styles.Container}>
      <code>{props.message}</code>
      {props.children}
      <div data-testid={TestIdWindowSize}>{`${width} x ${height}`}</div>
    </section>
  )

  return render()
}

And here’s the test file itself. In this test, the window is resized after rendering the component. Then the component is checked to see whether it got this change in width and height. If you were using a media query then you would check the DOM to see whether the structure that you were expecting is actually present.

MediaQueryTest.test.tsx

import { render, screen, waitFor } from "@testing-library/react"
import React from "react"
import { act } from "react-dom/test-utils"
import {
  ComponentWithoutState,
  TestIdWindowSize,
} from "../components/basics/ComponentWithoutState"

/** Get around `window.innerWidth` and `window.innerHeight` are readonly. */
const resizeWindow = (x: number, y: number) => {
  window = Object.assign(window, { innerWidth: x })
  window = Object.assign(window, { innerHeight: y })
  window.dispatchEvent(new Event("resize"))
}

describe("media query test", () => {
  it("should be large window size", async () => {
    render(
      <div>
        <ComponentWithoutState message={"test"} />
      </div>
    )

    // Wrap any calls to React state changes in act().
    // More info: https://davidwcai.medium.com/react-testing-library-and-the-not-wrapped-in-act-errors-491a5629193b
    act(() => resizeWindow(2000, 1000))

    await waitFor(() => screen.getByTestId(TestIdWindowSize))

    const checkContent = screen.getByTestId(TestIdWindowSize)
    expect(checkContent).toHaveTextContent("2000 x 1000")
  })
})

💡 Note the use of act() to wrap the call to resizeWindow(). This is necessary when you write code that mutates a component’s state using one of your functions. This ensures that all the required work has been done before you make your DOM assertions.

Writing integration tests #

Install msw package #

In order to mock web services, you will need to install the msw module as a dev dependency.

npm i -D msw

⚡ Learn more about mocking REST APIs (using msw) here.

⚡ This is a great tutorial on using msw and TypeScript.

Mocking the server (REST API endpoints) #

Before we get started writing the test, here are some functions that need to be created in order to mock the search endpoint of the TheCatApi.com.

⚡ Here’s the full source file TestUtils.ts.

  1. Setup and teardown - Create the server and tie its start and stop w/ the lifecycle of the tests.

    export function setupAndTeardown(): SetupServerApi {
      const server: SetupServerApi = setupServer()
      beforeAll(() => server.listen())
      afterEach(() => server.resetHandlers())
      afterAll(() => server.close())
      return server
    }
    

    ⚠ Make sure to call setupAndTeardown() at the start of your actual UI test.

  2. Define the data returned by the mock API - Here are some supporting constants that are needed in the next steps.

    export const cannedResponseOk = [
      {
        breeds: [],
        categories: [],
        id: "jK5X2xGJ7",
        url: "https://cdn2.thecatapi.com/images/jK5X2xGJ7.jpg",
      },
      {
        breeds: [],
        categories: [],
        id: "9c6",
        url: "https://cdn2.thecatapi.com/images/9c6.jpg",
      },
      {
        breeds: [],
        categories: [],
        id: "ab8",
        url: "https://cdn2.thecatapi.com/images/ab8.jpg",
      },
    ]
    const TheCatApi = {
      apiKey: process.env.REACT_APP_CAT_API_KEY,
      search: {
        host: "https://api.thecatapi.com",
        endpoint: "/v1/images/search",
        config: { params: { limit: 3, size: "full" } },
      },
    }
    
  3. Mock the REST API w/ handlers for the happy path - Create the function that handles HTTP GET requests that are made to https://api.thecatapi.com/v1/images/search.

    // More info: https://mswjs.io/docs/getting-started/mocks/rest-api
    export const restHandlerSearchOk: RestHandler = rest.get(
      TheCatApi.search.host + TheCatApi.search.endpoint,
      (
        req: RestRequest<DefaultRequestBody, RequestParams>,
        res: ResponseComposition,
        ctx: RestContext
      ) => {
        console.log("restHandlerSearchOk.req", req)
        // console.log("req.url.searchParams", req.url.searchParams)
        // console.log("req.params", req.params)
        return res(ctx.json(cannedResponseOk))
      }
    )
    
  4. Mock the REST API w/ handlers for the unhappy path - Create a function that handles returning a 500 HTTP error code for HTTP GET requests that are made to https://api.thecatapi.com/v1/images/search.

    export const restHandlerSearchError: RestHandler = rest.get(
      TheCatApi.search.host + TheCatApi.search.endpoint,
      (
        req: RestRequest<DefaultRequestBody, RequestParams>,
        res: ResponseComposition,
        ctx: RestContext
      ) => {
        console.log("restHandlerSearchError.req", req)
        // console.log("req.url.searchParams", req.url.searchParams)
        // console.log("req.params", req.params)
        return res(ctx.status(500))
      }
    )
    

Testing the mocked server (REST API endpoints) itself #

Now w/ the setup and supporting functions out of the way, we can actually write the tests. The tests shown here actually don’t have a React component under test, they’re just tests that make axios calls to the mocked API endpoints.

⚡ Here’s the full source file TheCatApiEndpointMock.test.tsx.

  1. Setup the server - The first step is calling setupAndTeardown() at the start of your test.

    const server: SetupServerApi = setupAndTeardown()
    
  2. Test the happy path mocked endpoint - The test does quite a few things.

    test("TheCatApi search endpoint works", async () => {
      server.use(restHandlerSearchOk)
      axios.defaults.headers.common["x-api-key"] = TheCatApi.apiKey
      _also(TheCatApi.search, async (it) => {
        const { data: payload } = await axios.get(it.host + it.endpoint, it.config)
        expect(payload).toHaveLength(3)
      })
    })
    
  3. First, it sets up the server to handle the search endpoint of the REST API.

    • It binds the restHandlerSearchOk to the server via server.use().
    • All RestHandlers registered via use() will be torn down after each test in the afterEach() call (declared in in the first step).
  4. Then, it connects to the msw mock server and makes the GET request, gets the response, and makes the assertion that 3 items are in the payload.

    ⚠ One thing to note is that the URL query params that are sent via HTTP GET (from the axios call in the following step) don’t actually show up on the mocked server. However, the headers do show up.

  5. Test the unhappy path mocked endpoint - This test is very similar to the previous one, except that the REST API is mocked to throw a 500 HTTP error code. It also demonstrates how to detect errors for async functions.

    // More info: https://stackoverflow.com/a/47887098/2085356
    const makeGetRequest: () => Promise<any> = async () =>
      _let(TheCatApi.search, async (it) => {
        axios.defaults.headers.common["x-api-key"] = TheCatApi.apiKey
        const { data: payload } = await axios.get(it.host + it.endpoint, it.config)
        return payload
      })
    
    test("TheCatApi endpoint fails as expected", async () => {
      server.use(restHandlerSearchError)
      await expect(makeGetRequest()).rejects.toThrow(Error)
    })
    

Using the mocked server in a UI test #

Now that we have the endpoints mocked and tested, we can write tests that renders a React component which uses these endpoints.

⚡ Here’s the full source file TheCatApiEndpointMock.test.tsx.

We are going to write two tests. One that makes sure that the CatApiComponent renders a list of images when the search endpoint returns a valid result. Another that renders an error message when the search endpoint returns an error. Here’s the describe block containing these which is simply added to our previous test.

import { CatApiComponent, TheCatApi } from "../CatApiComponent"

describe("CatApiComponent works as expected ⤵", () => {
  test("Component renders results with API happy path", async () => {
    server.use(restHandlerSearchOk)
    render(<CatApiComponent.FC />)
    await waitFor(() => screen.getByRole("list"))
    console.log(prettyDOM(screen.getByRole("list")))
    expect(screen.getAllByRole("listitem").length).toBe(3)
  })

  test("Component renders error state with API unhappy path", async () => {
    server.use(restHandlerSearchError)
    render(<CatApiComponent.FC />)
    await waitFor(() => screen.getByTestId(CatApiComponent.TestIds.fetchError))
    console.log(prettyDOM(screen.getByTestId(CatApiComponent.TestIds.fetchError)))
    expect(screen.getByRole("heading")).toHaveTextContent(
      "Error: Request failed with status code 500"
    )
  })
})

There are a couple of things to note in this test.

  1. You can use ARIA to search for components using screen.getByRole() or you can use the data-testid / screen.getByTestId() mechanism. You can see the use of each one in the test above.
  2. Since we are using test ids, we are wrapping the ids and the React component in the CatApiComponent namespace. This allows us to have clean import statements that don’t result in collisions of this component’s test ids w/ any other variables or imports.

  3. We can access the test ids via CatApiComponent.TestIds.
  4. We can access the React component via CatApiComponent.FC.

  5. The prettyDOM() function is really useful when trying to get a handle on the structure of the DOM for your assertions.
  6. Since we are making async axios calls, we need to use await waitFor(()=>screen.???) so that the DOM is actually populated before we make our assertions.

Writing snapshot tests #

Snapshot testing was introduced in Jest around 2016, and it’s meant to be a lightweight way to perform UI tests and it is meant to be a complement (not replacement) for unit and integration testing.

💡 Here are some good reference guides on snapshot testing:

The idea behind snapshot testing is that it should be easy to take a snapshot of what a component should look like, and then compare any future changes to a component to this snapshot. This is meant to be an easy way to do UI testing. There is no need to write any assertions for what is expected in the DOM. Simply save the “last known good” snapshot of a component, and when any changes are made to this component they will automatically be compared to the saved snapshot (when the tests are run).

⚡ Install the required packages for react-test-renderer

In order to write these tests we must use react-test-renderer and not the RTL or dom-testing-library. Here are the things you must install to get started.

# Install the package.
npm install react-test-renderer
# Get the TypeScript bindings for your IDE.
npm i --save-dev @types/react-test-renderer

Here’s a simple snapshot test for the ComponentWithoutState.tsx.

💡 Note that the first time you run this you will have to update the snapshot. You can do that by doing any of the following:

  1. Running the test in IDEA Ultimate or Webstorm.
  2. In a terminal by using npm test -u. You can also interactively update the snapshots if you run npm test in your terminal.
import React from "react"
import renderer from "react-test-renderer"
import { ComponentWithoutState } from "../ComponentWithoutState"

it("ComponentWithoutState renders correctly", function () {
  const tree = renderer
    .create(<ComponentWithoutState message={"snapshot test"} />)
    .toJSON()
  expect(tree).toMatchSnapshot()
})

ComponentWithoutState.snapshot.test.tsx.

When a snapshot is updated it will create a file that you should check into your version control. Here’s an example of a snapshot file generated by the test above /__tests__/__snapshots__/ComponentWithoutState.snapshot.test.tsx.snap.

Data APIs for development #

Here are some data APIs that might be useful when building prototype apps.

Debugging in Webstorm or IDEA Ultimate #

Use this guide for create-react-app.

  1. Simply package.json and click on the green arrow beside the “run” script.
  2. In the tool window, press “Ctrl+Shift” and click on the localhost:3000 hyperlink and that will spawn a debugging session w/ a Chrome browser that is spawned just for this session!
  3. Save the run configurations produced by the steps above in the project file.
  4. Also now that the JavaScript debugging session run configuration is created, you can just use npm run start to start the server in a terminal and still be able to debug it!

Upgrade CRA itself to the latest version #

Follow instructions in the CRA changelog. For example, you can run something like the following w/out ejecting CRA itself.

npm install --save --save-exact react-scripts@4.0.3

⚡ You can find the list of releases here.

You can upgrade any npm packages that you’re using with the following.

npm update

Using CRA and environment variables #

Read all about how to do this in this guide.

For example, to use the Cat API, you have to do the following steps:

  1. Get an API key from https://thecatapi.com/.
  2. Save this API key as REACT_APP_CAT_API_KEY in $HOME/.profile (for Ubuntu & GNOME).
  • You have to logout and log back in, in order for this to take effect on GUI apps launched via GNOME.
  • CRA will grab all the environment variables that are prefixed w/ REACT_APP and compile them into the minified Javascript code that it generates.
  1. In your TypeScript code, you can use the following variable process.env.REACT_APP_CAT_API_KEY in order to access the value of this environment variable.

CSS Reset #

In order to use CSS Reset, do the following:

  1. Copy the contents of this CSS file into reset.css. Feel free to modify this file to suit your needs.
  2. Then add @import "reset.css"; to App.module.css, which is used by App.tsx.
  3. I also add entries for elements like button and input which are not explicitly set by the default Reset CSS stylesheet (and thus end up using user agent stylesheet, which isn’t what I want and why I’m using Reset CSS in the first place).

normalize.css did not work for my needs (it’s supported by CRA). Even after using the following instructions, the browser user agent stylesheet was just messing up all the spacing.

  • Using normalize.css is pretty straight forwards following this guide.
    1. Simply run npm install normalize.css
    2. Then add import 'normalize.css' line to the top of index.tsx
  • CRA comes w/ normalize.css.

Using CSS class pseudo selectors to style child elements of a parent #

Using CSS class pseudo selectors in order to style child elements of a parent (which has this style applied) w/out having to manually assign classes to each of these children. Let’s say that the parent has this class DottedBox, which will do this, here’s the CSS. Here’s a video by Kevin Powell where he uses this pattern for flexbox.

  1. .DottedBox { padding: 8pt; border: 4pt dotted cornflowerblue; }
  2. .DottedBox > * { /* this gets applied to all the children */ }

Composition over inheritance #

Use composition over inheritance to make components reusable.

  1. This happens when you think about a component as a “generic box” and simply pass other JSX elements inside of them as props.children.
  2. You can see this in ComponentWithoutState.
  3. In order to get this to work with TypeScript you have to make sure to add this to the props type childComp?: React.ReactNode. For example, take a look at MessagePropsWithChildren in types.tsx

Callable #

Callable interfaces are great, and I’ve done an implementation of this in ColorConsole included in r3bl-ts-utils.

  • I took a slightly different approach this time, using the great information in this SO Answer which I also used in the ColorConsole implementation.
  • Here are the key takeaways in the ReactReplayClassComponent implementation:
    1. A class can’t implement the Callable interface.
    2. However, any member of the class can, and that member can be exposed as Callable.
    3. This member is exposed as a getter.
    4. This member can then be the only export in the module.
  • In this case, the getter simply returns the reference to the ‘generatorImpl’ method. So we can write things like GenerateReactElement.generator(...) instead of just GenerateReactElement.generator (which is the normal use of a getter).

TypeScript namespaces #

⚡ Read the official TypeScript docs on namespaces.

They allow encapsulation of variables, types, and other symbols in a neat package. This can be exported to other modules that need them or just be used inside a single file to organize and hide details.

Here’s an example of using this in CatApiComponent.tsx. Here are some excerpts from this file.

Here is a snippet that shows the use of namespace to encapsulate the details of accessing this web API.

export namespace TheCatApi {
  export const apiKey = process.env.REACT_APP_CAT_API_KEY as string
  export const search = {
    host: "https://api.thecatapi.com",
    endpoint: "/v1/images/search",
    config: { params: { limit: 3, size: "full" } },
  } as const
  export type SearchResults = { id: string; url: string }
}

It is possible to import a symbol from one namespace, into another namespace, even in the same file. Here’s an excerpt from the CatApiComponent.tsx file used above.

namespace MyActions {
  interface ActionFetchStart {
    type: "fetchStart"
  }

  interface ActionFetchOk {
    type: "fetchOk"
    payload: TheCatApi.SearchResults[]
  }

  interface ActionFetchError {
    type: "fetchError"
    error: any
  }

  export type Action = ActionFetchStart | ActionFetchOk | ActionFetchError
}

The encapsulated namespace for this web API can then be used in other files (eg: TestUtils.ts) and TheCatApiEndpointMock.test.tsx. Here are some excerpts from these files.

import { TheCatApi } from "../CatApiComponent"

export const restHandlerSearchOk: RestHandler = rest.get(
  TheCatApi.search.host + TheCatApi.search.endpoint,
  (
    req: RestRequest<DefaultRequestBody, RequestParams>,
    res: ResponseComposition,
    ctx: RestContext
  ) => res(ctx.json(cannedResponseOk))
)

export const makeGetRequest: () => Promise<any> = async () =>
  _let(TheCatApi.search, async (it) => {
    axios.defaults.headers.common["x-api-key"] = TheCatApi.apiKey
    const { data: payload } = await axios.get(it.host + it.endpoint, it.config)
    return payload
  })

The following sections show different ways to use namespace.

TypeScript readonly vs ReadonlyArray #

More info on readonly vs ReadonlyArray:

💡 Here’s more info on TypeScript type narrowing, truthy/falsy, user defined type predicates, and discriminated unions from the official docs.

If you mark an variable holding an array as readonly, you can’t reassign it. However, you can push(), pop(), and mutate it! In the example below, values supports mutation!

class ContainsArray {
  constructor(readonly values: string[]) {}
}

const object = new ContainsArray(["a", "b"])
object.values.push("d") // This is ok! 👎

So, in order to lock down that array, you can do the following. Note the subtle difference in the keyword readonly showing up twice!

class ContainsArray {
  constructor(readonly values: readonly string[]) {}
}

const object = new ContainsArray(["a", "b"])
object.values.push("d") // This is NOT ok! 👍

Another way to write the same thing is as follows.

class ContainsArray {
  constructor(readonly values: ReadonlyArray<string>) {}
}

const object = new ContainsArray(["a", "b"])
object.values.push("d") // This is ok!

In the code for ReactReplayClassComponent, the following lines do the same thing (preventing any mutations on elementArray):

  • readonly elementArray: readonly JSX.Element[]
  • readonly elementArray: ReadonlyArray<JSX.Element>

TypeScript prop and state types #

In strict mode, the prop and state types (if any) need to be declared explicitly. The React codebase supports generics which is how these types are declared.

💡 You can also pass {} to specify that there are no props or state.

Here is a tutorial that shows how to specify prop and state types for function and class components.

Here’s an example for a class component which takes props but contains no state. Note that no children can be passed.

export interface AnimationFramesProps {
  animationFrames: Readonly<ReactElement>
}

export class ReactReplayClassComponent extends React.Component<AnimationFramesProps, {}> {
  /* snip */
}

If you wanted children to be passed, you could do something like this.

export interface AnimationFramesPropsWithKids extends AnimationFramesProps {
  /** More info: https://linguinecode.com/post/pass-react-component-as-prop-with-TypeScript */
  children?: Readonly<ReactElement>
}

export class ReactReplayClassComponent extends React.Component<
  AnimationFramesPropsWithKids,
  {}
> {
  /* snip */
}

You can also wrap your prop type, eg: MyPropType, w/ PropsWithChildren<MyPropType> when declaring your function component to declare that your component can accept children. And then you can use the destructuring syntax to get the required props out.

💡 TypeScript supports built-in and user defined utility types and advanced types which are in leverage in PropsWithChildren type. It takes your prop type as an argument and returns a new type which includes everything in your type and children?: ReactNode | undefined.

Here’s an example.

export type TooltipOverlayProps = {
  text: string
}

export const TooltipOverlay: FC<PropsWithChildren<TooltipOverlayProps>> = ({
  children,
  text,
}) => {
  /* snip */
}

Note the use of the destructuring syntax to get the specific properties (children, text) out of the passed props object. The types are defined in TooltipOverlayProps (text comes from this) and PropsWithChildren (children comes from this).

Here’s an example for a function component. Note the use of FC to specify that this is a function component that takes a prop. Being a function component, you can’t declare any state types.

export const ReactReplayFunctionComponent: FC<AnimationFramesProps> = (
  props
): ReactElement => {
  /* snip */
}

TypeScript and ReactNode, ReactElement, JSX.Element #

This SO thread has the answers. Basically,

  1. Use ReactElement where possible.
  2. When TypeScript complains at times, use ReactElement | null.
  3. Class components (return ReactElement | null) and function components (return ReactElement) actually return different things.

TypeScript types in array and object destructuring #

More info:

Example of object destructuring.

type AnimationFrames = Readonly<Array<ReactElement>>
type MyProps = { animationFrames: AnimationFrames }
const { animationFrames }: MyProps = props

Example of array destructuring.

type FrameIndexStateType = [number, Dispatch<SetStateAction<number>>]
const [frameIndex, setFrameIndex]: FrameIndexStateType = useState<number>(0)
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.



📦 Install our useful Rust command line apps using cargo install r3bl-cmdr (they are from the r3bl-open-core project):
  • 🐱giti: run interactive git commands with confidence in your terminal
  • 🦜edi: edit Markdown with style in your terminal

giti in action

edi in action

Related Posts