Skip to content

Instantly share code, notes, and snippets.

@arnoldc
Last active December 3, 2023 01:10
Show Gist options
  • Save arnoldc/de8360e84f6427083ec3d65d845c570c to your computer and use it in GitHub Desktop.
Save arnoldc/de8360e84f6427083ec3d65d845c570c to your computer and use it in GitHub Desktop.
[React] Hooks
// ------------ 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}
&nbsp;
{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