The Journey of #100DaysOfCode (@sourabhbagrecha)

#Day97 of #100daysofcode

Today I learned how to emulate certain user interaction events that don’t have a dedicated method (like mouseover). Rather than use button.click().
To overcome this we can dispatchEvent API, here’s how it works.

  // First, we will create a mouse click event using the MouseEvent interface
  const incrementClickEvent = new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
    button: 0,
  })
  // Now we can simply utilise the above event in our increment button's dispatch event, 
  // which will trigger the incrementClickEvent whenever it gets clicked.
  act(() => increment.dispatchEvent(incrementClickEvent))

We have seen how to test React components by emulating the component render and UI interactions, but that comes with a lot of boilerplate, let’s take a look at how React Testing Library by Kent C Dodds can improve the developer experience and help us reduce the boilerplate:

import * as React from 'react'
import {render, fireEvent} from '@testing-library/react'
import Counter from '../../components/counter'

test('counter increments and decrements when the buttons are clicked', () => {
  const {container} = render(<Counter />)
  const [decrement, increment] = container.querySelectorAll('button')
  const message = container.firstChild.querySelector('div')

  expect(message).toHaveTextContent('Current count: 0')
  fireEvent.click(increment)
  expect(message).toHaveTextContent('Current count: 1')
  fireEvent.click(decrement)
  expect(message).toHaveTextContent('Current count: 0')
})
2 Likes

#Day98 of 100daysofcode

Today I learned that, in any software, we should avoid testing implementation details of our components. Because the implementation of our abstractions does not matter to the users, we will test our component in such a way that it feels like an actual user is interacting with our application totally unaware of the internals.
Now, instead of getting the DOM node reference like this:

We will utilize the accessibility tree to get the Role and Name of the element rendered to the screen. Let’s take a look at the accessibility pane to understand how we can access the “Increment” button from the screen utility provided by the React Testing Library:

// Getting the increment button by specifying the role as `button` and regex matching the name of the button.
const increment = screen.getByRole('button', {name: /increment/i})
const decrement = screen.getByRole('button', {name: /decrement/i})

// Getting the message text's DOM node reference by regex matching the inner text.
const message = screen.getByText(/current count/i)

I also learned how to test forms and form submissions in React:

test('submitting the form calls onSubmit with username and password', async () => {
  const handleSubmit = jest.fn()
  render(<Login onSubmit={handleSubmit} />)
  const username = 'chucknorris'
  const password = 'i need no password'

  await userEvent.type(screen.getByLabelText(/username/i), username)
  await userEvent.type(screen.getByLabelText(/password/i), password)
  await userEvent.click(screen.getByRole('button', {name: /submit/i}))

  expect(handleSubmit).toHaveBeenCalledWith({
    username,
    password,
  })
  expect(handleSubmit).toHaveBeenCalledTimes(1)
})
3 Likes

#Day99 of #100daysofcode

Today I learned how to test custom hooks, contexts, and API integration.
Here’s how we can utilize the renderHook utility provided by the React Testing Library to test our hooks without creating a proxy-component to :

import {renderHook, act} from '@testing-library/react'
import useCounter from '../../components/use-counter'

test('exposes the count and increment/decrement functions', () => {
  const {result} = renderHook(useCounter)
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

test('allows customization of the initial count', () => {
  const {result} = renderHook(useCounter, {initialProps: {initialCount: 3}})
  expect(result.current.count).toBe(3)
})

test('allows customization of the step', () => {
  const {result} = renderHook(useCounter, {initialProps: {step: 2}})
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(2)
  act(() => result.current.decrement())
  expect(result.current.count).toBe(0)
})

test('the step can be changed', () => {
  const {result, rerender} = renderHook(useCounter, {
    initialProps: {step: 3},
  })
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(3)
  rerender({step: 2})
  act(() => result.current.decrement())
  expect(result.current.count).toBe(1)
})

But how can we test our components that are dependent on some Context Provider? The easiest and cleanest way is to utilize the render function’s options object and provide our Context’s Provider to the Wrapper key of the options object.

import * as React from 'react'
import {render, screen} from '@testing-library/react'
import {ThemeProvider} from '../../components/theme'
import EasyButton from '../../components/easy-button'

function renderWithProviders(ui, {theme = 'light', ...options} = {}) {
  const Wrapper = ({children}) => (
    <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>
  )
  return render(ui, {wrapper: Wrapper, ...options})
}

test('renders with the light styles for the light theme', () => {
  renderWithProviders(<EasyButton>Easy</EasyButton>)
  const button = screen.getByRole('button', {name: /easy/i})
  expect(button).toHaveStyle(`
    background-color: white;
    color: black;
  `)
})

test('renders with the dark styles for the dark theme', () => {
  renderWithProviders(<EasyButton>Easy</EasyButton>, {
    theme: 'dark',
  })
  const button = screen.getByRole('button', {name: /easy/i})
  expect(button).toHaveStyle(`
    background-color: black;
    color: white;
  `)
})

Most of our React apps usually interact with an API through the internet, so here’s one of the ways using which we can test our app’s functionality when communicating through an API(refer to the comments to understand the flow):

test(`logging in displays the user's username`, async () => {
  render(<Login />)
  const {username, password} = buildLoginForm()

  await userEvent.type(screen.getByLabelText(/username/i), username)
  await userEvent.type(screen.getByLabelText(/password/i), password)
  await userEvent.click(screen.getByRole('button', {name: /submit/i}))
  
  // Once we click on the submit button, our app will display a loading screen 
  // unless we get the login request's response from the server.
  // Now, we want to wait for the loading screen to disappear:
  await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i))

  // Once the loading screen is unmounted from the screen.
  // We can test the outcomes.
  expect(screen.getByText(username)).toBeInTheDocument()
})
3 Likes

#Day100 of #100daysofcode

Finally!
Today I learned about React’s Suspense API and its different use cases. Basically, Suspense lets the component wait for something before rendering.
Let’s take a look at few use cases for Suspense

1. Suspense for data fetching

Since suspense lets component wait before rendering, it can be easily utilized for data fetching like this:

let pokemon
let pokemonError
// The fetchPokemon is simply calling an HTTP REST endpoint to get the details of a Pokemon
let pokemonPromise = fetchPokemon('pikachu').then(
  p => (pokemon = p),
  e => (pokemonError = e),
)

// PokemonInfo is a React Component that renders the UI to display the information for the specified Pokemon
function PokemonInfo() {
  if (pokemonError) {
    throw pokemonError
  }
  if (!pokemon) {
    throw pokemonPromise
  }
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  return (
    <PokemonErrorBoundary>
       <React.Suspense fallback={<div>Loading Pokemon...</div>}>
          <PokemonInfo />
       </React.Suspense>
    </PokemonErrorBoundary>
  )
}

2. Suspense for Render as you fetch

One of the biggest overheads of creating a React App is to manage the state efficiently and appropriately while keeping it readable. There’s a lot more time involved (first load the code, then parse the code, then run the code, then render the component, and finally make the request).

By utilizing the Suspense API we can simply suspend the component until a promise(HTTP call) is resolved/rejected and display the content once done and we don’t have to worry about any state updates.

function PokemonInfo({pokemonResource}) {
  const pokemon = pokemonResource.read()
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')
  const [pokemonResource, setPokemonResource] = React.useState(null)

  React.useEffect(() => {
    if (!pokemonName) {
      setPokemonResource(null)
      return
    }
    // Here we are simply setting the promise(returned by the createResource function) to the pokemonResource state
    setPokemonResource(createResource(fetchPokemon(pokemonName)))
  }, [pokemonName])

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleReset() {
    setPokemonName('')
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <React.Suspense fallback={<PokemonInfoFallback name={pokemonName} />}>
        <div className="pokemon-info">
          {pokemonResource ? (
            <PokemonErrorBoundary
              onReset={handleReset}
              resetKeys={[pokemonName]}
            >
              {/* Below we are utilizing the pokemonResource even if the promise isn't resolved yet,*/}
              {/* because anyways our <Suspense> boundary will detect this promise and it will */}
              {/* show a fallback component until and unless the promise gets resolved or rejected. */}
              <PokemonInfo pokemonResource={pokemonResource} />
            </PokemonErrorBoundary>
          ) : (
            'Submit a pokemon'
          )}
        </div>
      </React.Suspense>
    </div>
  )
}

export default App

3. Significantly improving the UX using useTransition hook along with Suspense

It’s fascinating to see how the Suspense API makes our components declarative with the least amount of effort. But that’s great for Developer Experience, what about User Experience?

The useTransition hook allows us to prevent abrupt UI changes that may cause bad UX. So, in the previous case, whenever the component has to wait for an async operation to complete we show the user a loading screen immediately.
But useTransition hook gives us the ability to pause abrupt changes for a while before showing them on the loading-screen. It helps us in smoothening the transition phase between 2 states.

// In the config, we will specify the timeoutMs for which we don't want the fallback component
// to be visible and do something else while it is pending.
const SUSPENSE_CONFIG = {timeoutMs: 4000}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
  const [pokemonResource, setPokemonResource] = React.useState(null)

  React.useEffect(() => {
    ...
    // Whenever there's a change in the pokemonName, we want to fetch the details of the 
    // pokemon, so we will avoid this abrupt state change to a promise and we will keep the state 
    // as before unless this API responds or the timeout ends.
    startTransition(() => {
      setPokemonResource(createResource(fetchPokemon(pokemonName)))
    })
  }, [pokemonName, startTransition])

  return (
    ...
      {/* We will make the current pokemon a little blur while the state transition is taking place */}
      {/* using the isPending flag provided by the hook. */}
      <div style={{opacity: isPending ? 0.6 : 1}} className="pokemon-info">
        {pokemonResource ? (
          <PokemonErrorBoundary
            onReset={handleReset}
            resetKeys={[pokemonResource]}
          >
            <React.Suspense
              fallback={<PokemonInfoFallback name={pokemonName} />}
            >
              <PokemonInfo pokemonResource={pokemonResource} />
            </React.Suspense>
          </PokemonErrorBoundary>
        ) : (
          'Submit a pokemon'
        )}
      </div>
    ...
  )
}

4. Cache resources

In order to optimize our app by avoiding any unnecessary/redundant API calls, we can implement caching, and together with Suspense it can provide a delightful experience to our users.
Everything remains the same as before, it’s just that we will keep a local cache object in memory to store results of recently made API calls.

const pokemonResourceCache = {}

function getPokemonResource(name) {
  const lowerName = name.toLowerCase()
  let resource = pokemonResourceCache[lowerName]
  if (!resource) {
    resource = createPokemonResource(lowerName)
    pokemonResourceCache[lowerName] = resource
  }
  return resource
}

function App() {
  ...
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)

  React.useEffect(() => {
    startTransition(() => {
      setPokemonResource(getPokemonResource(pokemonName))
    })
  }, [pokemonName, startTransition])
  ...
}

5. Suspense Images

Sometimes, in order to provide a consistent experience across our app, we may want to load the images together with the content. It feels weird when our app has loaded all the associated information about the content and the image is still loading.
To overcome this, we can suspend that part of the app where we want to re-render a component and we will wait for it to load in the memory, and the next time this image gets requested it will be served from the cache in the browser. But for the first time, we will have to handle the image load process.


function preloadImage(src) {
  return new Promise(resolve => {
    const img = document.createElement('img')
    img.src = src
    img.onload = () => resolve(src)
  })
}

const imgSrcResourceCache = {}

function Img({src, alt, ...props}) {
  let imgSrcResource = imgSrcResourceCache[src]
  if (!imgSrcResource) {
    imgSrcResource = createResource(preloadImage(src))
    imgSrcResourceCache[src] = imgSrcResource
  }
  return <img src={imgSrcResource.read()} alt={alt} {...props} />
}

function PokemonInfo({pokemonResource}) {
  const pokemon = pokemonResource.read()
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <Img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

...
  <React.Suspense
       fallback={<PokemonInfoFallback name={pokemonName} />}
   >
        <PokemonInfo pokemonResource={pokemonResource} />
    </React.Suspense>
...

6. Suspense custom hook

How cool would it be, if we could just wrap all of the Suspense logic into its own custom hook? The idea is to basically remove the boilerplate Suspense stuff and keep our components clean.

// Custom Suspense hook
function usePokemonResource(pokemonName) {
  const [pokemonResource, setPokemonResource] = React.useState(null)
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)

  React.useEffect(() => {
    if (!pokemonName) {
      setPokemonResource(null)
      return
    }
    startTransition(() => {
      setPokemonResource(getPokemonResource(pokemonName))
    })
  }, [pokemonName, startTransition])

  return [pokemonResource, isPending]
}

// Here's how we can consume this hook in our components
function App() {
  ...
  const [pokemonResource, isPending] = usePokemonResource(pokemonName)
  ...
}

7. Suspense List for a predictable loading experience

As our app grows in size, we might want to implement Suspense across different components in our app to make sure that a single component doesn’t slow down our complete app or even worse just crashes your app.
But how can we orchestrate their execution that’s predictable.
SuspenseList to the rescue.
The SuspenseList component has the following props:

  • revealOrder : the order in which the suspending components are to render
    • can have one of the following values: {undefined}, "forwards", "backwards", "together"
  • tail : determines how to show the fallbacks for the suspending components
    • can have one of the following values: {undefined}, "collapsed", "hidden"
  • children : other react elements which render

Let’s take a look at an example SuspenseList:

<PokemonErrorBoundary
onReset={handleReset}
resetKeys={[pokemonResource]}
>
<React.SuspenseList revealOrder="forwards" tail="collapsed">
  <React.Suspense fallback={fallback}>
    <NavBar pokemonResource={pokemonResource} />
  </React.Suspense>
  <div className={cn.mainContentArea}>
    <React.SuspenseList revealOrder="forwards">
      <React.Suspense fallback={fallback}>
        <LeftNav />
      </React.Suspense>
      <React.SuspenseList revealOrder="together">
        <React.Suspense fallback={fallback}>
          <MainContent pokemonResource={pokemonResource} />
        </React.Suspense>
        <React.Suspense fallback={fallback}>
          <RightNav pokemonResource={pokemonResource} />
        </React.Suspense>
      </React.SuspenseList>
    </React.SuspenseList>
  </div>
</React.SuspenseList>
</PokemonErrorBoundary>

And that’s a wrap!

5 Likes

Congratulations @SourabhBagrecha :100: !

2 Likes

:trophy: Congrats on finishing your #100DaysOfCode adventure @SourabhBagrecha! Thanks for taking us on a learning adventure with your daily updates :wink:

Cheers,
Stennie

3 Likes

Congratulations @SourabhBagrecha :tada: :100:!

3 Likes

Congratulations !! @SourabhBagrecha
It is inspiring to see your journey💯

3 Likes

This topic was automatically closed after 180 days. New replies are no longer allowed.