Created
February 9, 2021 18:43
-
-
Save BolajiOlajide/8c813d32b7d27fe25516557ac4ee99b8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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> | |
</> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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