Y
Published on

BigFrontEnd React Coding Questions

Authors
  • avatar
    Name
    Yinhuan Yuan
    Twitter

Introduction

This blog post summarizes React Coding Questions on BigFrontEnd.Dev.

1.the React Counter app

1.https://bigfrontend.dev/react/The-React-Counter

As the first React problem, you are asked to create the famous Counter app.

counter starts from 0.

  • click the '+' button to increment.
  • click the '-' button to decrement.
  • data-testid is used to test your code, please do not remove them.

Solution:

import React, {useState, useCallback} from 'react'

export function App() {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  const decrement = useCallback(() => {
    setCount((count) => count - 1);
  }, []);
  return (
    <div>
      <button data-testid="decrement-button" onClick={decrement}>
        -
      </button>
      <button data-testid="increment-button" onClick={increment}>
        +
      </button>
      <p>clicked: {count}</p>
    </div>
  );
}

2.useTimeout() Hook

2.https://bigfrontend.dev/react/usetimeout

Create a hook to easily use setTimeout(callback, delay).

reset the timer if delay changes DO NOT reset the timer if only callback changes

Solution:

We can utilize useRef to create a reference that stores the callback function and use useEffect to update the ref whenever the callback changes. Additionally, another useEffect can be used to update the setTimeout delay in the ref when the delay changes.

import { useEffect, useRef } from 'react'

export function useTimeout(callback: () => void, delay: number) {
  // useRef for Callback Storage:
  const savedCallback = useRef(callback)

  // Remember the latest callback if it changes
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  useEffect(() => {
    const id = setTimeout(() => savedCallback.current(), delay)

    // Cleanup timeout on component unmount or when delay changes
    return () => clearTimeout(id)
  }, [delay])
}

3.useIsFirstRender() Hook

3.https://bigfrontend.dev/react/useIsFirstRender

Create a hook to tell if it is the first render.

function App() {
  const isFirstRender = useIsFirstRender()
  // only true for the first render
  ...
}

Solution:

We can use useRef to create a value initialized as true. After the initial render (when the component mounts), this value is then reset to false.

import { useEffect, useRef } from 'react'

export function useIsFirstRender(): boolean {
  const isFirstRender = useRef<boolean>(true)

  useEffect(() => {
    isFirstRender.current = false
  }, [])

  return isFirstRender.current
}

4.useSWR Hook

  1. https://bigfrontend.dev/react/useSWR-1

SWR is a popular library of data fetching.

Let's try to implement the basic usage by ourselves.

import React from 'react'
function App() {
  const { data, error } = useSWR('/api', fetcher)
  if (error) return <div>failed</div>
  if (!data) return <div>loading</div>
  return <div>succeeded</div>
}

this is not to replicate the true implementation of useSWR() The first argument key is for deduplication, we can safely ignore it for now

Solution:

We can use useState to create two state variables, data and error, and utilize useEffect to trigger the data fetching callback whenever the key changes.

import { useState, useEffect } from 'react'

export function useSWR<T = any, E = any>(
  _key: string,
  fetcher: () => T | Promise<T>
): {
  data?: T
  error?: E
} {
  const promise = fetcher()
  if (!(promise instanceof Promise)) {
    return { data: promise }
  }
  const [data, setData] = useState<T>()
  const [error, setError] = useState<E>()

  useEffect(() => {
    const fetchData = async () => {
      try {
        const data = await promise
        setData(data)
      } catch (e) {
        setError(e)
      }
    }
    fetchData()
  }, [_key])

  return { data, error }
}

5.usePrevious() Hook

5.https://bigfrontend.dev/react/usePrevious

Create a hook usePrevious() to return the previous value, with initial value of undefined.

Solution:

We can use useRef to create a variable that stores the previous value. Each time the value changes, it is saved in this variable.

import { useRef, useEffect } from 'react'

export function usePrevious<T>(value: T): T | undefined {
  const previousValueRef = useRef<T | undefined>()

  useEffect(() => {
    /* This line runs after every render after the value is changed*/
    previousValueRef.current = value
  }, [value])
  // It will return the value before each render.
  return previousValueRef.current
}

6.useHover() Hook

6.https://bigfrontend.dev/react/useHover

It is common to see conditional rendering based on hover state of some element.

We can achive it by CSS pseduo class :hover, but for more complex cases it might be better to have state controlled by script.

Now you are asked to create a useHover() hook.

function App() {
  const [ref, isHovered] = useHover()
  return <div ref={ref}>{isHovered ? 'hovered' : 'not hovered'}</div>
}

Solution:

import { useRef, useState, useCallback } from 'react'
import type { Ref } from 'react'

export function useHover<T extends HTMLElement>(): [Ref<T>, boolean] {
  const elRef = useRef<T | null>(null)
  const [isHovered, setIsHovered] = useState(false)
  const onMouseEnter = () => {
    setIsHovered(true)
  }
  const onMouseLeave = () => {
    setIsHovered(false)
  }

  const setRef = useCallback((node: T) => {
    if (elRef.current) {
      elRef.current.removeEventListener('mouseenter', onMouseEnter)
      elRef.current.removeEventListener('mouseleave', onMouseLeave)
    }

    if (node) {
      elRef.current = node
      elRef.current.addEventListener('mouseenter', onMouseEnter)
      elRef.current.addEventListener('mouseleave', onMouseLeave)
    }
  }, [])

  return [setRef, isHovered]
}

7.useToggle() Hook

7.https://bigfrontend.dev/react/useToggle

It is quite common to see switches and checkboxes in web apps.

Implement useToggle() to make things easier

function App() {
  const [on, toggle] = useToggle()
  ...
}

Solution:

import { useState } from 'react'
export function useToggle(on: boolean = false): [boolean, () => void] {
  const [state, setState] = useState(on)
  const toggle = () => {
    setState((preState) => !preState)
  }
  return [state, toggle]
}

8.useDebounce() Hook

8.https://bigfrontend.dev/react/useDebounce

For a frequently changing value like text input you might want to debounce the changes.

Implement useDebounce() to achieve this.

function App() {
  const [value, setValue] = useState(...)
  // this value changes frequently,
  const debouncedValue = useDebounce(value, 1000)
  // now it is debounced
}

The logic should be similar to 6. implement basic debounce()

Solution:

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    }; // clearup function which will be called when value or delay is changed.
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

9.useEffectOnce() Hook

9.https://bigfrontend.dev/react/useEffectOnce

Here is a simple problem, implement useEffectOnce() as the name says itself, it runs an effect only once.

Solution:

import { useEffect, EffectCallback } from 'react'

export function useEffectOnce(effect: EffectCallback) {
  useEffect(effect, [])
  // Empty dependency array ensures the effect runs only once
}

10.Create a Phone Number Component

10.https://bigfrontend.dev/react/phone-number-input

Create a PhoneNumberInput component.

  • only accepts numerical digits
  • format the number automatically as (123)456-7890 by
  • adding the parenthesis when the 4th digit is entered
  • also adding - before 7th digit You can use the default text input without any styling.

Follow-up

What if user removes some digits in the middle, does caret jumps to the end in your approach?

caret position is not covered in our tests.

Solution:

import React, { useState } from 'react'

export const PhoneNumberInput = () => {
  const [phoneNumber, setPhoneNumber] = useState('')

  const handleInputChange = (e) => {
    // /\D/ matches any character that is not a digit (\D stands for “non-digit”).
    const input = e.target.value.replace(/\D/g, '') // Remove non-numeric characters
    let formattedPhoneNumber = ''
    // Format the phone number
    if (input.length <= 3) {
      formattedPhoneNumber = input
    } else if (input.length >= 4 && input.length <= 6) {
      formattedPhoneNumber = `(${input.slice(0, 3)})${input.slice(3)}`
    } else if (input.length >= 7 && input.length <= 10) {
      formattedPhoneNumber = `(${input.slice(0, 3)})${input.slice(3, 6)}-${input.slice(6)}`
    } else {
      formattedPhoneNumber = `(${input.slice(0, 3)})${input.slice(3, 6)}-${input.slice(6, 10)}`
    }
    setPhoneNumber(formattedPhoneNumber)
  }

  return (
    <input
      type="text"
      data-testid="phone-number-input"
      value={phoneNumber}
      onChange={handleInputChange}
    />
  )
}

11.useFocus() Hook

11.https://bigfrontend.dev/react/useFocus

CSS pseudo-class :focus-within could be used to allow conditional rendering in parent element on the focus state of descendant elements.

While it is cool, in complex web apps, it might be better to control the state in script.

Now please create useFocus() to support this.

function App() {
  const [ref, isFocused] = useFocus()
  return (
    <div>
      <input ref={ref} />
      {isFocused && <p>focused</p>}
    </div>
  )
}

Solution:

import React, { Ref, useState, useRef, useEffect, RefObject } from 'react'

export function useFocus<T extends HTMLElement>(): [Ref<T>, boolean] {
  const [isFocused, setIsFocused] = useState<boolean>(false)
  const ref = useRef<T>(null)

  useEffect(() => {
    const handleFocusIn = () => setIsFocused(true)
    const handleFocusOut = () => setIsFocused(false)

    const element = ref.current
    // !!IMPORTANT!!
    // initialize the focus state when currentElement changes.
    setIsFocused(document.activeElement === element)
    if (element) {
      element.addEventListener('focus', handleFocusIn)
      element.addEventListener('blur', handleFocusOut)
    }

    return () => {
      if (element) {
        element.removeEventListener('focus', handleFocusIn)
        element.removeEventListener('blur', handleFocusOut)
      }
    }
  }, [ref.current])

  return [ref, isFocused]
}

12.useArray()

12.https://bigfrontend.dev/react/useArray

When array is used in React state, we need to deal with actions such as push and remove.

Implement useArray() to make life easier.

const { value, push, removeByIndex } = useArray([1, 2, 3])

Solution:

import React, { useState, useCallback } from 'react'

type UseArrayActions<T> = {
  push: (item: T) => void
  removeByIndex: (index: number) => void
}

export function useArray<T>(initialValue: T[]): { value: T[] } & UseArrayActions<T> {
  const [array, setArray] = useState(initialValue)

  const push = useCallback((element: T) => {
    setArray((prevArray) => [...prevArray, element])
  }, [])

  const removeByIndex = useCallback((index: number) => {
    setArray((prevArray) => {
      const newArray = [...prevArray]
      newArray.splice(index, 1)
      return newArray
    })
  }, [])

  return {
    value: array,
    push,
    removeByIndex,
  }
}

13.proxy-state valtio

13.https://bigfrontend.dev/react/proxy-state-valtio

valtio claims to make proxy-state simple.

Let's take a look at the basic example.

import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
// This will re-render on `state.count` change
// but not on `state.text` change
function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}
// you can mutate the state from anywhere
setInterval(() => {
  ++state.count
}, 1000)

Now you are asked to implement proxy() and useSnapshot() to make above code example work.

This question is NOT to re-implement valtio, rather it is to test your understanding of proxy-state. The test cases on BFE.dev only covers the basic usage of above two functions, not the full abilities of valtio.

Solution:

This solution provides a function named proxy and a hook named useSnapshot, which together create a system for managing and reacting to changes in an object’s state using React’s state management system.

1. proxy Function

This function creates a proxy object that intercepts get and set operations on the object's properties. The goal is to monitor and react to changes in the object's properties.

Breakdown:
  • Generics:

    • The function is defined with a generic type <T extends object>. This means it can work with any object type T, ensuring type safety.
  • keys Set:

    • A Set called keys is used to track which properties of the object are accessed. The set only allows unique keys.
  • setState Reference:

    • A reference to React's setState function is stored in the setState variable. This is used later to trigger React re-renders.
  • Proxy Creation:

    • A Proxy object is created around the initial value. This proxy intercepts get and set operations:
      • get Trap:
        • Whenever a property on the proxy is accessed, the get trap is triggered. It adds the accessed key to the keys set and returns the value of that property.
      • set Trap:
        • Whenever a property is set (i.e., when its value changes), the set trap checks:
          1. If the key is "setState", it updates the setState reference (used for re-renders).
          2. If the new value is the same as the current one, no action is taken (the change is ignored).
          3. If the value changes and the key has been accessed before, it clears the keys set and calls setState to update the React component's state, causing a re-render.
  • Return:

    • The proxy object is returned, allowing the calling code to interact with the object while also tracking changes.

2. useSnapshot Function

This hook links the proxy object to React’s state system, enabling reactivity when properties on the proxy are modified.

Breakdown:
  • State Initialization:

    • useState is called with the proxy object. This initializes React state, and setState is used to update the state later.
  • Injecting setState:

    • The setState function is injected into the proxy object by setting a special "setState" property on the proxy. This allows the proxy's set trap to trigger React updates.
  • Return:

    • The proxy object is returned so that the component using useSnapshot can interact with it, with changes automatically causing re-renders.

How It Works Together

  • Proxy:

    • The proxy function creates a proxied object that monitors changes to its properties. It triggers re-renders when those properties change, as long as they have been accessed before.
  • useSnapshot:

    • The useSnapshot hook connects this proxy to React's state system, enabling components to reactively update when the proxy's properties are modified.
import React, { useState, useEffect } from 'react'

export function proxy<T extends object>(initialValue: T): T {
  const keys = new Set<string>()
  let setState: React.Dispatch<React.SetStateAction<T>>

  const proxy = new Proxy<T>(
    { ...initialValue },
    {
      get(target: T, key: string) {
        keys.add(key)
        return Reflect.get(target, key)
      },
      set(target: T, key: string, value) {
        if (key === 'setState') {
          // This is just to update the dispatch reference in this method, won't be required
          setState = value
          return false
        }

        if (Reflect.get(target, key) === value) {
          // no-change occured w.r.t prev value
          return true
        }

        const status = Reflect.set(target, key, value)

        if (status && keys.has(key)) {
          keys.clear()
          setState((prev) => ({
            ...prev,
            [key]: value,
          }))
        }

        return status
      },
    }
  )

  return proxy
}

export function useSnapshot<T extends object>(proxy: T): T {
  const [_, setState] = useState(proxy)

  // Pass dispatch handler to set interceptor in proxy method
  Reflect.set(proxy, 'setState', setState)

  return proxy
}

14.useIsMounted() Hook

14.https://bigfrontend.dev/react/implement-useismounted

When we handle async requests in React, we need to pay attention if the component is already unmounted.

Please implement useIsMounted() for us to easily tell if the component is still not unmounted.

Solution:

import React, { useRef, useEffect } from 'react'

export function useIsMounted(): () => boolean {
  const isMountedRef = useRef(false)

  useEffect(() => {
    isMountedRef.current = true
    return () => {
      isMountedRef.current = false
    }
  }, [])

  return () => isMountedRef.current
}

15.useClickOutside() Hook

15.https://bigfrontend.dev/react/useclickoutside

Click above header menu on this page, you can see that the dropdown menu is dismissed after clicking outside.

Now you are asked to implement a React hook to make it eaiser to implement such behavior.

function Component() {
  const ref = useClickOutside(() => {
    alert('clicked outside')
  })
  return <div ref={ref}>..</div>
}

Solution:

import { useEffect, useRef } from 'react'

export function useClickOutside(callback: () => void) {
  const ref = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        callback()
      }
    }

    document.addEventListener('mousedown', handleClickOutside)
    return () => {
      document.removeEventListener('mousedown', handleClickOutside)
    }
  }, [callback])

  return ref
}

16.useUpdateEffect() Hook

16.https://bigfrontend.dev/react/useUpdateEffect

Implement useUpdateEffect() that it works the same as useEffect() except that it skips running the callback on first render.

Solution:

import React, { useEffect, useRef, EffectCallback, DependencyList} from 'react';

export function useUpdateEffect(effect: EffectCallback, deps?: DependencyList) {
  const isMountedRef = useRef(false);

  useEffect(() => {
    if (!isMountedRef.current) {
      isMountedRef.current = true;
    } else {
      return effect();
    }
  }, deps); // Only re-run effect if dependencies change
}

17.useWindowWidth Hook

import { useState, useEffect } from 'react'
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])
  return width
}
export default useWindowWidth

Reference: Mastering Custom Hooks in React: A Comprehensive Guide

18.useFetch Hook

import { useState, useEffect } from 'react'
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url)
        if (!response.ok) throw new Error('Network response was not ok')
        const result = await response.json()
        setData(result)
      } catch (err) {
        setError(err)
      } finally {
        setLoading(false)
      }
    }
    fetchData()
  }, [url])
  return { data, loading, error }
}
export default useFetch

Usage:

import React from 'react'
import useFetch from './useFetch'
function App() {
  const { data, loading, error } = useFetch('https://api.example.com/data')
  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}
export default App

Reference: Mastering Custom Hooks in React: A Comprehensive Guide

19.useForm Hook

import { useState } from 'react'
function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues)
  const handleChange = (event) => {
    const { name, value } = event.target
    setValues({
      ...values,
      [name]: value,
    })
  }
  const handleSubmit = (event) => {
    event.preventDefault()
    onSubmit(values)
  }
  return {
    values,
    handleChange,
    handleSubmit,
  }
}
export default useForm

Usage:

import React from 'react'
import useForm from './useForm'
function App() {
  const initialValues = { username: '', email: '' }
  const onSubmit = (values) => {
    console.log('Form Submitted:', values)
  }
  const { values, handleChange, handleSubmit } = useForm(initialValues, onSubmit)
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Username:
          <input type="text" name="username" value={values.username} onChange={handleChange} />
        </label>
      </div>
      <div>
        <label>
          Email:
          <input type="email" name="email" value={values.email} onChange={handleChange} />
        </label>
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

export default App

Reference: Mastering Custom Hooks in React: A Comprehensive Guide