Skip to content

Instantly share code, notes, and snippets.

@BolajiOlajide
Created February 9, 2021 18:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BolajiOlajide/8c813d32b7d27fe25516557ac4ee99b8 to your computer and use it in GitHub Desktop.
Save BolajiOlajide/8c813d32b7d27fe25516557ac4ee99b8 to your computer and use it in GitHub Desktop.
/*
Sometimes we only want to run an effect the very first time the component mounts.
In my experience, the majority of these times have been firing off a tracking event.
Usually I’ll maintain a hasSent local variable that I flip from false to true after I’ve sent it the first time.
*/
import { useRef, useEffect } from 'react'
const useInitialMount = () => {
// refs exist across component re-renders, so
// we can use it to store a value for the
// subsequent renders. We're tracking if it's
// the first render, which is initially `true`
const isFirst = useRef(true)
// the very first render, the ref will be
// `true`. but we immediately set it to `false`
// so that every render after will be `false`
if (isFirst.current) {
isFirst.current = false
// return true the very first render
return true
}
// return false every following render
return false
}
const Page = ({ pageName, items }) => {
const isInitialMount = useInitialMount()
useEffect(() => {
// only call `trackEvent` for initial mount.
// don't call it ever again, even if
// `pageName` or `items.length` change
if (isInitialMount) {
trackEvent(pageName, items.length)
}
}, [pageName, items.length, isInitialMount])
// render UI
}
/*
In my previous post on Handling async React component effects after unmount, I outline 4 ways to solve the problem where we try to update the state of a component after it has already unmounted.
We prevent updating the state with the result of a fetch call when the response returned after the component had already been unmounted.
*/
import { useEffect, useRef, useCallback } from 'react'
// returns a function that when called will
// return `true` if the component is mounted
const useIsMounted = () => {
// the ref to keep track of mounted state across renders
const mountedRef = useRef(false)
// helper function that will return the mounted state.
// using `useCallback` because the function will likely
// be used in the deps array of a `useEffect` call
const isMounted = useCallback(() => mountedRef.current, [])
// effect sets mounted ref to `true` when run
// and the sets to `false` during effect cleanup (i.e. unmount)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return isMounted
}
const Results = () => {
const [items, setItems] = useState([])
const isMounted = useIsMounted()
useEffect(() => {
fetchItems().then((newItems) => {
// only set state if the component
// is still mounted after receiving
// the async data
if (isMounted()) {
setItems(newItems)
}
})
}, [isMounted])
// render UI
}
/*
Media queries allow us to change our UI based upon a host of media features (most commonly the size of the window).
Media queries are normally used in CSS, but the matchMedia() API allows us to execute media queries in JavaScript when necessary.
Maybe we need to change props or render entirely different components depending on the results of a media query.
*/
import { useState, useEffect } from 'react'
const useMedia = (query) => {
// initialize state to current match value
const [matches, setMatches] = useState(() => window.matchMedia(query).matches)
const isMounted = useIsMounted()
useEffect(() => {
if (!isMounted()) {
return
}
const mediaQueryList = window.matchMedia(query)
const listener = () => {
// update `matches` state whenever query match changes.
// `isMounted()` check is for extra protection in case
// listener somehow fires in between unmount and
// listener removal
if (isMounted()) {
setMatches(mediaQueryList.matches)
}
}
mediaQueryList.addListener(listener)
// sync initial matches again
setMatches(mediaQueryList.matches)
return () => {
mediaQueryList.removeListener(listener)
}
}, [query, isMounted])
return matches
}
const Example = () => {
const isSmall = useMedia('(max-width: 480px)')
return <p>Screen is {isSmall ? 'small' : 'large'}</p>
}
/*
if the component re-renders for another reason (for instance, a change in another state value), then our previous value gets overwritten even though it technically hasn’t changed.
We likely want a usePrevious that only updates the previous value if it differs from the new value.
*/
import { useRef, useState, useEffect } from 'react'
const usePrevious = (value) => {
// use refs to keep track of both the previous &
// current values
const prevRef = useRef()
const curRef = useRef(value)
const isInitialMount = useInitialMount()
// after the initial render, if the value passed in
// differs from the `curRef`, then we know that the
// value we're tracking actually changed. we can
// update the refs. otherwise if the `curRef` &
// value are the same, something else caused the
// re-render and we should *not* update `prevRef`.
if (!isInitialMount && curRef.current !== value) {
prevRef.current = curRef.current
curRef.current = value
}
return prevRef.current
}
const Example = () => {
const [time, setTime] = useState(() => new Date())
const [count, setCount] = useState(0)
// use `usePrevious` to keep track of the `count`
// from the last time it changed
const prevCount = usePrevious(count)
// update `date` every 1 sec to have another state updating
useEffect(() => {
const intervalId = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(intervalId)
})
return (
<div>
<button onClick={() => setCount((curCount) => curCount + 1)}>+</button>
<button onClick={() => setCount((curCount) => curCount - 1)}>-</button>
<p>
Count: {count}, Old Count: {prevCount}
</p>
<p>The time is {time.toLocaleTimeString()}.</p>
</div>
)
}
/*
There are some DOM events, like window resize or document scroll, the fire a lot.
They fire much faster than the DOM can update, so if we try to update the DOM for each event, our app feels sluggish.
We can try to work around this problem by manually debouncing or throttling the events, but there’s an interesting alternative.
We can make use of requestAnimationFrame to debounce in a different way by only setting the state on the next animation frame.
*/
import { useState, useCallback, useRef, useEffect } from 'react'
const useRafState = (initialState) => {
// this is the actual state
const [state, setState] = useState(initialState)
// keep track of the `requestAnimationFrame` request ID
// across renders and successive calls to `useRafState`
const requestId = useRef(0)
// the actual state setter we'll return.
// using `useCallback` so that it's memoized
// just like `setState`
const setRafState = useCallback((value) => {
// cancel active request before making next one.
// this is debouncing.
cancelAnimationFrame(requestId.current)
// create new request to set state on animation frame
requestId.current = requestAnimationFrame(() => {
setState(value)
})
}, [])
// cancel any active request when component unmounts
useEffect(() => {
return () => cancelAnimationFrame(requestId.current)
})
return [state, setRafState]
}
const Example = () => {
const [width, setWidth] = useRafState(0)
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
// set initial value
setWidth(window.innerWidth)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [setWidth])
return <p>Window width: {width}</p>
}
/*
Sometimes in React we need to create a unique ID to use an id for DOM elements.
The ID itself doesn’t really matter, but is needed for associating two elements together for accessibility purposes using ARIA attributes within a complex component.
The trick is that we don’t want to generate a new ID for every render. Once we’ve generated an ID for the component, we want it to remain the same.
Our good ol’ friend useRef will come to the rescue again, and we can wrap up the logic into something easily reusable.
*/
import { useRef } from 'react'
let GLOBAL_ID = 0
const useUniqueId = () => {
const idRef = useRef('')
const isInitialMount = useInitialMount()
// generate the ID for the first render
// and store in the ref to remain for
// subsequent renders
if (isInitialMount) {
GLOBAL_ID += 1
idRef.current = `id${GLOBAL_ID}`
}
return idRef.current
}
const NavMenu = ({ items }) => {
const id = useUniqueId()
const buttonId = `${id}-button`
const menuId = `${id}-menu`
return (
<>
<button id={buttonId} aria-controls={menuId}>
+
</button>
<ul id={menuId} aria-labelledby={buttonId} role="menu">
{items.map((item) => (
<li key={item.id}>
<a role="menuitem" href={item.url}>
{item.title}
</a>
</li>
))}
</ul>
</>
)
}
/*
We may need to track the scroll position of the window in order to know when to pin or unpin some sticky content on the page.
And since the document scroll event fires often, we’ll likely want to debounce updating the state. We can build useWindowScroll using the useRafState custom Hook we just implemented.
*/
import { useEffect } from 'react'
const getPos = () => ({
x: window.scrollX,
y: window.scrollY,
})
const useWindowScroll = () => {
const [pos, setPos] = useRafState({ x: 0, y: 0 })
useEffect(() => {
const handleScroll = () => {
// `useRafState` will debounce on animation frame
setPos(getPos())
}
window.addEventListener('scroll', handleScroll)
// set initial value
setPos(getPos())
return () => {
window.removeEventListener('scroll', handleScroll)
}
})
return pos
}
const Example = () => {
const { x, y } = useWindowScroll()
return (
<p>
Position: ({x}, {y})
</p>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment