Last active
December 3, 2023 01:10
-
-
Save arnoldc/de8360e84f6427083ec3d65d845c570c to your computer and use it in GitHub Desktop.
[React] Hooks
This file contains hidden or 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
// ------------ useState => dealing with multiple object in state | |
const [person ,setPerson] = useState(() => initialPerson); | |
setPerson((person) => ({ | |
...person, | |
firstName: e.target.value | |
})); | |
// ------------ customHook part 1 | |
export function usePerson(initialPerson: Person) { | |
const [person, setPerson] = useState<Person|null>(null) | |
useEffect(() => { | |
const getPerson = async () => { | |
const person = await localStorage.getItem<Person>("person") | |
setPerson(person ?? initialPerson); | |
} | |
getPerson(); | |
}, [initialPerson]); | |
useEffect(() => { | |
savePerson(person) | |
}, [person]); | |
return [person, setPerson] as const; | |
} | |
function PersonEditor() { | |
const [person, setPerson] = usePerson(initialPerson); | |
if(!person) { | |
return <Loading /> | |
} | |
} | |
// ------------ useRef - part 1 | |
function PersonEditor() { | |
const input = useRef<HTMLInputElement>(null) | |
useEffect(() => { | |
setTimeout(() => { | |
input.current?.focus(); | |
}, 1000); | |
}, []) | |
return ( | |
// make sure forwardRef is used | |
<LabeledInput | |
ref={input} | |
label="First Name" | |
/> | |
) | |
} | |
export const LabeledInput = forwardRef<HTMLInputElement, LabeledInputProps>(({ label, ...props }, ref) => { | |
return ( | |
...... | |
); | |
} | |
// ------------ useRef - part 2 | |
const addButtonRef = useRef<HTMLButtonElement>(null); | |
useEffect(() => { | |
if (!loading) { | |
addButtonRef.current?.focus(); | |
} | |
}, [loading]); | |
<button ref={addButtonRef} onClick={() => dispatch({ type: 'increment' })}> | |
Add | |
</button> | |
// ------------ useLayoutEffect | |
// - after the render but before the component is painted | |
useEffect(() => { | |
setTimeout(() => { | |
button.current?.focus() | |
}, 1000) | |
}) | |
useLayoutEffect(() => { | |
if(button.current) { | |
button.current.style.backgroundColor = "green" | |
} | |
}, []) | |
// ------------ custom hook part 2 | |
export function useDebounce(fn: () => void, timeout: number): void { | |
useEffect(() => { | |
const handle = setTimeout(fn, timeout); | |
return () => clearTimeout(handle); | |
}, [fn, timeout]) | |
} | |
// usage | |
useDebounce((): void => { | |
savePerson(); | |
}, 1000); | |
// ------------ HOC | |
// In the React community, it is very common to have the prefix with for HOCs. | |
//e.g | |
const withClassName = Component => props => ( | |
<Component {...props} className="my-class" /> | |
) | |
// usage | |
const withInnerWidth = Component => props => { | |
const [innerWidth, setInnerWidth] = useState(window.innerWidth) | |
const handleResize = () => { | |
setInnerWidth(window.innerWidth) | |
} | |
return <Component {...props} /> | |
} | |
const MyComponentWithInnerWidth = withInnerWidth(MyComponent) | |
// ------------ FunctionAsChild | |
// example 1 | |
<FunctionAsChild> | |
{() => <div>Hello, World!</div>} | |
</FunctionAsChild> | |
// example 2 | |
const Name = ({ children }) => children('World') | |
<Name> | |
{name => <div>Hello, {name}!</div>} | |
</Name> | |
// ------------ useMemo and memo part 1 | |
/* | |
- Memorizes a calculated value | |
- the component will render just once and will memorize. | |
- For computed properties | |
- more on heavy computation | |
- useCallback it needs a dependency array, while this one doesnt | |
- memo can be use for components and functions | |
memo is a function that you can wrap your component in to define a memoized version of it. | |
This will guarantee that your component doesn’t re-render unless its props have changed. | |
*/ | |
// before | |
const filteredTodoList = todoList.filter((todo: Todo) => { | |
console.log('Filtering...') | |
return todo.task.toLowerCase().includes(term.toLowerCase()) | |
}) | |
// after | |
const filteredTodoList = useMemo(() => todoList.filter((todo: Todo) => { | |
console.log('Filtering...') | |
return todo.task.toLowerCase().includes(term.toLowerCase()) | |
}), []) | |
// ------------ useCallback | |
// EXAMPLE 1 | |
/* | |
When to use useCallback? | |
- Memorizes a function definition to avoid redefining it on each render. | |
- Use it whenever a function is passed as an effect argument. | |
- Use it whenever a function is passed by props to a memorized component. | |
- use this when function is used as props to prevent unnecessary re-renders. | |
*/ | |
// before | |
const printTodoList = () => { | |
console.log('Changing todoList') | |
} | |
useEffect(() => { | |
printTodoList() | |
}, [todoList]) | |
/* | |
this causes | |
- The 'printTodoList' function makes the dependencies of useEffect Hook (at line 27) | |
change on every render. Move it inside the useEffect callback. | |
Alternatively, wrap the definition of 'printTodoList' in its own useCallback() Hook react-hooks/exhaustive-deps | |
*/ | |
// after | |
const printTodoList = useCallback(() => { | |
console.log('Changing todoList', todoList) | |
}, [todoList]) | |
useEffect(() => { | |
printTodoList() | |
}, [todoList]) | |
// before | |
const handleDelete = (taskId: number) => { | |
const newTodoList = todoList.filter((todo: Todo) => todo.id !== taskId) | |
setTodoList(newTodoList) | |
} | |
// after | |
const handleDelete = useCallback((taskId: number) => { | |
const newTodoList = todoList.filter((todo: Todo) => todo.id !== taskId) | |
setTodoList(newTodoList) | |
}, [todoList]) | |
// EXAMPLE 2 | |
const isCheckingUser = useCallback(() => { | |
if (!isAvailable) return false | |
return isTryingToCheck | |
}, [isAvailable, isTryingToCheck]) | |
// EXAMPLE 3 | |
const isChecked = useCallback(() => { | |
return status === 'registered' | |
}, [status]) | |
// ------------ custom hook part 3 | |
import { useEffect } from 'react'; | |
function useKeyEvent() { | |
useEffect(() => { | |
function keyPressedHandler(event) { | |
const pressedKey = event.key; | |
if (!['s', 'c', 'p'].includes(pressedKey)) { | |
alert('Invalid key!'); | |
return; | |
} | |
setPressedKey(pressedKey); | |
} | |
window.addEventListener('keydown', keyPressedHandler); | |
return () => window.removeEventListener('keydown', keyPressedHandler); | |
}, []); | |
} | |
export default useKeyEvent; | |
// usage | |
import useKeyEvent from './hooks/use-key-event'; | |
function App() { | |
const pressedKey = useKeyEvent(['s', 'c', 'p']); | |
let output = ''; | |
if (pressedKey === 's') { | |
output = '7'; | |
} else if (pressedKey === 'c') { | |
output = '8'; | |
} else if (pressedKey === 'p') { | |
output = '9'; | |
} | |
return ( | |
<main> | |
<h1>Press a key!</h1> | |
<p> | |
Supported keys: <kbd>s</kbd>, <kbd>c</kbd>, <kbd>p</kbd> | |
</p> | |
<p id="output">{output}</p> | |
</main> | |
); | |
} | |
export default App; | |
// ------------ useMemo part 2 | |
// example 1 | |
function App() { | |
const reverseWord = word => { | |
return word.split('').reverse().join(''); | |
} | |
const title = "Hello World"; | |
// cache the title value to not retun this function on every render | |
// essentially don't call function again if title isn't changing its value | |
const reversedTitle = useMemo(() => reverseWord(title), [title]) | |
return ( | |
<h1>{reversedTitle}</h1> | |
) | |
} | |
// example 2 | |
//function | |
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); | |
//component | |
const memoizedValue = useMemo(()=><ExpensiveValue a={a} b={b}/>, [a, b]); | |
// ------------ useTransition | |
// only available in reacf 18 | |
import { useState, useTransition } from 'react'; | |
export function FilterList({ names }) { | |
const [query, setQuery] = useState(''); | |
const [highlight, setHighlight] = useState(''); | |
const [isPending, startTransition] = useTransition(); | |
const changeHandler = ({ target: { value } }) => { | |
setQuery(value); | |
startTransition(() => setHighlight(value)); | |
}; | |
return ( | |
<div> | |
<input onChange={changeHandler} value={query} type="text" /> | |
{names.map((name, i) => ( | |
<ListItem key={i} name={name} highlight={highlight} /> | |
))} | |
</div> | |
); | |
} | |
// demo time: https://codesandbox.io/s/heavy-update-as-non-urgent-ifobc?file=/src/FilterList.js | |
// example 2 | |
function App() { | |
const [isPending, startTransition] = useTransition(); | |
const [filterTerm, setFilterTerm] = useState(''); | |
const filteredProducts = filterProducts(filterTerm); | |
function updateFilterHandler(event) { | |
startTransition(() => { | |
setFilterTerm(event.target.value); | |
}); | |
} | |
return ( | |
<div id="app"> | |
<input type="text" onChange={updateFilterHandler} /> | |
{isPending && <p>Updating List...</p>} | |
<ProductList products={filteredProducts} /> | |
</div> | |
); | |
} | |
// example 3 | |
let unfilteredItems = new Array(25000) | |
.fill(null) | |
.map((v, i) => ({ id: i, name: `Item ${i}` })); | |
function filterItems(filter) { | |
return new Promise(resolve => { | |
setTimeout(() => { | |
resolve(unfilteredItems.filter(item => item.name.includes(filter))); | |
}, 1000); | |
}); | |
} | |
export default function AsyncUpdates() { | |
let [loading, setLoading] = React.useState(false); | |
let [filter, setFilter] = React.useState(""); | |
let [items, setItems] = React.useState([]); | |
async function onChange(e) { | |
setLoading(true); | |
setFilter(e.target.value); | |
React.startTransition(async () => { | |
setItems(e.target.value === "" ? [] : await filterItems(e.target.value)); | |
setLoading(false); | |
}); | |
} | |
return ( | |
<div> | |
<div> | |
<input | |
type="text" | |
placeholder="Filter" | |
value={filter} | |
onChange={onChange} | |
/> | |
</div> | |
<div> | |
{loading && <em>loading...</em>} | |
<ul> | |
{items.map(item => ( | |
<li key={item.id}>{item.name}</li> | |
))} | |
</ul> | |
</div> | |
</div> | |
); | |
} | |
// https://github.dev/PacktPublishing/React-and-React-Native-4th-Edition/tree/main/Chapter13 | |
// ------------ useReducer | |
/* | |
- can be use for complex state logic | |
*/ | |
// example 1 | |
function reducer(state: State, action: Action): State { | |
switch (action.type) { | |
case 'initialize': | |
return { name: action.name, score: 0, loading: false }; | |
case 'increment': | |
return { ...state, score: state.score + 1 }; | |
case 'decrement': | |
return { ...state, score: state.score - 1 }; | |
case 'reset': | |
return { ...state, score: 0 }; | |
default: | |
return state; | |
} | |
} | |
type State = { | |
name: string | undefined; | |
score: number; | |
loading: boolean; | |
}; | |
type Action = | |
| { | |
type: 'initialize'; | |
name: string; | |
} | |
| { | |
type: 'increment'; | |
} | |
| { | |
type: 'decrement'; | |
} | |
| { | |
type: 'reset'; | |
}; | |
export function PersonScore() { | |
const [{ name, score, loading }, dispatch] = useReducer(reducer, { | |
name: undefined, | |
score: 0, | |
loading: true, | |
}); | |
useEffect(() => { | |
getPerson().then(({ name }) => dispatch({ type: 'initialize', name })); | |
}, []); | |
const handleReset = useCallback(() => dispatch({ type: 'reset' }), []); | |
return ( | |
<div /> | |
) | |
} | |
// another example useReducer and also same example of decrement and increment | |
const initialState = { count: 0 }; | |
const types = { | |
INCREMENT: "increment", | |
DECREMENT: "decrement", | |
RESET: "reset", | |
}; | |
const reducer = (state, action) => { | |
switch (action) { | |
case types.INCREMENT: | |
return { count: state.count + 1 }; | |
case types.DECREMENT: | |
return { count: state.count - 1 }; | |
case types.RESET: | |
return { count: 0 }; | |
default: | |
throw new Error("This type does not exist"); | |
} | |
}; | |
const AppWithReducer = () => { | |
const [state, dispatch] = useReducer(reducer, | |
initialState); | |
const increment = () => dispatch(types.INCREMENT); | |
const decrement = () => dispatch(types.DECREMENT); | |
const reset = () => dispatch(types.RESET); | |
return ( | |
<div className="App"> | |
<div>Counter: {state.count}</div> | |
<div> | |
<button onClick={increment}>+1</button> | |
<button onClick={decrement}>-1</button> | |
<button onClick={reset}>Reset</button> | |
</div> | |
</div> | |
); | |
}; | |
// ------------ useId | |
// example 1 | |
/* | |
NOTE: | |
- not use for keys in a list | |
*/ | |
export function LabelInput({ label, value, ...rest }: Props) { | |
const id = useId(); | |
return ( | |
<div className="mb-3"> | |
<label htmlFor={id} className="form-label">{label}</label> | |
<input id={id} className="form-control" value={value} {...rest} /> | |
</div> | |
); | |
} | |
// ------------ useTransition | |
// example 1 | |
/* | |
NOTE: | |
1. starTransition | |
- this can be used anywhere | |
- this is not a hook | |
- no additional renders | |
2. useTransition | |
- needs to be used in functional component | |
- const [isPending, startTransition] = useTransition(); | |
- ^ there is a pending state | |
*/ | |
import { startTransition, useState, useTransition } from 'react'; | |
import { CheckNumber } from './CheckNumber'; | |
import { PrimeRange } from './PrimeRange'; | |
const defaultValue = 250; | |
export function PrimeNumbers() { | |
const [isPending, startTransition] = useTransition(); | |
const [maxPrime, setMaxPrime] = useState(defaultValue); | |
const values = new Array(maxPrime).fill(null); | |
return ( | |
<div className="row"> | |
<h2 className="text-center mt-5">Prime Numbers</h2> | |
<PrimeRange | |
defaultValue={defaultValue} | |
// onChange={(value) => setMaxPrime(value)} | |
onChange={(value) => startTransition(() => setMaxPrime(value))} | |
/> | |
<div className="row row-cols-auto g-2"> | |
{values | |
.filter((_, index) => index < 10_000) | |
.map((_, index) => { | |
return <CheckNumber | |
key={index} | |
value={maxPrime - index} | |
isPending={isPending} // will display a hourglass , kinda like a loading state | |
/>; | |
})} | |
</div> | |
</div> | |
); | |
} | |
export function CheckNumber({ value, isPending }: Props) { | |
return ( | |
<div className="col"> | |
<div className={`card ${classes.CheckNumber}`}> | |
<div className="card-body text-center"> | |
<div className="fs-5">{value.toLocaleString()}</div> | |
<div className="fs-3"> | |
{isPending ? '⏳' : isPrime(value) ? '✔️' : '❌'} | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
// example 2 | |
export function UserList() { | |
const [isPending, startTransition] = useTransition(); | |
const [selectedUser, setSelectedUser] = useState<Account | null>(null); | |
const [selectedUserId, setSelectedUserId] = useState(NaN); | |
return ( | |
<button | |
className="btn shadow-none" | |
onClick={() => { | |
// notice here we set two useState value at the same time | |
// this will both fire in sequence and not at the same time | |
setSelectedUserId(user.id); | |
startTransition(() => setSelectedUser(user)); | |
}} | |
> | |
{isPending && selectedUserId === user.id && '⏳'} | |
{user.firstname} | |
| |
{user.surname} | |
</button> | |
) | |
} | |
// ------------ custom hook part 4 | |
// A store responsible for managing theme selection, fetching data, and tracking the loading state of this fetching request: | |
const theme = { | |
DARK: "dark", | |
LIGHT: "light", | |
}; | |
export const GlobalStore = () => { | |
const [selectedTheme, setSelectedTheme] = useState | |
(theme.LIGHT); | |
const [serverData, setServerData] = useState(null); | |
const [isLoadingData, setIsLoadingData] = useState | |
(false); | |
const toggleTheme = () => { | |
setSelectedTheme((currentTheme) => | |
currentTheme === theme.LIGHT ? theme.DARK : | |
theme.LIGHT | |
); | |
}; | |
const fetchData = (name = "Daniel") => { | |
setIsLoadingData(true); | |
fetch(`<insert_url_here>/${name}`) | |
.then((response) => response.json()) | |
.then((responseData) => { | |
setServerData(responseData); | |
}) | |
.finally(() => { | |
setIsLoadingData(false); | |
}) | |
.catch(() => setIsLoadingData(false)); | |
}; | |
useEffect(() => { | |
fetchData(); | |
}, []); | |
return { | |
selectedTheme, | |
toggleTheme, | |
serverData, | |
isLoadingData, | |
fetchData | |
}; | |
}; | |
// ------------ custom hook part 4 | |
import React, { useCallback, useState } from "react"; | |
export default function App() { | |
const [name, setName] = useState(""); | |
const [surname, setSurname] = useState(""); | |
const handleNameChange = useCallback((e) => { | |
setName(e.target.value); | |
}, []); | |
const handleSurnameChange = useCallback((e) => { | |
setSurname(e.target.value); | |
}, []); | |
return ( | |
<div> | |
<input value={name} onChange={handleNameChange} /> | |
<input value={surname} onChange={handleSurnameChange} /> | |
</div> | |
); | |
} | |
// simplify version above | |
import React, { useCallback, useState } from "react"; | |
function useInput(defaultValue = "") { | |
// We declare this state only once! | |
const [value, setValue] = useState(defaultValue); | |
// We write this handler only once! | |
const handleChange = useCallback((e) => { | |
setValue(e.target.value); | |
}, []); | |
// Cases when we need setValue are also possible | |
return [value, handleChange, setValue]; | |
} | |
export default function App() { | |
const [name, onChangeName] = useInput("Pavel"); | |
const [surname, onChangeSurname] = useInput("Pogosov"); | |
return ( | |
<div> | |
<input value={name} onChange={onChangeName} /> | |
<input value={surname} onChange={onChangeSurname} /> | |
</div> | |
); | |
} | |
// ------------ custom hook part 5 | |
import { useEffect } from "react" | |
import useMediaQuery from "../useMediaQuery/useMediaQuery" | |
import { useLocalStorage } from "../useStorage/useStorage" | |
export default function useDarkMode() { | |
const [darkMode, setDarkMode] = useLocalStorage("useDarkMode") | |
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)") | |
const enabled = darkMode ?? prefersDarkMode | |
useEffect(() => { | |
document.body.classList.toggle("dark-mode", enabled) | |
}, [enabled]) | |
return [enabled, setDarkMode] | |
} | |
// useDebounce | |
export default function useDebounce(value, delay) { | |
const [ debounceValue, setDebounceValue ] = useState(value); | |
useEffect(() => { | |
const timeoutId = setTimeout(() => { | |
setDebounceValue(value); | |
}, delay); | |
return () => clearTimeout(timeoutId); | |
}, [value]); | |
return debounceValue; | |
} | |
// to use above | |
// seperate file | |
const [value, setValue] = useState(""); | |
const debounceValue = useDebounce(value, 500); | |
// ------------ useEffect dependencies explained | |
useEffect(() => { | |
// Runs after first render and every re-render with dependency change | |
}, [name, status]); | |
useEffect(() => { | |
// Runs after initial render only | |
}, []); | |
useEffect(() => { | |
// Runs after every re-render | |
}); | |
// ------------ custom hook part 6 | |
// import { useState, useEffect } from "react"; | |
const useFetchData = (url, initialData) => { | |
const [data, setData] = useState(initialData); | |
const [loading, setLoading] = useState(false); | |
useEffect(() => { | |
setLoading(true); | |
fetch(url) | |
.then((res) => res.json()) | |
.then((data) => setData(data)) | |
.catch((err) => console.log(err)) | |
.finally(() => setLoading(false)); | |
}, [url]); | |
return {data, loading}; | |
}; | |
export default useFetchData; | |
// usage.... | |
// import useFetchData from './useFetchData.js'; | |
export default function Posts() { | |
const url = "https://jsonplaceholder.typicode.com/posts?userId=1"; | |
const { data, loading} = useFetchData(url, []); | |
return ( | |
<> | |
{loading && <p>Loading posts… </p>} | |
{data && ( | |
data.map((item) => | |
<div key={item?.title}> | |
<p> | |
{item?.title} | |
<br/> | |
{item?.body} | |
</p> | |
</div> | |
) | |
)} | |
</> | |
); | |
} | |
// ------------ useDebug | |
// have to have React DevTools extension | |
const useFetchData = (url, initialData) => { | |
useDebugValue(url); | |
const [data, setData] = useState(initialData); | |
const [loading, setLoading] = useState(false); | |
const [error, setError] = useState(null); | |
useDebugValue(error, (err) => | |
err ? `fetch is failed with ${err.message}` : | |
"fetch is successful" | |
); | |
useEffect(() => { | |
setLoading(true); | |
fetch(url) | |
.then((res) => res.json()) | |
.then((data) => setData(data)) | |
.catch((err) => setError(err)) | |
.finally(() => setLoading(false)); | |
}, [url]); | |
useDebugValue(data, (items) => | |
items.length > 0 ? items.map((item) => item.title) : | |
"No posts available" | |
); | |
return {data, loading}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment