Skip to content

Commit 5f3a3d2

Browse files
authored
Merge pull request #92 from bozheville/add-ts-hooks-tier-1
Add ts hooks tier 1
2 parents 083c8d9 + bf71ada commit 5f3a3d2

File tree

3 files changed

+3
-0
lines changed

3 files changed

+3
-0
lines changed

src/pages/useDarkMode.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ links:
1010
name: donavon/use-dark-mode
1111
description: A more configurable implementation of this hook that syncs changes across browser tabs and handles SSR. Provided much of the code and inspiration for this post.
1212
code: "\/\/ Usage\r\nfunction App() {\r\n const [darkMode, setDarkMode] = useDarkMode();\r\n\r\n return (\r\n <div>\r\n <div className=\"navbar\">\r\n <Toggle darkMode={darkMode} setDarkMode={setDarkMode} \/>\r\n <\/div>\r\n <Content \/>\r\n <\/div>\r\n );\r\n}\r\n\r\n\/\/ Hook\r\nfunction useDarkMode() {\r\n \/\/ Use our useLocalStorage hook to persist state through a page refresh.\r\n \/\/ Read the recipe for this hook to learn more: usehooks.com\/useLocalStorage\r\n const [enabledState, setEnabledState] = useLocalStorage('dark-mode-enabled');\r\n\r\n \/\/ See if user has set a browser or OS preference for dark mode.\r\n \/\/ The usePrefersDarkMode hook composes a useMedia hook (see code below).\r\n const prefersDarkMode = usePrefersDarkMode();\r\n\r\n \/\/ If enabledState is defined use it, otherwise fallback to prefersDarkMode.\r\n \/\/ This allows user to override OS level setting on our website.\r\n const enabled =\r\n typeof enabledState !== 'undefined' ? enabledState : prefersDarkMode;\r\n\r\n \/\/ Fire off effect that add\/removes dark mode class\r\n useEffect(\r\n () => {\r\n const className = 'dark-mode';\r\n const element = window.document.body;\r\n if (enabled) {\r\n element.classList.add(className);\r\n } else {\r\n element.classList.remove(className);\r\n }\r\n },\r\n [enabled] \/\/ Only re-call effect when value changes\r\n );\r\n\r\n \/\/ Return enabled state and setter\r\n return [enabled, setEnabledState];\r\n}\r\n\r\n\/\/ Compose our useMedia hook to detect dark mode preference.\r\n\/\/ The API for useMedia looks a bit weird, but that's because ...\r\n\/\/ ... it was designed to support multiple media queries and return values.\r\n\/\/ Thanks to hook composition we can hide away that extra complexity!\r\n\/\/ Read the recipe for useMedia to learn more: usehooks.com\/useMedia\r\nfunction usePrefersDarkMode() {\r\n return useMedia(['(prefers-color-scheme: dark)'], [true], false);\r\n}"
13+
tsCode: "// Usage\r\nfunction App() {\r\n const [darkMode, setDarkMode] = useDarkMode();\r\n\r\n return (\r\n <div>\r\n <div className=\"navbar\">\r\n <Toggle darkMode={darkMode} setDarkMode={setDarkMode} />\r\n </div>\r\n <Content />\r\n </div>\r\n );\r\n}\r\n\r\n// Hook\r\nfunction useDarkMode() {\r\n // Use our useLocalStorage hook to persist state through a page refresh.\r\n // Read the recipe for this hook to learn more: usehooks.com/useLocalStorage\r\n const [enabledState, setEnabledState] = useLocalStorage<boolean>('dark-mode-enabled', false);\r\n\r\n // See if user has set a browser or OS preference for dark mode.\r\n // The usePrefersDarkMode hook composes a useMedia hook (see code below).\r\n const prefersDarkMode = usePrefersDarkMode();\r\n\r\n // If enabledState is defined use it, otherwise fallback to prefersDarkMode.\r\n // This allows user to override OS level setting on our website.\r\n const enabled = enabledState ?? prefersDarkMode;\r\n\r\n // Fire off effect that add/removes dark mode class\r\n useEffect(\r\n () => {\r\n const className = 'dark-mode';\r\n const element = window.document.body;\r\n if (enabled) {\r\n element.classList.add(className);\r\n } else {\r\n element.classList.remove(className);\r\n }\r\n },\r\n [enabled] // Only re-call effect when value changes\r\n );\r\n\r\n // Return enabled state and setter\r\n return [enabled, setEnabledState];\r\n}\r\n\r\n// Compose our useMedia hook to detect dark mode preference.\r\n// The API for useMedia looks a bit weird, but that's because ...\r\n// ... it was designed to support multiple media queries and return values.\r\n// Thanks to hook composition we can hide away that extra complexity!\r\n// Read the recipe for useMedia to learn more: usehooks.com/useMedia\r\nfunction usePrefersDarkMode() {\r\n return useMedia<boolean>(['(prefers-color-scheme: dark)'], [true], false);\r\n}"
1314
---
1415

1516
This hook handles all the stateful logic required to add a <b>☾ dark mode</b> toggle to your website. It utilizes localStorage to remember the user's chosen mode, defaults to their browser or OS level setting using the `prefers-color-scheme` media query and manages the setting of a `.dark-mode` className on `body` to apply your styles.

src/pages/useLocalStorage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ links:
99
name: use-persisted-state
1010
description: A more advanced implementation that syncs between tabs and browser windows.
1111
code: "import { useState } from 'react';\r\n\r\n\/\/ Usage\r\nfunction App() {\r\n \/\/ Similar to useState but first arg is key to the value in local storage.\r\n const [name, setName] = useLocalStorage('name', 'Bob');\r\n\r\n return (\r\n <div>\r\n <input\r\n type=\"text\"\r\n placeholder=\"Enter your name\"\r\n value={name}\r\n onChange={e => setName(e.target.value)}\r\n \/>\r\n <\/div>\r\n );\r\n}\r\n\r\n\/\/ Hook\r\nfunction useLocalStorage(key, initialValue) {\r\n \/\/ State to store our value\r\n \/\/ Pass initial state function to useState so logic is only executed once\r\n const [storedValue, setStoredValue] = useState(() => {\r\n try {\r\n \/\/ Get from local storage by key\r\n const item = window.localStorage.getItem(key);\r\n \/\/ Parse stored json or if none return initialValue\r\n return item ? JSON.parse(item) : initialValue;\r\n } catch (error) {\r\n \/\/ If error also return initialValue\r\n console.log(error);\r\n return initialValue;\r\n }\r\n });\r\n\r\n \/\/ Return a wrapped version of useState's setter function that ...\r\n \/\/ ... persists the new value to localStorage.\r\n const setValue = value => {\r\n try {\r\n \/\/ Allow value to be a function so we have same API as useState\r\n const valueToStore =\r\n value instanceof Function ? value(storedValue) : value;\r\n \/\/ Save state\r\n setStoredValue(valueToStore);\r\n \/\/ Save to local storage\r\n window.localStorage.setItem(key, JSON.stringify(valueToStore));\r\n } catch (error) {\r\n \/\/ A more advanced implementation would handle the error case\r\n console.log(error);\r\n }\r\n };\r\n\r\n return [storedValue, setValue];\r\n}"
12+
tsCode: "import { useState } from 'react';\r\n\r\n// Usage\r\nfunction App() {\r\n // Similar to useState but first arg is key to the value in local storage.\r\n const [name, setName] = useLocalStorage<string>('name', 'Bob');\r\n\r\n return (\r\n <div>\r\n <input\r\n type=\"text\"\r\n placeholder=\"Enter your name\"\r\n value={name}\r\n onChange={e => setName(e.target.value)}\r\n />\r\n </div>\r\n );\r\n}\r\n\r\n// Hook\r\nfunction useLocalStorage<T>(key: string, initialValue: T) {\r\n // State to store our value\r\n // Pass initial state function to useState so logic is only executed once\r\n const [storedValue, setStoredValue] = useState<T>(() => {\r\n try {\r\n // Get from local storage by key\r\n const item = window.localStorage.getItem(key);\r\n // Parse stored json or if none return initialValue\r\n return item ? JSON.parse(item) : initialValue;\r\n } catch (error) {\r\n // If error also return initialValue\r\n console.log(error);\r\n return initialValue;\r\n }\r\n });\r\n\r\n // Return a wrapped version of useState's setter function that ...\r\n // ... persists the new value to localStorage.\r\n const setValue = (value: T | ((val: T) => T)) => {\r\n try {\r\n // Allow value to be a function so we have same API as useState\r\n const valueToStore =\r\n value instanceof Function ? value(storedValue) : value;\r\n // Save state\r\n setStoredValue(valueToStore);\r\n // Save to local storage\r\n window.localStorage.setItem(key, JSON.stringify(valueToStore));\r\n } catch (error) {\r\n // A more advanced implementation would handle the error case\r\n console.log(error);\r\n }\r\n };\r\n\r\n return [storedValue, setValue];\r\n}"
1213
---
1314

1415
Sync state to local storage so that it persists through a page refresh.

src/pages/useMedia.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ links:
1212
name: Masonry Grid
1313
description: Original source of our useMedia v1 code. This demo uses react-spring to animate when images change columns.
1414
code: "import { useState, useEffect } from 'react';\r\n\r\nfunction App() {\r\n const columnCount = useMedia(\r\n \/\/ Media queries\r\n ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],\r\n \/\/ Column counts (relates to above media queries by array index)\r\n [5, 4, 3],\r\n \/\/ Default column count\r\n 2\r\n );\r\n\r\n \/\/ Create array of column heights (start at 0)\r\n let columnHeights = new Array(columnCount).fill(0);\r\n\r\n \/\/ Create array of arrays that will hold each column's items\r\n let columns = new Array(columnCount).fill().map(() => []);\r\n\r\n data.forEach(item => {\r\n \/\/ Get index of shortest column\r\n const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));\r\n \/\/ Add item\r\n columns[shortColumnIndex].push(item);\r\n \/\/ Update height\r\n columnHeights[shortColumnIndex] += item.height;\r\n });\r\n\r\n \/\/ Render columns and items\r\n return (\r\n <div className=\"App\">\r\n <div className=\"columns is-mobile\">\r\n {columns.map(column => (\r\n <div className=\"column\">\r\n {column.map(item => (\r\n <div\r\n className=\"image-container\"\r\n style={{\r\n \/\/ Size image container to aspect ratio of image\r\n paddingTop: (item.height \/ item.width) * 100 + '%'\r\n }}\r\n >\r\n <img src={item.image} alt=\"\" \/>\r\n <\/div>\r\n ))}\r\n <\/div>\r\n ))}\r\n <\/div>\r\n <\/div>\r\n );\r\n}\r\n\r\n\/\/ Hook\r\nfunction useMedia(queries, values, defaultValue) {\r\n \/\/ Array containing a media query list for each query\r\n const mediaQueryLists = queries.map(q => window.matchMedia(q));\r\n\r\n \/\/ Function that gets value based on matching media query\r\n const getValue = () => {\r\n \/\/ Get index of first media query that matches\r\n const index = mediaQueryLists.findIndex(mql => mql.matches);\r\n \/\/ Return related value or defaultValue if none\r\n return typeof values[index] !== 'undefined' ? values[index] : defaultValue;\r\n };\r\n\r\n \/\/ State and setter for matched value\r\n const [value, setValue] = useState(getValue);\r\n\r\n useEffect(\r\n () => {\r\n \/\/ Event listener callback\r\n \/\/ Note: By defining getValue outside of useEffect we ensure that it has ...\r\n \/\/ ... current values of hook args (as this hook callback is created once on mount).\r\n const handler = () => setValue(getValue);\r\n \/\/ Set a listener for each media query with above handler as callback.\r\n mediaQueryLists.forEach(mql => mql.addListener(handler));\r\n \/\/ Remove listeners on cleanup\r\n return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));\r\n },\r\n [] \/\/ Empty array ensures effect is only run on mount and unmount\r\n );\r\n\r\n return value;\r\n}"
15+
tsCode: "import { useState, useEffect } from 'react';\r\n\r\nfunction App() {\r\n const columnCount = useMedia<number>(\r\n // Media queries\r\n ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],\r\n // Column counts (relates to above media queries by array index)\r\n [5, 4, 3],\r\n // Default column count\r\n 2\r\n );\r\n\r\n // Create array of column heights (start at 0)\r\n let columnHeights = new Array(columnCount).fill(0);\r\n\r\n // Create array of arrays that will hold each column's items\r\n let columns = new Array(columnCount).fill().map(() => []) as Array<DataProps[]>;\r\n\r\n (data as DataProps[]).forEach(item => {\r\n // Get index of shortest column\r\n const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));\r\n // Add item\r\n columns[shortColumnIndex].push(item);\r\n // Update height\r\n columnHeights[shortColumnIndex] += item.height;\r\n });\r\n\r\n // Render columns and items\r\n return (\r\n <div className=\"App\">\r\n <div className=\"columns is-mobile\">\r\n {columns.map(column => (\r\n <div className=\"column\">\r\n {column.map(item => (\r\n <div\r\n className=\"image-container\"\r\n style={{\r\n // Size image container to aspect ratio of image\r\n paddingTop: (item.height / item.width) * 100 + '%'\r\n }}\r\n >\r\n <img src={item.image} alt=\"\" />\r\n </div>\r\n ))}\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\n// Hook\r\nconst useMedia = <T>(queries: string[], values: T[], defaultValue: T) => {\r\n // Array containing a media query list for each query\r\n const mediaQueryLists = queries.map(q => window.matchMedia(q));\r\n\r\n // Function that gets value based on matching media query\r\n const getValue = () => {\r\n // Get index of first media query that matches\r\n const index = mediaQueryLists.findIndex(mql => mql.matches);\r\n // Return related value or defaultValue if none\r\n return values?.[index] || defaultValue;\r\n };\r\n\r\n // State and setter for matched value\r\n const [value, setValue] = useState<T>(getValue);\r\n\r\n useEffect(\r\n () => {\r\n // Event listener callback\r\n // Note: By defining getValue outside of useEffect we ensure that it has ...\r\n // ... current values of hook args (as this hook callback is created once on mount).\r\n const handler = () => setValue(getValue);\r\n // Set a listener for each media query with above handler as callback.\r\n mediaQueryLists.forEach(mql => mql.addListener(handler));\r\n // Remove listeners on cleanup\r\n return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));\r\n },\r\n [] // Empty array ensures effect is only run on mount and unmount\r\n );\r\n\r\n return value;\r\n}"
1516
---
1617

1718
This hook makes it super easy to utilize media queries in your component logic. In our example below we render a different number of columns depending on which media query matches the current screen width, and then distribute images amongst the columns in a way that limits column height difference (we don't want one column way longer than the rest).

0 commit comments

Comments
 (0)