├── useIsoLayoutEffect.tsx ├── useMount.tsx ├── README.md ├── useIsHover.tsx ├── useTheme.tsx ├── useIncrement.tsx ├── useWindowWidth.tsx ├── useInterval.tsx ├── useMeasure.tsx ├── useTimeout.tsx ├── useScrollFreeze.tsx ├── useClickOutside.tsx ├── useWdyu.tsx ├── useLocalStorage.tsx ├── useCoursePath.tsx ├── useScript.ts ├── .gitignore └── useDimensions.tsx /useIsoLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | const useIsoLayoutEffect = 4 | typeof window !== 'undefined' ? useLayoutEffect : useEffect 5 | 6 | export default useIsoLayoutEffect 7 | -------------------------------------------------------------------------------- /useMount.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export const useMount = (fn: () => void) => { 4 | useEffect(fn, []) 5 | } 6 | 7 | export const useUnmount = (fn: () => void) => { 8 | useEffect(() => fn) 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scott's Dope Hooks 2 | 3 | These are hooks that I've either written or collected from various sources. I will update these hooks individually with attribution. Either way these are the hooks we use on https://www.leveluptutorials.com 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /useIsHover.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from 'react' 2 | const useIsHover = () => { 3 | const [state, setState] = useState(false) 4 | 5 | const bind = useMemo(() => { 6 | return { 7 | onMouseOver: () => setState(true), 8 | onMouseLeave: () => setState(false), 9 | } 10 | }, []) 11 | 12 | return [state, bind] 13 | } 14 | 15 | export { useIsHover } 16 | -------------------------------------------------------------------------------- /useTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | const useTheme = (user: any) => { 4 | useEffect(() => { 5 | if (user?.settings?.theme) { 6 | document.body.className = '' 7 | document.body.classList.add(user.settings.theme) 8 | } else { 9 | document.body.className = '' 10 | } 11 | return () => {} 12 | }, [user, user?.settings?.theme]) 13 | } 14 | 15 | export default useTheme 16 | -------------------------------------------------------------------------------- /useIncrement.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export const useIncrement = (initial: number, step = 1, lowerLimit = 0) => { 4 | const [value, setValue] = useState(initial) 5 | const inc = () => setValue(prev => prev + step) 6 | const deinc = () => 7 | setValue(prev => (prev === lowerLimit ? lowerLimit : prev - step)) 8 | return [ 9 | value, 10 | { 11 | inc, 12 | deinc, 13 | }, 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /useWindowWidth.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function useWindowWidth() { 4 | const [width, setWidth] = useState(window.innerWidth); 5 | 6 | useEffect(() => { 7 | const handleResize = () => setWidth(window.innerWidth); 8 | window.addEventListener('resize', handleResize); 9 | return () => { 10 | window.removeEventListener('resize', handleResize); 11 | }; 12 | }); 13 | 14 | return width; 15 | } 16 | 17 | export default useWindowWidth; 18 | -------------------------------------------------------------------------------- /useInterval.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export function useInterval(callback, delay) { 4 | const savedCallback = useRef() 5 | 6 | // Remember the latest function. 7 | useEffect(() => { 8 | savedCallback.current = callback 9 | }, [callback]) 10 | 11 | // Set up the interval. 12 | useEffect(() => { 13 | function tick() { 14 | savedCallback.current() 15 | } 16 | if (delay !== null) { 17 | let id = setInterval(tick, delay) 18 | return () => clearInterval(id) 19 | } 20 | }, [delay]) 21 | } 22 | -------------------------------------------------------------------------------- /useMeasure.tsx: -------------------------------------------------------------------------------- 1 | // import { useState, useRef, useEffect } from 'react'; 2 | // import ResizeObserver from 'resize-observer-polyfill'; 3 | 4 | // export default function useMeasure() { 5 | // const ref: any = useRef(); 6 | // const [bounds, set] = useState({ left: 0, top: 0, width: 0, height: 0 }); 7 | // const [ro] = useState( 8 | // () => new ResizeObserver(([entry]) => set(entry.contentRect)) 9 | // ); 10 | // useEffect(() => { 11 | // if (ref.current) ro.observe(ref.current); 12 | // return () => ro.disconnect(); 13 | // }, [ro]); 14 | // return [{ ref }, bounds]; 15 | // } 16 | -------------------------------------------------------------------------------- /useTimeout.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | function useTimeout(callback: () => void, delay: number) { 4 | const savedCallback: any = useRef(); 5 | 6 | // Remember the latest callback. 7 | useEffect(() => { 8 | savedCallback.current = callback; 9 | }, [callback]); 10 | 11 | // Set up the interval. 12 | useEffect(() => { 13 | function tick() { 14 | savedCallback.current(); 15 | } 16 | if (delay !== null) { 17 | const id = setTimeout(tick, delay); 18 | return () => clearTimeout(id); 19 | } 20 | }, [delay]); 21 | } 22 | 23 | export default useTimeout; 24 | -------------------------------------------------------------------------------- /useScrollFreeze.tsx: -------------------------------------------------------------------------------- 1 | // ? About This Hook ? 2 | // This hook adds overflow hidden to body to prevent scrolling 3 | 4 | import { useLayoutEffect } from 'react' 5 | 6 | export default function useScrollFreeze() { 7 | useLayoutEffect(() => { 8 | // Get original body overflow 9 | const originalStyle = window.getComputedStyle(document.body).overflow 10 | // Prevent scrolling on mount 11 | document.body.style.overflow = 'hidden' 12 | // Re-enable scrolling when component unmounts 13 | return () => { 14 | document.body.style.overflow = originalStyle 15 | } 16 | }, []) // Empty array ensures effect is only run on mount and unmount 17 | } 18 | -------------------------------------------------------------------------------- /useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | /* function used to close/toggle local state when clicking off of element with ref */ 4 | const useClickOutside = (ref: any, handler: any) => { 5 | useEffect(() => { 6 | const listener = (event: any) => { 7 | if (!ref.current || ref.current.contains(event.target)) { 8 | return 9 | } 10 | handler(event) 11 | } 12 | document.addEventListener('mousedown', listener) 13 | document.addEventListener('touchstart', listener) 14 | return () => { 15 | document.removeEventListener('mousedown', listener) 16 | document.removeEventListener('touchstart', listener) 17 | } 18 | }, [handler, ref]) 19 | } 20 | 21 | export default useClickOutside 22 | -------------------------------------------------------------------------------- /useWdyu.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | function useWhyDidYouUpdate(name: string, props: any) { 4 | // Get a mutable ref object where we can store props ... 5 | // ... for comparison next time this hook runs. 6 | const previousProps: any = useRef() 7 | 8 | useEffect(() => { 9 | if (previousProps.current) { 10 | // Get all keys from previous and current props 11 | const allKeys = Object.keys({ ...previousProps.current, ...props }) 12 | // Use this object to keep track of changed props 13 | const changesObj: any = {} 14 | // Iterate through keys 15 | allKeys.forEach(key => { 16 | // If previous is different from current 17 | if (previousProps.current[key] !== props[key]) { 18 | // Add to changesObj 19 | changesObj[key] = { 20 | from: previousProps.current[key], 21 | to: props[key], 22 | } 23 | } 24 | }) 25 | 26 | // If changesObj not empty then output to console 27 | if (Object.keys(changesObj).length) { 28 | console.log('[why-did-you-update]', name, changesObj) 29 | } 30 | } 31 | 32 | // Finally update previousProps with current props for next hook call 33 | previousProps.current = props 34 | }) 35 | } 36 | 37 | export default useWhyDidYouUpdate -------------------------------------------------------------------------------- /useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useLocalStorage(key: string, initialValue: any) { 4 | // State to store our value 5 | // Pass initial state function to useState so logic is only executed once 6 | const [storedValue, setStoredValue] = useState(() => { 7 | try { 8 | // Get from local storage by key 9 | const item = window.localStorage.getItem(key) 10 | // Parse stored json or if none return initialValue 11 | return item ? JSON.parse(item) : initialValue 12 | } catch (error) { 13 | // If error also return initialValue 14 | console.log(error) 15 | return initialValue 16 | } 17 | }) 18 | 19 | // Return a wrapped version of useState's setter function that ... 20 | // ... persists the new value to localStorage. 21 | const setValue = value => { 22 | try { 23 | // Allow value to be a function so we have same API as useState 24 | const valueToStore = 25 | value instanceof Function ? value(storedValue) : value 26 | // Save state 27 | setStoredValue(valueToStore) 28 | // Save to local storage 29 | window.localStorage.setItem(key, JSON.stringify(valueToStore)) 30 | } catch (error) { 31 | // A more advanced implementation would handle the error case 32 | console.log(error) 33 | } 34 | } 35 | 36 | return [storedValue, setValue] 37 | } 38 | -------------------------------------------------------------------------------- /useCoursePath.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import sortBy from 'lodash/sortBy' 3 | import { roundPathCorners } from 'utilities' 4 | 5 | type Dimensions = { 6 | top: number 7 | left: number 8 | width: number 9 | height: number 10 | position: number 11 | id: string 12 | } 13 | 14 | /* used to draw `a path from SeriesCards in CoursePaths */ 15 | const useCoursePath = (dimensions: Dimensions[] = []) => { 16 | const [path, setPath] = useState('') 17 | 18 | useEffect(() => { 19 | const points = Object.entries(dimensions).map(([key, val]) => { 20 | val.id = key 21 | return val 22 | }) 23 | const sortedPoints = sortBy(points, point => point.position) 24 | let tempPath = '' 25 | sortedPoints.forEach((point, index) => { 26 | const xCenter = Math.floor(point.left + point.width / 2) 27 | const yCenter = Math.floor(point.top + point.height / 2) 28 | const xCenterMid = sortedPoints[index + 1] 29 | ? Math.floor(sortedPoints[index + 1].left + point.width / 2) 30 | : '' 31 | const yCenterMid = sortedPoints[index + 1] 32 | ? `L${xCenterMid} ${yCenter}` 33 | : '' 34 | if (index > 0) { 35 | tempPath = `${tempPath} L${xCenter} ${yCenter} ${yCenterMid}` 36 | } else { 37 | tempPath = `M ${xCenter} ${yCenter} ${yCenterMid}` 38 | } 39 | }) 40 | setPath(roundPathCorners(tempPath, 15, false)) 41 | return () => {} 42 | }, [dimensions, path]) 43 | return { 44 | path, 45 | } 46 | } 47 | 48 | export default useCoursePath 49 | -------------------------------------------------------------------------------- /useScript.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | // Hook 4 | let cachedScripts = [] 5 | export function useScript(src) { 6 | // Keeping track of script loaded and error state 7 | const [state, setState] = useState({ 8 | loaded: false, 9 | error: false, 10 | }) 11 | 12 | useEffect( 13 | () => { 14 | // If cachedScripts array already includes src that means another instance ... 15 | // ... of this hook already loaded this script, so no need to load again. 16 | if (cachedScripts.includes(src)) { 17 | setState({ 18 | loaded: true, 19 | error: false, 20 | }) 21 | } else { 22 | cachedScripts.push(src) 23 | 24 | // Create script 25 | let script = document.createElement('script') 26 | script.src = src 27 | script.async = true 28 | 29 | // Script event listener callbacks for load and error 30 | const onScriptLoad = () => { 31 | setState({ 32 | loaded: true, 33 | error: false, 34 | }) 35 | } 36 | 37 | const onScriptError = () => { 38 | // Remove from cachedScripts we can try loading again 39 | const index = cachedScripts.indexOf(src) 40 | if (index >= 0) cachedScripts.splice(index, 1) 41 | script.remove() 42 | 43 | setState({ 44 | loaded: true, 45 | error: true, 46 | }) 47 | } 48 | 49 | script.addEventListener('load', onScriptLoad) 50 | script.addEventListener('error', onScriptError) 51 | 52 | // Add script to document body 53 | document.body.appendChild(script) 54 | 55 | // Remove event listeners on cleanup 56 | return () => { 57 | script.removeEventListener('load', onScriptLoad) 58 | script.removeEventListener('error', onScriptError) 59 | } 60 | } 61 | }, 62 | [src] // Only re-run effect if script src changes 63 | ) 64 | 65 | return [state.loaded, state.error] 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/VisualStudioCode,macOS,Windows,Node 3 | # Edit at https://www.gitignore.io/?templates=VisualStudioCode,macOS,Windows,Node 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Node ### 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | lerna-debug.log* 41 | 42 | # Diagnostic reports (https://nodejs.org/api/report.html) 43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # Directory for instrumented libs generated by jscoverage/JSCover 52 | lib-cov 53 | 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (https://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # TypeScript v1 declaration files 78 | typings/ 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Optional REPL history 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | *.tgz 94 | 95 | # Yarn Integrity file 96 | .yarn-integrity 97 | 98 | # dotenv environment variables file 99 | .env 100 | .env.test 101 | 102 | # parcel-bundler cache (https://parceljs.org/) 103 | .cache 104 | 105 | # next.js build output 106 | .next 107 | 108 | # nuxt.js build output 109 | .nuxt 110 | 111 | # rollup.js default build output 112 | dist/ 113 | 114 | # Uncomment the public line if your project uses Gatsby 115 | # https://nextjs.org/blog/next-9-1#public-directory-support 116 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 117 | # public 118 | 119 | # Storybook build outputs 120 | .out 121 | .storybook-out 122 | 123 | # vuepress build output 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # Temporary folders 136 | tmp/ 137 | temp/ 138 | 139 | ### VisualStudioCode ### 140 | .vscode/* 141 | !.vscode/settings.json 142 | !.vscode/tasks.json 143 | !.vscode/launch.json 144 | !.vscode/extensions.json 145 | 146 | ### VisualStudioCode Patch ### 147 | # Ignore all local history of files 148 | .history 149 | 150 | ### Windows ### 151 | # Windows thumbnail cache files 152 | Thumbs.db 153 | Thumbs.db:encryptable 154 | ehthumbs.db 155 | ehthumbs_vista.db 156 | 157 | # Dump file 158 | *.stackdump 159 | 160 | # Folder config file 161 | [Dd]esktop.ini 162 | 163 | # Recycle Bin used on file shares 164 | $RECYCLE.BIN/ 165 | 166 | # Windows Installer files 167 | *.cab 168 | *.msi 169 | *.msix 170 | *.msm 171 | *.msp 172 | 173 | # Windows shortcuts 174 | *.lnk 175 | 176 | # End of https://www.gitignore.io/api/VisualStudioCode,macOS,Windows,Node 177 | -------------------------------------------------------------------------------- /useDimensions.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useLayoutEffect } from 'react' 2 | export interface DimensionObject { 3 | width: number 4 | height: number 5 | top: number 6 | left: number 7 | x: number 8 | y: number 9 | right: number 10 | bottom: number 11 | } 12 | 13 | export type UseDimensionsHook = [ 14 | (node: HTMLElement) => void, 15 | {} | DimensionObject, 16 | HTMLElement 17 | ] 18 | 19 | export interface UseDimensionsArgs { 20 | liveMeasure?: boolean 21 | boundsType?: BoundsType 22 | } 23 | 24 | export const enum BoundsType { 25 | client = 'CLIENT', 26 | offset = 'OFFSET', 27 | scroll = 'SCROLL', 28 | bbox = 'BBOX', 29 | } 30 | // const client = 'CLIENT' 31 | // const offset = 'OFFSET' 32 | // const scroll = 'SCROLL' 33 | // const bbox = 'BBOX' 34 | // export const BoundsType = { 35 | // client, 36 | // offset, 37 | // scroll, 38 | // bbox, 39 | // } 40 | 41 | function getDimensionObject( 42 | node: Element, 43 | boundsType: BoundsType 44 | ): DimensionObject { 45 | let rect 46 | switch (boundsType) { 47 | case BoundsType.bbox: 48 | // getBBox() only exists on SVGGraphicsElements 49 | if (!(node instanceof SVGGraphicsElement)) throw new Error() 50 | rect = node.getBBox() 51 | return { 52 | width: rect.width, 53 | height: rect.height, 54 | left: rect.x, 55 | top: rect.y, 56 | x: rect.x, 57 | y: rect.y, 58 | right: rect.right, 59 | bottom: rect.bottom, 60 | } 61 | case BoundsType.client: 62 | rect = node.getBoundingClientRect() 63 | return { 64 | width: rect.width, 65 | height: rect.height, 66 | top: 'x' in rect ? rect.x : rect.top, 67 | left: 'y' in rect ? rect.y : rect.left, 68 | x: 'x' in rect ? rect.x : rect.left, 69 | y: 'y' in rect ? rect.y : rect.top, 70 | right: rect.right, 71 | bottom: rect.bottom, 72 | } 73 | case BoundsType.offset: 74 | // The offset* properties only exist on HTMLElements 75 | if (!(node instanceof HTMLElement)) throw new Error() 76 | return { 77 | width: node.offsetWidth, 78 | height: node.offsetHeight, 79 | top: node.offsetTop, 80 | left: node.offsetLeft, 81 | x: node.offsetLeft, 82 | y: node.offsetTop, 83 | right: null, 84 | bottom: null, 85 | } 86 | case BoundsType.scroll: 87 | return { 88 | width: node.scrollWidth, 89 | height: node.scrollHeight, 90 | top: node.scrollTop, 91 | left: node.scrollLeft, 92 | x: node.scrollLeft, 93 | y: node.scrollTop, 94 | right: null, 95 | bottom: null, 96 | } 97 | } 98 | } 99 | 100 | function useDimensions({ 101 | liveMeasure = true, 102 | boundsType = BoundsType.client, 103 | }: UseDimensionsArgs = {}): UseDimensionsHook { 104 | const [dimensions, setDimensions] = useState({}) 105 | const [node, setNode] = useState(null) 106 | 107 | const ref = useCallback(node => { 108 | setNode(node) 109 | }, []) 110 | 111 | useLayoutEffect(() => { 112 | if (node) { 113 | const measure = () => 114 | window.requestAnimationFrame(() => 115 | setDimensions(getDimensionObject(node, boundsType)) 116 | ) 117 | measure() 118 | 119 | if (liveMeasure) { 120 | window.addEventListener('resize', measure) 121 | window.addEventListener('scroll', measure) 122 | 123 | return () => { 124 | window.removeEventListener('resize', measure) 125 | window.removeEventListener('scroll', measure) 126 | } 127 | } 128 | } 129 | }, [node]) 130 | 131 | return [ref, dimensions, node] 132 | } 133 | 134 | export default useDimensions 135 | --------------------------------------------------------------------------------