Last active
September 2, 2022 06:41
-
-
Save emreavcilar/23cd4fd1c1071644fe3a26bd8b22b539 to your computer and use it in GitHub Desktop.
useful react hooks
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
/* | |
Useful react hooks | |
What are react hooks ? | |
Hooks are the new feature introduced in the React 16.8 version. It allows you to use | |
state and other React features without writing a class. | |
Hooks are the functionswhich "hook into" React state and lifecycle features from function components. | |
It does not work inside classes. | |
Hooks are backward-compatible, which means it does not contain any breaking changes. | |
Also, it does not replace your knowledge of React concepts. | |
--------------------------------------------------- | |
When to use a Hooks | |
If you write a function component, and then you want to add some state to it, | |
previously you do this by converting it to a class. But, now you can do it by | |
using a Hook inside the existing function component. | |
-------------------------------------------------- | |
Rules of Hooks | |
Hooks are similar to JavaScript functions, but you need to follow these two rules when | |
using them. Hooks rule ensures that all the stateful logic in a component is visible in its source code. | |
These rules are: | |
1. Only call Hooks at the top level | |
Do not call Hooks inside loops, conditions, or nested functions. | |
Hooks should always be used at the top level of the React functions. | |
This rule ensures that Hooks are called in the same order each time a components renders. | |
2. Only call Hooks from React functions | |
You cannot call Hooks from regular JavaScript functions. Instead, | |
you can call Hooks from React function components. Hooks can also be called from custom Hooks. | |
---------------------------------------------- | |
Types of hooks | |
- Basic Hooks | |
- useState | |
- useEffect | |
- useContext | |
- Additional Hooks | |
- useReducer | |
- useCallback | |
- uesMemo | |
- useRef | |
- useImperativeHandle | |
- useLayoutEffect | |
- uesDebugValue | |
*/ |
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
/* | |
You cannot use hooks conditionally in your reactjs projects. Hooks must be | |
called in the same order every time a component is rendered, but there is | |
a trick to get around this problem. Conditionally render that Null component. | |
*/ | |
/* | |
You can not conditionally use hooks!! ----- WRONG | |
*/ | |
const Example = ({enabled}) => { | |
const [data,setData] = useState([]); | |
if(enabled){ | |
useEffect(()=>{ | |
fetch('https://api.example.com/items') | |
.then((res) => res.json()) | |
.then((data) => setData(data)); | |
}, [setData]); | |
} | |
return ( | |
<div> | |
{data.map((item,key)=>( | |
<div key={key}>item</div> | |
))} | |
</div> | |
); | |
}; | |
/* | |
Use the hook inside a Null component. --- TRUE | |
*/ | |
const FetchData = ({setData}) => { | |
useEffect(()=>{ | |
fetch('https://api.example.com/items') | |
.then((res) => res.json()) | |
.then((data) => setData(data)); | |
}, [setData]); | |
return null; | |
}; | |
/* | |
You can conditionally use the Null component | |
*/ | |
const Example = ({enabled}) => { | |
const [data, setData] = useState([]); | |
return ( | |
<div> | |
{enabled && <FetchData setData={setData} />} | |
{data.map((item,key) => ( | |
<div key={key}>item</div> | |
))} | |
</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
/* | |
This hook allows you to smoothly animate any value | |
using an easing function (linear, elastic, etc). | |
In the example we call the useAnimation hook three | |
times to animated three balls on to the screen at | |
different intervals. Additionally we show how easy | |
it is to compose hooks. Our useAnimation hook doesn't | |
actual make use of useState or useEffect itself, but | |
instead serves as a wrapper around the useAnimationTimer | |
hook. Having the timer logic abstracted out into its | |
own hook gives us better code readability and the | |
ability to use timer logic in other contexts. Be | |
sure to check out the CodeSandbox Demo for this one. | |
*/ | |
import { useState, useEffect } from "react"; | |
// Usage | |
function App() { | |
// Call hook multiple times to get animated values with different start delays | |
const animation1 = useAnimation("elastic", 600, 0); | |
const animation2 = useAnimation("elastic", 600, 150); | |
const animation3 = useAnimation("elastic", 600, 300); | |
return ( | |
<div style={{ display: "flex", justifyContent: "center" }}> | |
<Ball | |
innerStyle={{ | |
marginTop: animation1 * 200 - 100, | |
}} | |
/> | |
<Ball | |
innerStyle={{ | |
marginTop: animation2 * 200 - 100, | |
}} | |
/> | |
<Ball | |
innerStyle={{ | |
marginTop: animation3 * 200 - 100, | |
}} | |
/> | |
</div> | |
); | |
} | |
const Ball = ({ innerStyle }) => ( | |
<div | |
style={{ | |
width: 100, | |
height: 100, | |
marginRight: "40px", | |
borderRadius: "50px", | |
backgroundColor: "#4dd5fa", | |
...innerStyle, | |
}} | |
/> | |
); | |
// Hook | |
function useAnimation(easingName = "linear", duration = 500, delay = 0) { | |
// The useAnimationTimer hook calls useState every animation frame ... | |
// ... giving us elapsed time and causing a rerender as frequently ... | |
// ... as possible for a smooth animation. | |
const elapsed = useAnimationTimer(duration, delay); | |
// Amount of specified duration elapsed on a scale from 0 - 1 | |
const n = Math.min(1, elapsed / duration); | |
// Return altered value based on our specified easing function | |
return easing[easingName](n); | |
} | |
// Some easing functions copied from: | |
// https://github.com/streamich/ts-easing/blob/master/src/index.ts | |
// Hardcode here or pull in a dependency | |
const easing = { | |
linear: (n) => n, | |
elastic: (n) => | |
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15), | |
inExpo: (n) => Math.pow(2, 10 * (n - 1)), | |
}; | |
function useAnimationTimer(duration = 1000, delay = 0) { | |
const [elapsed, setTime] = useState(0); | |
useEffect( | |
() => { | |
let animationFrame, timerStop, start; | |
// Function to be executed on each animation frame | |
function onFrame() { | |
setTime(Date.now() - start); | |
loop(); | |
} | |
// Call onFrame() on next animation frame | |
function loop() { | |
animationFrame = requestAnimationFrame(onFrame); | |
} | |
function onStart() { | |
// Set a timeout to stop things when duration time elapses | |
timerStop = setTimeout(() => { | |
cancelAnimationFrame(animationFrame); | |
setTime(Date.now() - start); | |
}, duration); | |
// Start the loop | |
start = Date.now(); | |
loop(); | |
} | |
// Start after specified delay (defaults to 0) | |
const timerDelay = setTimeout(onStart, delay); | |
// Clean things up | |
return () => { | |
clearTimeout(timerStop); | |
clearTimeout(timerDelay); | |
cancelAnimationFrame(animationFrame); | |
}; | |
}, | |
[duration, delay] // Only re-run effect if duration or delay changes | |
); | |
return elapsed; | |
} |
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
// A reactjs hook that joins an Array of Elements with a separating Element. | |
// Takes an Array of elements as the first argument, and a function as the second | |
// that returns the separator. | |
export const useArrayJoin = (arr, join) => { | |
return useMemo(()=>{ | |
return arr.reduce( | |
(acc,next,key) => | |
acc === null ? [next] : [...acc, join(key), next], | |
null | |
); | |
}, [arr, join]); | |
}; | |
//usage | |
export const Example = () => { | |
const list = [ | |
<div key="first">First</div>, | |
<div key="second">Second</div>, | |
<div key="third">Third</div> | |
]; | |
const joined = useArrayJoin( | |
list, | |
(key) => <div key={key}>•</div> | |
); | |
return <div class="list">{joined}</div> | |
} | |
//output | |
<div class="list"> | |
<div>First</div> | |
<div>•</div> | |
<div>Second</div> | |
<div>•</div> | |
<div>Third</div> | |
</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
/* | |
It's generally a good practice to indicate to users the status of | |
any async request. An example would be fetching data from an API | |
and displaying a loading indicator before rendering the results. | |
Another example would be a form where you want to disable the submit | |
button when the submission is pending and then display either a | |
success or error message when it completes. | |
Rather than litter your components with a bunch of useState | |
calls to keep track of the state of an async function, | |
you can use our custom hook which takes an async function | |
as an input and returns the value, error, and status values | |
we need to properly update our UI. Possible values for status | |
prop are: "idle", "pending", "success", "error". As you'll see | |
in the code below, our hook allows both immediate execution and | |
delayed execution using the returned execute function. | |
*/ | |
import React, { useState, useEffect, useCallback } from "react"; | |
// Usage | |
/* | |
function App() { | |
const { execute, status, value, error } = useAsync(myFunction, false); | |
return ( | |
<div> | |
{status === "idle" && <div>Start your journey by clicking a button</div>} | |
{status === "success" && <div>{value}</div>} | |
{status === "error" && <div>{error}</div>} | |
<button onClick={execute} disabled={status === "pending"}> | |
{status !== "pending" ? "Click me" : "Loading..."} | |
</button> | |
</div> | |
); | |
} | |
// An async function for testing our hook. | |
// Will be successful 50% of the time. | |
const myFunction = () => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
const rnd = Math.random() * 10; | |
rnd <= 5 | |
? resolve("Submitted successfully 🙌") | |
: reject("Oh no there was an error 😞"); | |
}, 2000); | |
}); | |
}; | |
*/ | |
// Hook | |
const useAsync = (asyncFunction, immediate = true) => { | |
const [status, setStatus] = useState("idle"); | |
const [value, setValue] = useState(null); | |
const [error, setError] = useState(null); | |
// The execute function wraps asyncFunction and | |
// handles setting state for pending, value, and error. | |
// useCallback ensures the below useEffect is not called | |
// on every render, but only if asyncFunction changes. | |
const execute = useCallback(() => { | |
setStatus("pending"); | |
setValue(null); | |
setError(null); | |
return asyncFunction() | |
.then((response) => { | |
setValue(response); | |
setStatus("success"); | |
}) | |
.catch((error) => { | |
setError(error); | |
setStatus("error"); | |
}); | |
}, [asyncFunction]); | |
// Call execute if we want to fire it right away. | |
// Otherwise execute can be called later, such as | |
// in an onClick handler. | |
useEffect(() => { | |
if (immediate) { | |
execute(); | |
} | |
}, [execute, immediate]); | |
return { execute, status, value, error }; | |
}; |
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
/* | |
reactjs hook that plays an audio file from a source URL. | |
*/ | |
export const useAudio = (src,play,volume,loop,onDone) => { | |
const audio = useMemo( | |
() => { | |
const audio = new Audio(src); | |
audio.loop = loop; | |
audio.volume = volume / 100; | |
return audio; | |
}, [src,volume,loop]); | |
useEffect(()=>{ | |
const ended = () => onDone && onDone(); | |
if(play){ | |
audio.play().then(); | |
audio.addEventListener('ended',ended); | |
} | |
return () => { | |
audio.pause(); | |
audio.removeEventListener('ended',ended); | |
}; | |
}, [audio,play,onDone]); | |
}; |
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
/* | |
A very common scenario is you have a bunch of components | |
that need to render different depending on whether the | |
current user is logged in and sometimes call authentication | |
methods like signin, signout, sendPasswordResetEmail, etc. | |
This is a perfect use-case for a useAuth hook that enables | |
any component to get the current auth state and re-render | |
if it changes. Rather than have each instance of the useAuth | |
hook fetch the current user, the hook simply calls useContext | |
to get the data from farther up in the component tree. | |
The real magic happens in our <ProvideAuth> component and our | |
useProvideAuth hook which wraps all our authentication methods | |
(in this case we're using Firebase) and then uses React Context | |
to make the current auth object available to all child components | |
that call useAuth. Whew, that was a mouthfull... | |
Hopefully as you read through the code below it should all | |
make sense. Another reason I like this method is it neatly | |
abstracts away our actual auth provider (Firebase), making it | |
super easy to change providers in the future. | |
*/ | |
// Top level App component | |
import React from "react"; | |
import { ProvideAuth } from "./use-auth.js"; | |
function App(props) { | |
return ( | |
<ProvideAuth> | |
{/* | |
Route components here, depending on how your app is structured. | |
If using Next.js this would be /pages/_app.js | |
*/} | |
</ProvideAuth> | |
); | |
} | |
// Any component that wants auth state | |
import React from "react"; | |
import { useAuth } from "./use-auth.js"; | |
function Navbar(props) { | |
// Get auth state and re-render anytime it changes | |
const auth = useAuth(); | |
return ( | |
<NavbarContainer> | |
<Logo /> | |
<Menu> | |
<Link to="/about">About</Link> | |
<Link to="/contact">Contact</Link> | |
{auth.user ? ( | |
<Fragment> | |
<Link to="/account">Account ({auth.user.email})</Link> | |
<Button onClick={() => auth.signout()}>Signout</Button> | |
</Fragment> | |
) : ( | |
<Link to="/signin">Signin</Link> | |
)} | |
</Menu> | |
</NavbarContainer> | |
); | |
} | |
// Hook (use-auth.js) | |
import React, { useState, useEffect, useContext, createContext } from "react"; | |
import * as firebase from "firebase/app"; | |
import "firebase/auth"; | |
// Add your Firebase credentials | |
firebase.initializeApp({ | |
apiKey: "", | |
authDomain: "", | |
projectId: "", | |
appID: "", | |
}); | |
const authContext = createContext(); | |
// Provider component that wraps your app and makes auth object ... | |
// ... available to any child component that calls useAuth(). | |
export function ProvideAuth({ children }) { | |
const auth = useProvideAuth(); | |
return <authContext.Provider value={auth}>{children}</authContext.Provider>; | |
} | |
// Hook for child components to get the auth object ... | |
// ... and re-render when it changes. | |
export const useAuth = () => { | |
return useContext(authContext); | |
}; | |
// Provider hook that creates auth object and handles state | |
function useProvideAuth() { | |
const [user, setUser] = useState(null); | |
// Wrap any Firebase methods we want to use making sure ... | |
// ... to save the user to state. | |
const signin = (email, password) => { | |
return firebase | |
.auth() | |
.signInWithEmailAndPassword(email, password) | |
.then((response) => { | |
setUser(response.user); | |
return response.user; | |
}); | |
}; | |
const signup = (email, password) => { | |
return firebase | |
.auth() | |
.createUserWithEmailAndPassword(email, password) | |
.then((response) => { | |
setUser(response.user); | |
return response.user; | |
}); | |
}; | |
const signout = () => { | |
return firebase | |
.auth() | |
.signOut() | |
.then(() => { | |
setUser(false); | |
}); | |
}; | |
const sendPasswordResetEmail = (email) => { | |
return firebase | |
.auth() | |
.sendPasswordResetEmail(email) | |
.then(() => { | |
return true; | |
}); | |
}; | |
const confirmPasswordReset = (code, password) => { | |
return firebase | |
.auth() | |
.confirmPasswordReset(code, password) | |
.then(() => { | |
return true; | |
}); | |
}; | |
// Subscribe to user on mount | |
// Because this sets state in the callback it will cause any ... | |
// ... component that utilizes this hook to re-render with the ... | |
// ... latest auth object. | |
useEffect(() => { | |
const unsubscribe = firebase.auth().onAuthStateChanged((user) => { | |
if (user) { | |
setUser(user); | |
} else { | |
setUser(false); | |
} | |
}); | |
// Cleanup subscription on unmount | |
return () => unsubscribe(); | |
}, []); | |
// Return the user object and auth methods | |
return { | |
user, | |
signin, | |
signup, | |
signout, | |
sendPasswordResetEmail, | |
confirmPasswordReset, | |
}; | |
} |
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
// reactjs hook that listens for Click events that are outside of a target DOM element. | |
// Allows you to handle when a user clicks away from a component. | |
// For example, closing a popup without using an overlay element. | |
const useClickOutside = (cb, dep) => { | |
const ref = useRef(null); | |
const callback = useCallback(cb,dep); | |
useEffect(()=>{ | |
const listener => (e) => { | |
if(!ref.current?.contains(e.target)){ | |
callback(e); | |
} | |
}; | |
document.addEventListener('click',listener); | |
return () => document.removeEventListener('click',listener); | |
},[ref]); | |
return ref; | |
} |
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
// reactjs hook that formats a number as a currency using the browser's locale. | |
// Allows currencies to be shown differently depending where in the world the | |
// user is viewing the webpage. I've included 8 common ISO 4217 codes. | |
const CODES = { | |
IN: 'INR', SE: 'SEK', GB: 'GBP', IE: 'EUR', | |
CN: 'CNY', JP: 'JPY', US: 'USD', KW: 'KWD' | |
}; | |
const useCurrency = (value, locale = navigator.language) => { | |
const format = useMemo(()=> { | |
const currency = CODES[locale.substr(-2)] || 'USD'; | |
return new Intl.NumberFormat(locale, { | |
style:'currency', | |
currency | |
}); | |
}, [locale]); | |
return useMemo(()=> format.format(value), [value]); | |
}; |
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
/* | |
This hook handles all the stateful logic required to add | |
a ☾ dark mode 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. | |
This post also helps illustrate the power of hook composition. | |
The syncing of state to localStorage is handled by our useLocalStorage hook. | |
Detecting the user's dark mode preference is handled by our useMedia hook. | |
Both of these hooks were created for other use-cases, but here we've composed | |
them to create a super useful hook in relatively few lines of code. It's almost | |
as if hooks bring the compositional power of React components to stateful logic! 🤯 | |
*/ | |
// Usage | |
function App() { | |
const [darkMode, setDarkMode] = useDarkMode(); | |
return ( | |
<div> | |
<div className="navbar"> | |
<Toggle darkMode={darkMode} setDarkMode={setDarkMode} /> | |
</div> | |
<Content /> | |
</div> | |
); | |
} | |
// Hook | |
function useDarkMode() { | |
// Use our useLocalStorage hook to persist state through a page refresh. | |
// Read the recipe for this hook to learn more: usehooks.com/useLocalStorage | |
const [enabledState, setEnabledState] = useLocalStorage("dark-mode-enabled"); | |
// See if user has set a browser or OS preference for dark mode. | |
// The usePrefersDarkMode hook composes a useMedia hook (see code below). | |
const prefersDarkMode = usePrefersDarkMode(); | |
// If enabledState is defined use it, otherwise fallback to prefersDarkMode. | |
// This allows user to override OS level setting on our website. | |
const enabled = | |
typeof enabledState !== "undefined" ? enabledState : prefersDarkMode; | |
// Fire off effect that add/removes dark mode class | |
useEffect( | |
() => { | |
const className = "dark-mode"; | |
const element = window.document.body; | |
if (enabled) { | |
element.classList.add(className); | |
} else { | |
element.classList.remove(className); | |
} | |
}, | |
[enabled] // Only re-call effect when value changes | |
); | |
// Return enabled state and setter | |
return [enabled, setEnabledState]; | |
} | |
// Compose our useMedia hook to detect dark mode preference. | |
// The API for useMedia looks a bit weird, but that's because ... | |
// ... it was designed to support multiple media queries and return values. | |
// Thanks to hook composition we can hide away that extra complexity! | |
// Read the recipe for useMedia to learn more: usehooks.com/useMedia | |
function usePrefersDarkMode() { | |
return useMedia(["(prefers-color-scheme: dark)"], [true], false); | |
} |
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
//A reactjs hook that debounces values by discarding inputs | |
//that take less than the specified milliseconds between changes. | |
export const useDebounce = (next,ms) => { | |
const [value,setValue] = useState(next); | |
useEffect(()=>{ | |
const id = setTimeout(() => setValue(next),ms); | |
return () => clearTimeout(id); | |
}, [next,ms]); | |
return value; | |
} |
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
/* | |
This hook allows you to debounce any fast changing value. | |
The debounced value will only reflect the latest value when | |
the useDebounce hook has not been called for the specified | |
time period. When used in conjunction with useEffect, as we | |
do in the recipe below, you can easily ensure that expensive | |
operations like API calls are not executed too frequently. | |
The example below allows you to search the Marvel Comic API | |
and uses useDebounce to prevent API calls from being fired | |
on every keystroke. Be sure to check out the CodeSandbox demo | |
for this one. Hook code and inspiration from github.com/xnimorz/use-debounce. | |
*/ | |
import { useState, useEffect, useRef } from "react"; | |
// Usage | |
function App() { | |
// State and setters for ... | |
// Search term | |
const [searchTerm, setSearchTerm] = useState(""); | |
// API search results | |
const [results, setResults] = useState([]); | |
// Searching status (whether there is pending API request) | |
const [isSearching, setIsSearching] = useState(false); | |
// Debounce search term so that it only gives us latest value ... | |
// ... if searchTerm has not been updated within last 500ms. | |
// The goal is to only have the API call fire when user stops typing ... | |
// ... so that we aren't hitting our API rapidly. | |
const debouncedSearchTerm = useDebounce(searchTerm, 500); | |
// Effect for API call | |
useEffect( | |
() => { | |
if (debouncedSearchTerm) { | |
setIsSearching(true); | |
searchCharacters(debouncedSearchTerm).then((results) => { | |
setIsSearching(false); | |
setResults(results); | |
}); | |
} else { | |
setResults([]); | |
setIsSearching(false); | |
} | |
}, | |
[debouncedSearchTerm] // Only call effect if debounced search term changes | |
); | |
return ( | |
<div> | |
<input | |
placeholder="Search Marvel Comics" | |
onChange={(e) => setSearchTerm(e.target.value)} | |
/> | |
{isSearching && <div>Searching ...</div>} | |
{results.map((result) => ( | |
<div key={result.id}> | |
<h4>{result.title}</h4> | |
<img | |
src={`${result.thumbnail.path}/portrait_incredible.${result.thumbnail.extension}`} | |
/> | |
</div> | |
))} | |
</div> | |
); | |
} | |
// API search function | |
function searchCharacters(search) { | |
const apiKey = "f9dfb1e8d466d36c27850bedd2047687"; | |
return fetch( | |
`https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`, | |
{ | |
method: "GET", | |
} | |
) | |
.then((r) => r.json()) | |
.then((r) => r.data.results) | |
.catch((error) => { | |
console.error(error); | |
return []; | |
}); | |
} | |
// Hook | |
function useDebounce(value, delay) { | |
// State and setters for debounced value | |
const [debouncedValue, setDebouncedValue] = useState(value); | |
useEffect( | |
() => { | |
// Update debounced value after delay | |
const handler = setTimeout(() => { | |
setDebouncedValue(value); | |
}, delay); | |
// Cancel the timeout if value changes (also on delay change or unmount) | |
// This is how we prevent debounced value from updating if value is changed ... | |
// .. within the delay period. Timeout gets cleared and restarted. | |
return () => { | |
clearTimeout(handler); | |
}; | |
}, | |
[value, delay] // Only re-call effect if value or delay changes | |
); | |
return debouncedValue; | |
} |
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
/* | |
reactjs has a built-in debouncing hook called useDeferredValue(). | |
It returns the previous value until the input value stabilizes | |
from changes and then returns the latest value. | |
*/ | |
function App() { | |
const [text, setText] = useState("hello"); | |
const deferredText = useDeferredValue(text, {timeoutMs:200}); | |
const handleChange = (e) => setText(e.target.value); | |
return ( | |
<div className="app"> | |
{/* Keep passing the current text to the input */} | |
<input value={text} onChange={handleChange} /> | |
... | |
{/* But the list is allowed to "lag behind" */} | |
<MySlowList text={deferredText} /> | |
</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
// reactjs hook that emits when the document changes visibility. | |
// You can use this to pause music, or check if a user's data should be | |
// refreshed on the screen. | |
export const useDocVisible = () => { | |
const isVisible = () => document.visibilityState === 'visible'; | |
const [visible, setVisible] = useState(isVisible()); | |
useEffect(()=>{ | |
const onVisible = () => setVisible(isVisible()); | |
document.addEventListener('visibilitychange',onVisible); | |
return () => document.removeEventListener('visibilitychange',onVisible); | |
}, []); | |
return visible; | |
} |
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
import { useSelector } from 'react-redux'; | |
export function useEndpoint(value,prefix){ | |
const availableEndpoints = useSelector(state => state.sessionReducer.availableEndpoints); | |
if(value && value.path){ | |
let address = value.path; | |
if(prefix){ | |
address = prefix + value.path; | |
} | |
return availableEndpoints.indexOf(address)>-1; | |
} | |
return false; | |
} |
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
import { useRef, useEffect, useEffect, useCallback } from 'react'; | |
/** | |
* If you find yourself adding a lot of event listeners using useEffect | |
* you might consider moving that logic to a custom hook. In the recipe | |
* below we create a useEventListener hook that handles checking if | |
* addEventListener is supported, adding the event listener, and removal | |
* on cleanup. | |
* | |
// Usage | |
function App(){ | |
// State for storing mouse coordinates | |
const [coords, setCoords] = useState({ x: 0, y: 0 }); | |
// Event handler utilizing useCallback ... | |
// ... so that reference never changes. | |
const handler = useCallback( | |
({ clientX, clientY }) => { | |
// Update coordinates | |
setCoords({ x: clientX, y: clientY }); | |
}, | |
[setCoords] | |
); | |
// Add event listener using our hook | |
useEventListener('mousemove', handler); | |
return ( | |
<h1> | |
The mouse position is ({coords.x}, {coords.y}) | |
</h1> | |
); | |
} | |
*/ | |
export function useEventListener(eventName, handler, element = window){ | |
// Create a ref that stores handler | |
const savedHandler = useRef(); | |
// Update ref.current value if handler changes. | |
// This allows our effect below to always get latest handler ... | |
// ... without us needing to pass it in effect deps array ... | |
// ... and potentially cause effect to re-run every render. | |
useEffect(() => { | |
savedHandler.current = handler; | |
}, [handler]); | |
useEffect( | |
() => { | |
// Make sure element supports addEventListener | |
// On | |
const isSupported = element && element.addEventListener; | |
if (!isSupported) return; | |
// Create event listener that calls handler function stored in ref | |
const eventListener = event => savedHandler.current(event); | |
// Add event listener | |
element.addEventListener(eventName, eventListener); | |
// Remove event listener on cleanup | |
return () => { | |
element.removeEventListener(eventName, eventListener); | |
}; | |
}, | |
[eventName, element] // Re-run if eventName or element changes | |
); | |
}; |
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
// A reactjs hook that only calls setState() for values that pass the provided condition. | |
export const useFilterState = (filter,initialState) => { | |
const [value,setValue] = useState(initialState); | |
const setFilter = useCallback( | |
next => filter(next) && setValue(next), | |
[filter] | |
); | |
return [value, setFilter]; | |
} |
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
/* | |
This hook makes it super easy to subscribe to data in your Firestore | |
database without having to worry about state management. Instead of | |
calling Firestore's query.onSnapshot() method you simply pass a query | |
to useFirestoreQuery() and you get back everything you need, including | |
status, data, and error. Your component will re-render when data changes | |
and your subscription will be automatically removed when the component | |
unmounts. Our example even supports dependent queries where you can | |
wait on needed data by passing a falsy value to the hook. Read through | |
the recipe and comments below to see how it works. | |
*/ | |
// Usage | |
/* | |
function ProfilePage({ uid }) { | |
// Subscribe to Firestore document | |
const { data, status, error } = useFirestoreQuery( | |
firestore.collection("profiles").doc(uid) | |
); | |
if (status === "loading") { | |
return "Loading..."; | |
} | |
if (status === "error") { | |
return `Error: ${error.message}`; | |
} | |
return ( | |
<div> | |
<ProfileHeader avatar={data.avatar} name={data.name} /> | |
<Posts posts={data.posts} /> | |
</div> | |
); | |
} | |
// Reducer for hook state and actions | |
const reducer = (state, action) => { | |
switch (action.type) { | |
case "idle": | |
return { status: "idle", data: undefined, error: undefined }; | |
case "loading": | |
return { status: "loading", data: undefined, error: undefined }; | |
case "success": | |
return { status: "success", data: action.payload, error: undefined }; | |
case "error": | |
return { status: "error", data: undefined, error: action.payload }; | |
default: | |
throw new Error("invalid action"); | |
} | |
}; | |
*/ | |
// Hook | |
function useFirestoreQuery(query) { | |
// Our initial state | |
// Start with an "idle" status if query is falsy, as that means hook consumer is | |
// waiting on required data before creating the query object. | |
// Example: useFirestoreQuery(uid && firestore.collection("profiles").doc(uid)) | |
const initialState = { | |
status: query ? "loading" : "idle", | |
data: undefined, | |
error: undefined, | |
}; | |
// Setup our state and actions | |
const [state, dispatch] = useReducer(reducer, initialState); | |
// Get cached Firestore query object with useMemoCompare (https://usehooks.com/useMemoCompare) | |
// Needed because firestore.collection("profiles").doc(uid) will always being a new object reference | |
// causing effect to run -> state change -> rerender -> effect runs -> etc ... | |
// This is nicer than requiring hook consumer to always memoize query with useMemo. | |
const queryCached = useMemoCompare(query, (prevQuery) => { | |
// Use built-in Firestore isEqual method to determine if "equal" | |
return prevQuery && query && query.isEqual(prevQuery); | |
}); | |
useEffect(() => { | |
// Return early if query is falsy and reset to "idle" status in case | |
// we're coming from "success" or "error" status due to query change. | |
if (!queryCached) { | |
dispatch({ type: "idle" }); | |
return; | |
} | |
dispatch({ type: "loading" }); | |
// Subscribe to query with onSnapshot | |
// Will unsubscribe on cleanup since this returns an unsubscribe function | |
return queryCached.onSnapshot( | |
(response) => { | |
// Get data for collection or doc | |
const data = response.docs | |
? getCollectionData(response) | |
: getDocData(response); | |
dispatch({ type: "success", payload: data }); | |
}, | |
(error) => { | |
dispatch({ type: "error", payload: error }); | |
} | |
); | |
}, [queryCached]); // Only run effect if queryCached changes | |
return state; | |
} | |
// Get doc data and merge doc.id | |
function getDocData(doc) { | |
return doc.exists === true ? { id: doc.id, ...doc.data() } : null; | |
} | |
// Get array of doc data from collection | |
function getCollectionData(collection) { | |
return collection.docs.map(getDocData); | |
} |
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
import * as React from 'react'; | |
const useFirstRender = () => { | |
const isFirst = React.useRef(true); | |
if(isFirst.current){ | |
isFirst.current = false; | |
return true; | |
} | |
return isFirst.current; | |
} | |
export { useFirstRender }; |
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
/* eslint-disable no-restricted-globals */ | |
import { useSelector } from 'react-redux'; | |
import { LANG_VALUES } from 'config/constants/appConstants'; | |
import { isNil } from 'lodash'; | |
export const addZero = (num, zeroCount = 2) => { | |
if (isNil(num) || isNil(zeroCount) || isNaN(num) || isNaN(zeroCount)) { | |
return num; | |
} | |
return parseFloat(num).toFixed(zeroCount); | |
}; | |
export const useFormatConverter = () => { | |
const systemLang = useSelector((state) => state.sessionReducer.locale); | |
const convertBaseNumber = (value, lang = systemLang) => { | |
/* | |
value='1234', lang='tr' -> 1.234 | |
value='850ab', lang='tr' -> 850ab | |
value='1234', lang='en' -> 1,234 | |
value='850ab', lang='en' -> 850ab | |
*/ | |
if (value === null) { | |
return null; | |
} | |
let result = String(value); | |
const digitSeperator = lang === LANG_VALUES.tr ? '.' : ','; | |
if (isNaN(result)) { | |
return result; | |
} | |
if (result.indexOf('.') !== -1) { | |
result = String(result.split('.')[0]); | |
} | |
if (result.length > 3) { | |
result = result.replace(/(\d)(?=(\d{3})+$)/g, `$1${digitSeperator}`); | |
} | |
return result; | |
}; | |
const formatNumber = (value, digit = 2, lang = systemLang) => { | |
/* | |
value='1234', digit=2, lang='tr' -> 1.234 | |
value='1234.842', digit=2, lang='tr' -> 1.234,84 | |
value='1234.842', digit=1, lang='tr' -> 1.234,8 | |
value='123abc', digit=2, lang='tr' -> 123abc | |
value='1234', digit=2, lang='en' -> 1,234 | |
value='1234.842', digit=2, lang='en' -> 1,234.84 | |
value='1234.842', digit=1, lang='en' -> 1,234.8 | |
value='123abc', digit=2, lang='en' -> 123abc | |
*/ | |
// if( value === null || isNaN( value ) || isNaN( digit ) ) { return null; } | |
if (value === null || value === undefined) { | |
return null; | |
} | |
digit = digit === false ? -1 : digit; | |
// değer yerine "Hesaplanmadı" gibi string değer döndüğü için return edildi. | |
if (isNaN(value) || isNaN(digit)) { | |
return value; | |
} | |
const str = String(value); | |
const floatSeperator = lang === 'tr' ? ',' : '.'; | |
if (str.indexOf('.') === -1 || digit === 0) { | |
// ondalık değer yok veya ondalık istenmiyor | |
return convertBaseNumber(str); | |
} | |
const numArray = str.split('.'); | |
const baseVal = convertBaseNumber(numArray[0]); | |
// let floatPart = numArray[1]; | |
// if (numArray[1].length < digit) { // gelen deger 2.4 digit 2 ise 2,4 yerine => 2,40 goserebilmek icin eklendi ! | |
// for(let i = 1; i <= (digit - numArray[1].length); i++) { | |
// floatPart += '0'; | |
// } | |
// } | |
// const floatVal = String(floatPart).substr(0, digit); | |
const floatVal = digit > 0 ? String(numArray[1]).substr(0, digit) : numArray[1]; | |
// console.log( value, digit, lang ); | |
return `${baseVal}${floatSeperator}${floatVal}`; | |
}; | |
const addZeroToFormattedNumber = (value, digit = 2, lang = systemLang) => { | |
if (value === null || value === undefined || digit === null || digit === undefined) { | |
return null; | |
} | |
digit = digit === false ? -1 : digit; | |
const floatSeperator = lang === 'tr' ? ',' : '.'; | |
const splitted = String(value).split(floatSeperator); | |
if (digit === 0) { | |
return splitted[0]; | |
} | |
let floatedPart = splitted.length > 1 ? String(splitted[1]) : ''; | |
if (floatedPart !== '') { | |
floatedPart = digit > 0 ? floatedPart.substr(0, digit) : floatedPart; | |
} | |
if (digit === -1) { | |
floatedPart = floatedPart !== '' ? `${floatSeperator}${floatedPart}` : floatedPart; | |
return `${splitted[0]}${floatedPart}`; | |
} | |
return `${splitted[0]}${floatSeperator}${floatedPart.padEnd(digit, '0')}`; | |
}; | |
const convertNumberToFrontend = (value, digit = 2, lang = systemLang) => { | |
const formatted = formatNumber(value, digit, lang); | |
// return formatted; | |
return addZeroToFormattedNumber(formatted, digit, lang); | |
}; | |
return { convertNumberToFrontend }; | |
}; |
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
// reactjs hook for watching the users current location as geo coordinates. | |
const useGeoLocation = (options) => { | |
const [geo, setGeo] = useState(); | |
const [err, setErr] = useState(); | |
useEffect(() => { | |
const id = navigator.geolocation.watchPosition( | |
pos => { | |
setGeo(pos); | |
err && setError(undefined); | |
}, | |
err => setError(err), | |
options | |
); | |
return () => navigator.geolocation.clearWatch(id); | |
}, [err, options]); | |
return [geo,err]; | |
} |
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
import {useEffect, useState} from 'react'; | |
const useGotQuote = () => { | |
const [quote, setQuote] = useState(''); | |
useEffect(() => { | |
fetch('https://game-of-thrones-quotes.herokuapp.com/v1/random') | |
.then((response) => response.json()) | |
.then((data) => setQuote(data?.sentence)); | |
}, []); | |
return quote; | |
}; |
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
/* | |
This hook makes it really easy to add undo/redo | |
functionality to your app. Our recipe is a simple | |
drawing app. It generates a grid of blocks, allows | |
you to click any block to toggle its color, and uses | |
the useHistory hook so we can undo, redo, or clear all | |
changes to the canvas. Check out our CodeSandbox demo. | |
Within our hook we're using useReducer to store state | |
instead of useState, which should look familiar to anyone | |
that's used redux (read more about useReducer in the official docs). | |
The hook code was copied, with minor changes, from the excellent | |
use-undo library, so if you'd like to pull this into your project | |
you can also use that library via npm. | |
*/ | |
import { useReducer, useCallback } from "react"; | |
// Usage | |
function App() { | |
const { state, set, undo, redo, clear, canUndo, canRedo } = useHistory({}); | |
return ( | |
<div className="container"> | |
<div className="controls"> | |
<div className="title">👩🎨 Click squares to draw</div> | |
<button onClick={undo} disabled={!canUndo}> | |
Undo | |
</button> | |
<button onClick={redo} disabled={!canRedo}> | |
Redo | |
</button> | |
<button onClick={clear}>Clear</button> | |
</div> | |
<div className="grid"> | |
{((blocks, i, len) => { | |
// Generate a grid of blocks | |
while (++i <= len) { | |
const index = i; | |
blocks.push( | |
<div | |
// Give block "active" class if true in state object | |
className={"block" + (state[index] ? " active" : "")} | |
// Toggle boolean value of clicked block and merge into current state | |
onClick={() => set({ ...state, [index]: !state[index] })} | |
key={i} | |
/> | |
); | |
} | |
return blocks; | |
})([], 0, 625)} | |
</div> | |
</div> | |
); | |
} | |
// Initial state that we pass into useReducer | |
const initialState = { | |
// Array of previous state values updated each time we push a new state | |
past: [], | |
// Current state value | |
present: null, | |
// Will contain "future" state values if we undo (so we can redo) | |
future: [], | |
}; | |
// Our reducer function to handle state changes based on action | |
const reducer = (state, action) => { | |
const { past, present, future } = state; | |
switch (action.type) { | |
case "UNDO": | |
const previous = past[past.length - 1]; | |
const newPast = past.slice(0, past.length - 1); | |
return { | |
past: newPast, | |
present: previous, | |
future: [present, ...future], | |
}; | |
case "REDO": | |
const next = future[0]; | |
const newFuture = future.slice(1); | |
return { | |
past: [...past, present], | |
present: next, | |
future: newFuture, | |
}; | |
case "SET": | |
const { newPresent } = action; | |
if (newPresent === present) { | |
return state; | |
} | |
return { | |
past: [...past, present], | |
present: newPresent, | |
future: [], | |
}; | |
case "CLEAR": | |
const { initialPresent } = action; | |
return { | |
...initialState, | |
present: initialPresent, | |
}; | |
} | |
}; | |
// Hook | |
const useHistory = (initialPresent) => { | |
const [state, dispatch] = useReducer(reducer, { | |
...initialState, | |
present: initialPresent, | |
}); | |
const canUndo = state.past.length !== 0; | |
const canRedo = state.future.length !== 0; | |
// Setup our callback functions | |
// We memoize with useCallback to prevent unnecessary re-renders | |
const undo = useCallback(() => { | |
if (canUndo) { | |
dispatch({ type: "UNDO" }); | |
} | |
}, [canUndo, dispatch]); | |
const redo = useCallback(() => { | |
if (canRedo) { | |
dispatch({ type: "REDO" }); | |
} | |
}, [canRedo, dispatch]); | |
const set = useCallback( | |
(newPresent) => dispatch({ type: "SET", newPresent }), | |
[dispatch] | |
); | |
const clear = useCallback(() => dispatch({ type: "CLEAR", initialPresent }), [ | |
dispatch, | |
]); | |
// If needed we could also return past and future state | |
return { state: state.present, set, undo, redo, clear, canUndo, canRedo }; | |
}; |
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
import { useRef, useState, useEffect } from 'react'; | |
/** | |
* Detect whether the mouse is hovering an element. The hook | |
* returns a ref and a boolean value indicating whether the | |
* element with that ref is currently being hovered. | |
* Just add the returned ref to any element whose hover | |
* state you want to monitor. One potential bug with this method: | |
* If you have logic that changes the element that hoverRef is added | |
* to then your event listeners will not necessarily get applied | |
* to the new element. If you need this functionality then use this | |
* alternate version that utilizes a callback ref. | |
// Usage | |
function App() { | |
const [hoverRef, isHovered] = useHover(); | |
return ( | |
<div ref={hoverRef}> | |
{isHovered ? '😁' : '☹️'} | |
</div> | |
); | |
} | |
*/ | |
export function useHover(){ | |
const [value, setValue] = useState(false); | |
const ref = useRef(null); | |
const handleMouseOver = () => setValue(true); | |
const handleMouseOut = () => setValue(false); | |
useEffect( | |
() => { | |
const node = ref.current; | |
if (node) { | |
node.addEventListener('mouseover', handleMouseOver); | |
node.addEventListener('mouseout', handleMouseOut); | |
return () => { | |
node.removeEventListener('mouseover', handleMouseOver); | |
node.removeEventListener('mouseout', handleMouseOut); | |
}; | |
} | |
}, | |
[ref.current] // Recall only if ref changes | |
); | |
return [ref, value]; | |
} |
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
import { useState, useEffect } from 'react'; | |
/** | |
* This hook makes it easy to detect when the user is pressing | |
* a specific key on their keyboard. The recipe is fairly simple, | |
* as I want to show how little code is required, but I challenge | |
* any readers to create a more advanced version of this hook. | |
* Detecting when multiple keys are held down at the same time | |
* would be a nice addition. Bonus points: also require they be | |
* held down in a specified order. Feel free to share anything you've | |
* created in this recipe's gist. (https://gist.github.com/gragland/b61b8f46114edbcf2a9e4bd5eb9f47f5) | |
// Usage | |
function App() { | |
// Call our hook for each key that we'd like to monitor | |
const happyPress = useKeyPress('h'); | |
const sadPress = useKeyPress('s'); | |
const robotPress = useKeyPress('r'); | |
const foxPress = useKeyPress('f'); | |
return ( | |
<div> | |
<div>h, s, r, f</div> | |
<div> | |
{happyPress && '😊'} | |
{sadPress && '😢'} | |
{robotPress && '🤖'} | |
{foxPress && '🦊'} | |
</div> | |
</div> | |
); | |
} | |
*/ | |
// Hook | |
export function useKeyPress(targetKey){ | |
// State for keeping track of whether key is pressed | |
const [keyPressed, setKeyPressed] = useState(false); | |
// If pressed key is our target key then set to true | |
function downHandler({ key }) { | |
if (key === targetKey) { | |
setKeyPressed(true); | |
} | |
} | |
// If released key is our target key then set to false | |
const upHandler = ({ key }) => { | |
if (key === targetKey) { | |
setKeyPressed(false); | |
} | |
}; | |
// Add event listeners | |
useEffect(() => { | |
window.addEventListener('keydown', downHandler); | |
window.addEventListener('keyup', upHandler); | |
// Remove event listeners on cleanup | |
return () => { | |
window.removeEventListener('keydown', downHandler); | |
window.removeEventListener('keyup', upHandler); | |
}; | |
}, []); // Empty array ensures that effect is only run on mount and unmount | |
return keyPressed; | |
} |
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
/* | |
Sync state to local storage so that it persists through | |
a page refresh. Usage is similar to useState except we | |
pass in a local storage key so that we can default to | |
that value on page load instead of the specified initial value. | |
*/ | |
import { useState } from "react"; | |
// Usage | |
function App() { | |
// Similar to useState but first arg is key to the value in local storage. | |
const [name, setName] = useLocalStorage("name", "Bob"); | |
return ( | |
<div> | |
<input | |
type="text" | |
placeholder="Enter your name" | |
value={name} | |
onChange={(e) => setName(e.target.value)} | |
/> | |
</div> | |
); | |
} | |
// Hook | |
function useLocalStorage(key, initialValue) { | |
// State to store our value | |
// Pass initial state function to useState so logic is only executed once | |
const [storedValue, setStoredValue] = useState(() => { | |
try { | |
// Get from local storage by key | |
const item = window.localStorage.getItem(key); | |
// Parse stored json or if none return initialValue | |
return item ? JSON.parse(item) : initialValue; | |
} catch (error) { | |
// If error also return initialValue | |
console.log(error); | |
return initialValue; | |
} | |
}); | |
// Return a wrapped version of useState's setter function that ... | |
// ... persists the new value to localStorage. | |
const setValue = (value) => { | |
try { | |
// Allow value to be a function so we have same API as useState | |
const valueToStore = | |
value instanceof Function ? value(storedValue) : value; | |
// Save state | |
setStoredValue(valueToStore); | |
// Save to local storage | |
window.localStorage.setItem(key, JSON.stringify(valueToStore)); | |
} catch (error) { | |
// A more advanced implementation would handle the error case | |
console.log(error); | |
} | |
}; | |
return [storedValue, setValue]; | |
} |
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
import { useLayoutEffect } from 'react'; | |
/** | |
* Sometimes you want to prevent your users from being able to scroll | |
* the body of your page while a particular component is absolutely | |
* positioned over your page (think modal or full-screen mobile menu). | |
* It can be confusing to see the background content scroll underneath | |
* a modal, especially if you intended to scroll an area within the modal. | |
* Well, this hook solves that! Simply call the useLockBodyScroll hook | |
* in any component and body scrolling will be locked until that | |
* component unmounts. | |
* | |
// Usage | |
function App(){ | |
// State for our modal | |
const [modalOpen, setModalOpen] = useState(false); | |
return ( | |
<div> | |
<button onClick={() => setModalOpen(true)}>Show Modal</button> | |
<Content /> | |
{modalOpen && ( | |
<Modal | |
title="Try scrolling" | |
content="I bet you you can't! Muahahaha 😈" | |
onClose={() => setModalOpen(false)} | |
/> | |
)} | |
</div> | |
); | |
} | |
function Modal({ title, content, onClose }){ | |
// Call hook to lock body scroll | |
useLockBodyScroll(); | |
return ( | |
<div className="modal-overlay" onClick={onClose}> | |
<div className="modal"> | |
<h2>{title}</h2> | |
<p>{content}</p> | |
</div> | |
</div> | |
); | |
} | |
*/ | |
export function useLockBodyScroll() { | |
useLayoutEffect(() => { | |
// Get original body overflow | |
const originalStyle = window.getComputedStyle(document.body).overflow; | |
// Prevent scrolling on mount | |
document.body.style.overflow = 'hidden'; | |
// Re-enable scrolling when component unmounts | |
return () => document.body.style.overflow = originalStyle; | |
}, []); // Empty array ensures effect is only run on mount and unmount | |
} |
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
/* | |
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). | |
You could create a hook that directly measures screen | |
width instead of using media queries, but this method | |
is nice because it makes it easy to share media queries | |
between JS and your stylesheet. See it in action in | |
the CodeSandbox Demo. | |
*/ | |
import { useState, useEffect } from "react"; | |
function App() { | |
const columnCount = useMedia( | |
// Media queries | |
["(min-width: 1500px)", "(min-width: 1000px)", "(min-width: 600px)"], | |
// Column counts (relates to above media queries by array index) | |
[5, 4, 3], | |
// Default column count | |
2 | |
); | |
// Create array of column heights (start at 0) | |
let columnHeights = new Array(columnCount).fill(0); | |
// Create array of arrays that will hold each column's items | |
let columns = new Array(columnCount).fill().map(() => []); | |
data.forEach((item) => { | |
// Get index of shortest column | |
const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); | |
// Add item | |
columns[shortColumnIndex].push(item); | |
// Update height | |
columnHeights[shortColumnIndex] += item.height; | |
}); | |
// Render columns and items | |
return ( | |
<div className="App"> | |
<div className="columns is-mobile"> | |
{columns.map((column) => ( | |
<div className="column"> | |
{column.map((item) => ( | |
<div | |
className="image-container" | |
style={{ | |
// Size image container to aspect ratio of image | |
paddingTop: (item.height / item.width) * 100 + "%", | |
}} | |
> | |
<img src={item.image} alt="" /> | |
</div> | |
))} | |
</div> | |
))} | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useMedia(queries, values, defaultValue) { | |
// Array containing a media query list for each query | |
const mediaQueryLists = queries.map((q) => window.matchMedia(q)); | |
// Function that gets value based on matching media query | |
const getValue = () => { | |
// Get index of first media query that matches | |
const index = mediaQueryLists.findIndex((mql) => mql.matches); | |
// Return related value or defaultValue if none | |
return typeof values[index] !== "undefined" ? values[index] : defaultValue; | |
}; | |
// State and setter for matched value | |
const [value, setValue] = useState(getValue); | |
useEffect( | |
() => { | |
// Event listener callback | |
// Note: By defining getValue outside of useEffect we ensure that it has ... | |
// ... current values of hook args (as this hook callback is created once on mount). | |
const handler = () => setValue(getValue); | |
// Set a listener for each media query with above handler as callback. | |
mediaQueryLists.forEach((mql) => mql.addListener(handler)); | |
// Remove listeners on cleanup | |
return () => | |
mediaQueryLists.forEach((mql) => mql.removeListener(handler)); | |
}, | |
[] // Empty array ensures effect is only run on mount and unmount | |
); | |
return value; | |
} |
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
/* | |
This hook is similar to useMemo, but instead of passing an array of dependencies | |
we pass a custom compare function that receives the previous and new value. | |
The compare function can then compare nested properties, call object methods, | |
or anything else to determine equality. If the compare function returns true | |
then the hook returns the old object reference. | |
It's worth noting that, unlike useMemo, this hook isn't meant to avoid expensive | |
calculations. It needs to be passed a computed value so that it can compare it | |
to the old value. Where this comes in handy is if you want to offer a library | |
to other developers and it would be annoying to force them to memoize an object | |
before passing it to your library. If that object is created in the component | |
body (often the case if it's based on props) then it's going to be a new object | |
on every render. If that object is a useEffect dependency then it's going to | |
cause the effect to fire on every render, which can lead to problems or even | |
an infinite loop. This hook allows you to avoid that scenario by using the old | |
object reference instead of the new one if your custom comparison function deems | |
them equal. | |
Read through the recipe and comments below. For a more practical example be | |
sure to check out our useFirestoreQuery hook. | |
*/ | |
import React, { useState, useEffect, useRef } from "react"; | |
// Usage | |
/* | |
function MyComponent({ obj }) { | |
const [state, setState] = useState(); | |
// Use the previous obj value if the "id" property hasn't changed | |
const objFinal = useMemoCompare(obj, (prev, next) => { | |
return prev && prev.id === next.id; | |
}); | |
// Here we want to fire off an effect if objFinal changes. | |
// If we had used obj directly without the above hook and obj was technically a | |
// new object on every render then the effect would fire on every render. | |
// Worse yet, if our effect triggered a state change it could cause an endless loop | |
// where effect runs -> state change causes rerender -> effect runs -> etc ... | |
useEffect(() => { | |
// Call a method on the object and set results to state | |
return objFinal.someMethod().then((value) => setState(value)); | |
}, [objFinal]); | |
// So why not pass [obj.id] as the dependency array instead? | |
useEffect(() => { | |
// Then eslint-plugin-hooks would rightfully complain that obj is not in the | |
// dependency array and we'd have to use eslint-disable-next-line to work around that. | |
// It's much cleaner to just get the old object reference with our custom hook. | |
return obj.someMethod().then((value) => setState(value)); | |
}, [obj.id]); | |
return <div> ... </div>; | |
} | |
*/ | |
// Hook | |
function useMemoCompare(next, compare) { | |
// Ref for storing previous value | |
const previousRef = useRef(); | |
const previous = previousRef.current; | |
// Pass previous and next value to compare function | |
// to determine whether to consider them equal. | |
const isEqual = compare(previous, next); | |
// If not equal update previousRef to next value. | |
// We only update if not equal so that this hook continues to return | |
// the same old value if compare keeps returning true. | |
useEffect(() => { | |
if (!isEqual) { | |
previousRef.current = next; | |
} | |
}); | |
// Finally, if equal then return the previous value | |
return isEqual ? previous : next; | |
} |
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
import { useEffect } from 'react'; | |
/** | |
* This hook allows you to detect clicks outside of a specified | |
* element. In the example below we use it to close a modal | |
* when any element outside of the modal is clicked. By abstracting | |
* this logic out into a hook we can easily use it across all of our | |
* components that need this kind of functionality (dropdown menus, | |
* tooltips, etc). | |
// Usage | |
function App() { | |
// Create a ref that we add to the element for which we want to detect outside clicks | |
const ref = useRef(); | |
// State for our modal | |
const [isModalOpen, setModalOpen] = useState(false); | |
// Call hook passing in the ref and a function to call on outside click | |
useOnClickOutside(ref, () => setModalOpen(false)); | |
return ( | |
<div> | |
{isModalOpen ? ( | |
<div ref={ref}> | |
👋 Hey, I'm a modal. Click anywhere outside of me to close. | |
</div> | |
) : ( | |
<button onClick={() => setModalOpen(true)}>Open Modal</button> | |
)} | |
</div> | |
); | |
} | |
*/ | |
export function useOnClickOutside(ref, handler) { | |
useEffect( | |
() => { | |
const listener = event => { | |
// Do nothing if clicking ref's element or descendent elements | |
if (!ref.current || ref.current.contains(event.target)) { | |
return; | |
} | |
handler(event); | |
}; | |
document.addEventListener('mousedown', listener); | |
document.addEventListener('touchstart', listener); | |
return () => { | |
document.removeEventListener('mousedown', listener); | |
document.removeEventListener('touchstart', listener); | |
}; | |
}, | |
// Add ref and handler to effect dependencies | |
// It's worth noting that because passed in handler is a new ... | |
// ... function on every render that will cause this effect ... | |
// ... callback/cleanup to run every render. It's not a big deal ... | |
// ... but to optimize you can wrap handler in useCallback before ... | |
// ... passing it into this hook. | |
[ref, handler], | |
); | |
} |
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
import { useState, useEffect } from 'react'; | |
/** | |
* This hook allows you to easily detect when an element is | |
* visible on the screen as well as specify how much of the | |
* element should be visible before being considered on screen. | |
* Perfect for lazy loading images or triggering animations when | |
* the user has scrolled down to a particular section. | |
// Usage | |
function App() { | |
// Ref for the element that we want to detect whether on screen | |
const ref = useRef(); | |
// Call the hook passing in ref and root margin | |
// In this case it would only be considered onScreen if more ... | |
// ... than 300px of element is visible. | |
const onScreen = useOnScreen(ref, '-300px'); | |
return ( | |
<div> | |
<div style={{ height: '100vh' }}> | |
<h1>Scroll down to next section 👇</h1> | |
</div> | |
<div | |
ref={ref} | |
style={{ | |
height: '100vh', | |
backgroundColor: onScreen ? '#23cebd' : '#efefef' | |
}} | |
> | |
{onScreen ? ( | |
<div> | |
<h1>Hey I'm on the screen</h1> | |
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" /> | |
</div> | |
) : ( | |
<h1>Scroll down 300px from the top of this section 👇</h1> | |
)} | |
</div> | |
</div> | |
); | |
} | |
*/ | |
export function useOnScreen(ref, rootMargin = '0px'){ | |
// State and setter for storing whether element is visible | |
const [isIntersecting, setIntersecting] = useState(false); | |
useEffect(() => { | |
const observer = new IntersectionObserver( | |
([entry]) => { | |
// Update our state when observer callback fires | |
setIntersecting(entry.isIntersecting); | |
}, | |
{ | |
rootMargin | |
} | |
); | |
if (ref.current) { | |
observer.observe(ref.current); | |
} | |
return () => { | |
observer.unobserve(ref.current); | |
}; | |
}, []); // Empty array ensures that effect is only run on mount and unmount | |
return isIntersecting; | |
} |
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
/* | |
Returns a stateful value, persisted in localStorage, and a function to update it. | |
- Use the useState() hook to initialize the value to defaultValue. | |
- Use the useRef() hook to create a ref that will hold the name of the value in localStorage. | |
- Use 3 instances of the useEffect() hook for initialization, value change and name change respectively. | |
- When the component is first mounted, use Storage.getItem() to update value if there's a stored value or Storage.setItem() to persist the current value. | |
- When value is updated, use Storage.setItem() to store the new value. | |
- When name is updated, use Storage.setItem() to create the new key, update the nameRef and use Storage.removeItem() to remove the previous key from localStorage. | |
- Note: The hook is meant for use with primitive values (i.e. not objects) and doesn't account for changes to localStorage due to other code. Both of these issues can be easily handled (e.g. JSON serialization and handling the 'storage' event). | |
*/ | |
const usePersistedState = (name, defaultValue) => { | |
const [value, setValue] = React.useState(defaultValue); | |
const nameRef = React.useRef(name); | |
React.useEffect(() => { | |
try { | |
const storedValue = localStorage.getItem(name); | |
if (storedValue !== null) setValue(storedValue); | |
else localStorage.setItem(name, defaultValue); | |
} catch { | |
setValue(defaultValue); | |
} | |
}, []); | |
React.useEffect(() => { | |
try { | |
localStorage.setItem(nameRef.current, value); | |
} catch {} | |
}, [value]); | |
React.useEffect(() => { | |
const lastName = nameRef.current; | |
if (name !== lastName) { | |
try { | |
localStorage.setItem(name, value); | |
nameRef.current = name; | |
localStorage.removeItem(lastName); | |
} catch {} | |
} | |
}, [name]); | |
return [value, setValue]; | |
}; | |
// example | |
const MyComponent = ({ name }) => { | |
const [val, setVal] = usePersistedState(name, 10); | |
return ( | |
<input | |
value={val} | |
onChange={e => { | |
setVal(e.target.value); | |
}} | |
/> | |
); | |
}; | |
const MyApp = () => { | |
const [name, setName] = React.useState('my-value'); | |
return ( | |
<> | |
<MyComponent name={name} /> | |
<input | |
value={name} | |
onChange={e => { | |
setName(e.target.value); | |
}} | |
/> | |
</> | |
); | |
}; | |
ReactDOM.render(<MyApp />, document.getElementById('root')); |
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
import { useEffect, useRef } from 'react'; | |
/** | |
* One question that comes up a lot is "When using hooks how do I | |
* get the previous value of props or state?". With React class | |
* components you have the componentDidUpdate method which receives | |
* previous props and state as arguments or you can update an | |
* instance variable (this.previous = value) and reference it | |
* later to get the previous value. So how can we do this inside | |
* a functional component that doesn't have lifecycle methods or | |
* an instance to store values on? Hooks to the rescue! | |
* We can create a custom hook that uses the useRef hook internally | |
* for storing the previous value. See the recipe below with inline | |
* comments. You can also find this example in the official | |
* React Hooks FAQ (https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state) | |
// Usage | |
function App() { | |
// State value and setter for our example | |
const [count, setCount] = useState(0); | |
// Get the previous value (was passed into hook on last render) | |
const prevCount = usePrevious(count); | |
// Display both current and previous count value | |
return ( | |
<div> | |
<h1>Now: {count}, before: {prevCount}</h1> | |
<button onClick={() => setCount(count + 1)}>Increment</button> | |
</div> | |
); | |
} | |
*/ | |
export function usePrevious(value){ | |
// The ref object is a generic container whose current property is mutable ... | |
// ... and can hold any value, similar to an instance property on a class | |
const ref = useRef(); | |
// Store current value in ref | |
useEffect(() => { | |
ref.current = value; | |
}, [value]); // Only re-run if value changes | |
// Return previous value (happens before update in useEffect above) | |
return ref.current; | |
} |
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
/* | |
#javascript for a #reactjs hook that returns the previous | |
value. Keeps track of when the "value" argument changes | |
and returns it's previous value. Handy for comparing | |
state changes inside a component. | |
*/ | |
const usePrevious = (value) => { | |
const [[prev,current], setPrev] = useState([ | |
undefined, | |
value | |
]); | |
useEffect(()=> { | |
if(current !== value){ | |
setPrev([current,value]); | |
} | |
}, [current,value]); | |
return prev; | |
}; |
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
/* eslint-disable consistent-return */ | |
import { useEffect, useRef } from 'react'; | |
import { useHistory } from 'react-router-dom'; | |
import axios from 'axios'; | |
import store from 'index'; | |
import { | |
generateUid, | |
handleCookies, | |
instanaIneum, | |
objectConvertQueryString, | |
serviceDurationConverter, | |
} from 'utils/helpers'; | |
import { setResponseError } from 'components/common/serviceError/actions'; | |
import { setNotAllowedPageErrorMessage } from 'actions/common/sessionAction'; | |
import { RESPONSE_RESULT_CODES, RESPONSE_RESULT_TYPES, cookieNames, LANG_VALUES } from 'config/constants/appConstants'; | |
export const useRequestManagement = () => { | |
const history = useHistory(); | |
const requestList = useRef({}); // { [reqId]: { cancelId, isCancelable, cancel, cancelToken} }} | |
const generateUniqueRequestId = () => { | |
const idList = Object.keys(requestList.current); | |
let idString = generateUid(); | |
while (idList.includes(idString)) { | |
idString = generateUid(); | |
} | |
return idString; | |
}; | |
const apiServices = axios.create({ | |
withCredentials: true, | |
headers: { | |
common: {}, | |
}, | |
}); | |
apiServices.interceptors.request.use( | |
config => ({ ...config, startTime: performance.now() }), | |
error => Promise.reject(error), | |
); | |
apiServices.interceptors.response.use( | |
response => { | |
const finishTime = performance.now(); | |
removeRequestByRequestId(response?.config?.reqId); | |
const { responseType } = response.request; | |
if (responseType === 'arraybuffer') { | |
const byteArrLength = response.data ? response.data.byteLength : null; | |
let decodedString = null; | |
if (byteArrLength < 65535) { | |
decodedString = String.fromCharCode.apply(null, new Uint8Array(response.data)); | |
} | |
let responseObj = null; | |
try { | |
responseObj = JSON.parse(decodedString); | |
} catch (e) { | |
/**/ | |
} | |
if (responseObj && responseObj.body && responseObj.body['redirect-url']) { | |
window.location.href = responseObj.body['redirect-url']; | |
return Promise.reject(); | |
} | |
if (responseObj && responseObj.resultType !== RESPONSE_RESULT_TYPES.SUCCESS && responseObj.resultCode !== '0') { | |
store.dispatch(setResponseError(decodeURIComponent(escape(responseObj.resultDescription)))); | |
return Promise.reject(); | |
} | |
} | |
if (response && response.data) { | |
if ( | |
response.data.resultType === RESPONSE_RESULT_TYPES.AUTHORIZATION && | |
response.data.resultCode === RESPONSE_RESULT_CODES.AUTH000 && | |
response.data.body['redirect-url'] | |
) { | |
window.location.href = response.data.body['redirect-url']; | |
} | |
if (response.data.resultCode === RESPONSE_RESULT_CODES.MPAR001) { | |
store.dispatch(setNotAllowedPageErrorMessage(response.data.resultDescription)); | |
} | |
if ( | |
response.data.resultCode !== '0' && | |
response.data.resultCode !== RESPONSE_RESULT_CODES.AUTH000 && | |
response.data.resultCode !== RESPONSE_RESULT_CODES.MPAR001 && | |
response.data.resultDescription | |
) { | |
const resultDescription = String(response?.data?.resultDescription); | |
let showErrorModal = true; | |
if (response.config) { | |
if (response.config.method === 'get' && response.config.showErrorModal === false) { | |
showErrorModal = false; | |
} else if (response.config.method === 'post' && response.config.data) { | |
const parsed = response.config.data; | |
let parsedJson = {}; | |
if (typeof parsed === 'string') { | |
if (parsed.charAt(0) === '{' || parsed.charAt(parsed.length - 1) === '}') { | |
parsedJson = JSON.parse(parsed); | |
} | |
} else if (typeof parsed === 'object') { | |
parsedJson = parsed; | |
} | |
showErrorModal = !parsedJson.showErrorModal ? false : showErrorModal; | |
} | |
} | |
if (showErrorModal === true) { | |
store.dispatch(setResponseError(resultDescription)); | |
} | |
/* | |
instanaIneum('reportError', resultDescription, { | |
userId: store.getState().sessionReducer?.userData?.organizationId, | |
userName: store.getState().sessionReducer?.userData?.username, | |
serviceUrl: response?.config?.url, | |
responseDuration: serviceDurationConverter(response?.config?.startTime, finishTime), | |
resultCode: response?.data?.resultCode, | |
resultDescription, | |
}); | |
*/ | |
instanaIneum('reportError', resultDescription); | |
instanaIneum('reportEvent', resultDescription, { | |
timestamp: Date.now(), | |
duration: serviceDurationConverter(response?.config?.startTime, finishTime), | |
backendTraceId: String(response?.data?.resultCode), | |
componentStack: `Username: ${store.getState().sessionReducer?.userData?.username}, UserId: ${ | |
store.getState().sessionReducer?.userData?.organizationId | |
}`, | |
meta: { | |
state: response?.config?.url, | |
}, | |
}); | |
} | |
} | |
return Promise.resolve(response); | |
}, | |
error => { | |
const finishTime = performance.now(); | |
if (error && error.response) { | |
removeRequestByRequestId(error?.response?.config?.reqId); | |
const resultDescription = String(error?.response?.data?.error); | |
instanaIneum('reportError', resultDescription); | |
instanaIneum('reportEvent', resultDescription, { | |
timestamp: Date.now(), | |
duration: serviceDurationConverter(error?.response?.config?.startTime, finishTime), | |
backendTraceId: String(error?.response?.status), | |
componentStack: `Username: ${store.getState().sessionReducer?.userData?.username}, UserId: ${ | |
store.getState().sessionReducer?.userData?.organizationId | |
}`, | |
meta: { | |
state: error?.response?.config?.url, | |
}, | |
}); | |
/* | |
instanaIneum('reportError', resultDescription, { | |
userId: store.getState().sessionReducer?.userData?.organizationId, | |
userName: store.getState().sessionReducer?.userData?.username, | |
serviceUrl: error?.response?.config?.url, | |
responseDuration: serviceDurationConverter(error?.response?.config?.startTime, finishTime), | |
statusCode: error?.response?.status, | |
resultDescription, | |
}); | |
*/ | |
return Promise.reject(error); | |
} | |
}, | |
); | |
const serviceWrapper = ({ | |
servicePathObj = null, | |
params = {}, | |
options = {}, | |
prefix = '', | |
suffix = '', | |
isCancelable = true, | |
showErrorModal = true, | |
cancelId = null, | |
} = {}) => { | |
if (servicePathObj === null) { | |
return null; | |
} | |
let path = null; | |
const type = servicePathObj.type.toLowerCase(); | |
if (servicePathObj.fullPath && servicePathObj.fullPath.length > 0) { | |
path = servicePathObj.fullPath; | |
} else { | |
path = process.env.REACT_APP_API_DOMAIN + prefix + servicePathObj.path; | |
} | |
path += suffix; | |
if ((params && type === 'get') || (params && type === 'delete')) { | |
if (Object.entries(params).length > 0) { | |
const paramString = objectConvertQueryString(params); | |
path += paramString; | |
} | |
} | |
if (params === null) { | |
params = {}; | |
} | |
if (isCancelable) { | |
params.isCancelable = true; | |
} | |
params.showErrorModal = showErrorModal; | |
const systemLang = handleCookies.getCookieWithNativeJs(cookieNames.SYSTEM_LANGUAGE); | |
let requestLang = LANG_VALUES.tr_TR; | |
if (systemLang && String(systemLang) !== '') { | |
requestLang = systemLang === LANG_VALUES.tr ? LANG_VALUES.tr_TR : LANG_VALUES.en_US; | |
} | |
options = { | |
...options, | |
headers: { | |
...options?.headers, | |
common: { | |
'Accept-Language': requestLang, | |
}, | |
}, | |
}; | |
const reqId = generateUniqueRequestId(); | |
const source = axios.CancelToken.source(); | |
options.cancelToken = source.token; | |
options.reqId = reqId; | |
requestList.current = { | |
...requestList.current, | |
[reqId]: { cancelId, isCancelable, cancel: source.cancel, cancelToken: source.token }, | |
}; | |
if (type === 'get') { | |
return apiServices[type](path, options); | |
} | |
return apiServices[type](path, params, options); | |
}; | |
const removeRequestByRequestId = reqId => { | |
const newList = { ...requestList.current }; | |
delete newList[reqId]; | |
requestList.current = newList; | |
}; | |
const cancelAllRequestOnRoutingChanged = () => { | |
Object.values(requestList.current).forEach(reqItem => { | |
const { isCancelable, cancel, cancelToken } = reqItem; | |
if (isCancelable && typeof cancel === 'function' && cancelToken !== null) { | |
cancel(cancelToken); | |
} | |
}); | |
}; | |
const cancelRequestByCancelId = paramId => { | |
const currentList = { ...requestList.current }; | |
const keyList = Object.keys(currentList); | |
const newList = {}; | |
keyList.forEach(keyId => { | |
const { cancelId, cancel, cancelToken } = currentList[keyId]; | |
if (cancelId === paramId && typeof cancel === 'function' && cancelToken !== null) { | |
cancel(cancelToken); | |
} else { | |
newList[keyId] = currentList[keyId]; | |
} | |
}); | |
requestList.current = newList; | |
}; | |
useEffect(() => history.listen(cancelAllRequestOnRoutingChanged), [history]); | |
return { serviceWrapper, cancelRequestByCancelId }; | |
}; |
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
/* | |
A common need is a way to redirect the user if they are | |
signed out and trying to view a page that should require | |
them to be authenticated. This example shows how you can | |
easily compose our useAuth and useRouter hooks to create a new | |
useRequireAuth hook that does just that. Of course, this | |
functionality could be added directly to our useAuth hook, | |
but then we'd need to make that hook aware of our router logic. | |
Using the power of hook composition we can keep the other | |
two hooks as simple as possible and just utilize our new | |
useRequireAuth when redirection is needed. | |
*/ | |
import Dashboard from "./Dashboard.js"; | |
import Loading from "./Loading.js"; | |
import { useRequireAuth } from "./use-require-auth.js"; | |
function DashboardPage(props) { | |
const auth = useRequireAuth(); | |
// If auth is null (still fetching data) | |
// or false (logged out, above hook will redirect) | |
// then show loading indicator. | |
if (!auth) { | |
return <Loading />; | |
} | |
return <Dashboard auth={auth} />; | |
} | |
// Hook (use-require-auth.js) | |
import { useEffect } from "react"; | |
import { useAuth } from "./use-auth.js"; | |
import { useRouter } from "./use-router.js"; | |
function useRequireAuth(redirectUrl = "/signup") { | |
const auth = useAuth(); | |
const router = useRouter(); | |
// If auth.user is false that means we're not | |
// logged in and should redirect. | |
useEffect(() => { | |
if (auth.user === false) { | |
router.push(redirectUrl); | |
} | |
}, [auth, router]); | |
return auth; | |
} |
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
import { useMemo } from 'react'; | |
import { useParams, useLocation, useHistory, useRouteMatch } from 'react-router-dom'; | |
/** | |
* If you use React Router you might have noticed they recently | |
* added a number of useful hooks, specifically useParams, useLocation, | |
* useHistory, and use useRouteMatch. But let's see if we can make | |
* it even simpler by wrapping them up into a single useRouter hook | |
* that exposes just the data and methods we need. In this recipe we | |
* show how easy it is to compose multiple hooks and combine their | |
* returned state into a single object. It makes a lot of sense for | |
* libraries like React Router to offer a selection of low-level hooks, | |
* as using only the hook you need can minimize unnecessary re-renders. | |
* That said, sometimes you want a simpler developer experience and custom | |
* hooks make that easy. | |
// Usage | |
function MyComponent(){ | |
// Get the router object | |
const router = useRouter(); | |
// Get value from query string (?postId=123) or route param (/:postId) | |
console.log(router.query.postId); | |
// Get current pathname | |
console.log(router.pathname) | |
// Navigate with with router.push() | |
return ( | |
<button onClick={(e) => router.push('/about')}>About</button> | |
); | |
} | |
*/ | |
export function useRouter() { | |
const params = useParams(); | |
const location = useLocation(); | |
const history = useHistory(); | |
const match = useRouteMatch(); | |
// Return our custom router object | |
// Memoize so that a new object is only returned if something changes | |
return useMemo( | |
() => ({ | |
// For convenience add push(), replace(), pathname at top level | |
push: history.push, | |
replace: history.replace, | |
pathname: location.pathname, | |
// Merge params and parsed query string into single "query" object | |
// so that they can be used interchangeably. | |
// Example: /:topic?sort=popular -> { topic: "react", sort: "popular" } | |
query: { | |
// ...queryString.parse(location.search), // {tab: "daily-parameters"} or {} | |
...Object.fromEntries(new URLSearchParams(location.search)), | |
...params, | |
}, | |
// Include match, location, history objects so we have | |
// access to extra React Router functionality if needed. | |
match, | |
location, | |
history, | |
}), | |
[params, match, location, history], | |
); | |
} |
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
import { useSelector } from 'react-redux'; | |
import { saveAs } from 'file-saver'; | |
export const useSampleFileDownloader = () => { | |
const systemLang = useSelector(state => state.sessionReducer.locale); | |
const fileDownloader = (fileServerName, fileRecordName = null, lang = systemLang) => { | |
if (fileServerName === null) { | |
return null; | |
} | |
const fileName = fileRecordName === null ? fileServerName : fileRecordName; | |
const fileUrl = `${window.location.origin}/web/static/docs/${lang}/${fileServerName}`; | |
// console.log(fileUrl, fileName); | |
// sample url -> https://vep-qa.epias.com.tr/web/static/docs/en/tt01.xlsx | |
saveAs(fileUrl, fileName); | |
return null; | |
}; | |
return { fileDownloader }; | |
}; |
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
import { useState, useEffect } from 'react'; | |
/** | |
* This hook makes it super easy to dynamically load an external | |
* script and know when its loaded. This is useful when you need | |
* to interact with a 3rd party library (Stripe, Google Analytics, etc) | |
* and you'd prefer to load the script when needed rather then include | |
* it in the document head for every page request. In the example | |
* below we wait until the script has loaded successfully before | |
* calling a function declared in the script. If you're interested | |
* in seeing how this would look if implemented as a | |
* Higher Order Component then check out the source of | |
* react-script-loader-hoc (https://github.com/sesilio/react-script-loader-hoc/blob/master/src/index.js. I personally find it much more readable | |
* as a hook. Another advantage is because it's so easy to call the | |
* same hook multiple times to load multiple different scripts, | |
* unlike the HOC implementation, we can skip adding support for | |
* passing in multiple src strings. | |
// Usage | |
function App() { | |
const [loaded, error] = useScript( | |
'https://pm28k14qlj.codesandbox.io/test-external-script.js' | |
); | |
return ( | |
<div> | |
<div> | |
Script loaded: <b>{loaded.toString()}</b> | |
</div> | |
{loaded && !error && ( | |
<div> | |
Script function call response: <b>{TEST_SCRIPT.start()}</b> | |
</div> | |
)} | |
</div> | |
); | |
} | |
*/ | |
const cachedScripts = []; | |
export function useScript(src){ | |
// Keeping track of script loaded and error state | |
const [state, setState] = useState({ | |
loaded: false, | |
error: false | |
}); | |
useEffect( | |
() => { | |
// If cachedScripts array already includes src that means another instance ... | |
// ... of this hook already loaded this script, so no need to load again. | |
if (cachedScripts.includes(src)) { | |
setState({ | |
loaded: true, | |
error: false | |
}); | |
} else { | |
cachedScripts.push(src); | |
// Create script | |
const script = document.createElement('script'); | |
script.src = src; | |
script.async = true; | |
// Script event listener callbacks for load and error | |
const onScriptLoad = () => { | |
setState({ | |
loaded: true, | |
error: false | |
}); | |
}; | |
const onScriptError = () => { | |
// Remove from cachedScripts we can try loading again | |
const index = cachedScripts.indexOf(src); | |
if (index >= 0) cachedScripts.splice(index, 1); | |
script.remove(); | |
setState({ | |
loaded: true, | |
error: true | |
}); | |
}; | |
script.addEventListener('load', onScriptLoad); | |
script.addEventListener('error', onScriptError); | |
// Add script to document body | |
document.body.appendChild(script); | |
// Remove event listeners on cleanup | |
return () => { | |
script.removeEventListener('load', onScriptLoad); | |
script.removeEventListener('error', onScriptError); | |
}; | |
} | |
}, | |
[src] // Only re-run effect if script src changes | |
); | |
return [state.loaded, state.error]; | |
} |
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
import { useEffect, useState } from "react"; | |
export const scrollToTop = () => | |
window.scrollTo({ | |
top: 0, | |
behavior: "smooth" | |
}); | |
const throttle = (callback, sleepTime) => { | |
let time = Date.now(); | |
return (...args) => { | |
if (time + sleepTime - Date.now() < 0) { | |
callback(...args); | |
time = Date.now(); | |
} | |
}; | |
}; | |
export const useScroll = () => { | |
const [scrollPosition, setScrollPosition] = useState(window.scrollY); | |
const updateScrollPosition = throttle(() => { | |
setScrollPosition(window.scrollY); | |
}, 100); | |
useEffect(() => { | |
window.addEventListener("scroll", updateScrollPosition); | |
return () => window.removeEventListener("scroll", updateScrollPosition); | |
}, [updateScrollPosition]); | |
return scrollPosition; | |
}; |
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
import { useCallback } from 'react'; | |
const useScrollLock = () => { | |
const lockScroll = useCallback(() => { | |
document.body.style.overflow = 'hidden'; | |
document.body.style.position = 'relative'; | |
}, []); | |
const unlockScroll = useCallback(() => { | |
document.body.style.overflow = ''; | |
document.body.style.position = ''; | |
}, []); | |
return { | |
lock:lockScroll, | |
unlock:unlockScroll, | |
}; | |
}; | |
export { useScrollLock }; |
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
/* | |
This hook is part of the react-spring animation library | |
which allows for highly performant physics-based animations. | |
I try to avoid including dependencies in these recipes, but once | |
in awhile I'm going to make an exception for hooks that expose the | |
functionality of really useful libraries. One nice thing about | |
react-spring is that it allows you to completely skip the React | |
render cycle when applying animations, often giving a pretty | |
substantial performance boost. In our recipe below we render | |
a row of cards and apply a springy animation effect related | |
to the mouse position over any given card. To make this work we | |
call the useSpring hook with an array of values we want to animate, | |
render an animated.div component (exported by react-spring), | |
get the mouse position over a card with the onMouseMove event, | |
then call setAnimatedProps (function returned by the hook) to update | |
that set of values based on the mouse position. Read through the | |
comments in the recipe below for more details or jump right over to the | |
CodeSandbox demo. I liked this effect so much I ended up using it on | |
my startup's landing page 😎 | |
*/ | |
import { useState, useRef } from "react"; | |
import { useSpring, animated } from "react-spring"; | |
// Displays a row of cards | |
// Usage of hook is within <Card> component below | |
function App() { | |
return ( | |
<div className="container"> | |
<div className="row"> | |
{cards.map((card, i) => ( | |
<div className="column"> | |
<Card> | |
<div className="card-title">{card.title}</div> | |
<div className="card-body">{card.description}</div> | |
<img className="card-image" src={card.image} /> | |
</Card> | |
</div> | |
))} | |
</div> | |
</div> | |
); | |
} | |
function Card({ children }) { | |
// We add this ref to card element and use in onMouseMove event ... | |
// ... to get element's offset and dimensions. | |
const ref = useRef(); | |
// Keep track of whether card is hovered so we can increment ... | |
// ... zIndex to ensure it shows up above other cards when animation causes overlap. | |
const [isHovered, setHovered] = useState(false); | |
// The useSpring hook | |
const [animatedProps, setAnimatedProps] = useSpring(() => { | |
return { | |
// Array containing [rotateX, rotateY, and scale] values. | |
// We store under a single key (xys) instead of separate keys ... | |
// ... so that we can use animatedProps.xys.interpolate() to ... | |
// ... easily generate the css transform value below. | |
xys: [0, 0, 1], | |
// Setup physics | |
config: { mass: 10, tension: 400, friction: 40, precision: 0.00001 }, | |
}; | |
}); | |
return ( | |
<animated.div | |
ref={ref} | |
className="card" | |
onMouseEnter={() => setHovered(true)} | |
onMouseMove={({ clientX, clientY }) => { | |
// Get mouse x position within card | |
const x = | |
clientX - | |
(ref.current.offsetLeft - | |
(window.scrollX || window.pageXOffset || document.body.scrollLeft)); | |
// Get mouse y position within card | |
const y = | |
clientY - | |
(ref.current.offsetTop - | |
(window.scrollY || window.pageYOffset || document.body.scrollTop)); | |
// Set animated values based on mouse position and card dimensions | |
const dampen = 50; // Lower the number the less rotation | |
const xys = [ | |
-(y - ref.current.clientHeight / 2) / dampen, // rotateX | |
(x - ref.current.clientWidth / 2) / dampen, // rotateY | |
1.07, // Scale | |
]; | |
// Update values to animate to | |
setAnimatedProps({ xys: xys }); | |
}} | |
onMouseLeave={() => { | |
setHovered(false); | |
// Set xys back to original | |
setAnimatedProps({ xys: [0, 0, 1] }); | |
}} | |
style={{ | |
// If hovered we want it to overlap other cards when it scales up | |
zIndex: isHovered ? 2 : 1, | |
// Interpolate function to handle css changes | |
transform: animatedProps.xys.interpolate( | |
(x, y, s) => | |
`perspective(600px) rotateX(${x}deg) rotateY(${y}deg) scale(${s})` | |
), | |
}} | |
> | |
{children} | |
</animated.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
// A reactjs hook the records the history of state changes as an Array buffer upto a certain length. | |
export const useStateBuffer = (size, initialState) => { | |
const [values, setState] => useState([initialState]); | |
const setValue = useCallback( | |
(next) => setState([...values,next].slice(0,size)), | |
[values] | |
); | |
return [values,setValue]; | |
} |
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
// A reactjs hook the records the history of state changes | |
//as an Array buffer upto a certain length. | |
export const useStateBuffer = (size, init) => { | |
const [values, setState] => useState([init]); | |
const setValue = useCallback( | |
(val) => setState( | |
(prev) => ([...prev,val].slice(0,size)) | |
),[] | |
); | |
return [values,setValue]; | |
} |
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
// A reactjs hook that increments a counter every time useState() is changed. | |
export const useStateCount = (initialState) => { | |
const [[count,value],setState] = useState([0, initialState]); | |
const setValue = useCallback( | |
(next) => setState([count+1,next]), | |
[count] | |
); | |
return [count,value,setValue]; | |
} |
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
// A reactjs hook that debounces useState() changes. | |
//A modified hook from my previous debounce hook that | |
//uses a [value, setValue] pair instead of an input parameter. | |
export const useStateDebounce = () => { | |
const [state,setState] = useState(initialState); | |
const ref = useRef(0); | |
const setValue = useCallback(value => { | |
window.clearTimeout(ref.current); | |
ref.current = window.setTimeout(()=> setState(value),ms); | |
}, [ms]); | |
useEffect(() => () => window.clearTimeout(ref.current)); | |
return [state,setValue]; | |
} |
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
// A reactjs hook that stores a state value as a JSON string to | |
//prevent render updates when an object value is mutated but doesn't change. | |
export const useStateJson = (initialState) => { | |
const [json, setState] => useState( | |
JSON.stringify(initialState ?? '') | |
); | |
const value = useMemo(()=>JSON.parse(json),[json]); | |
const setValue = useCallback( | |
(next) => setState(JSON.stringify(next ?? '')), | |
[] | |
); | |
return [value,setValue]; | |
} |
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
// A reactjs hook that emits the previous and current values of useState(). | |
export const useStatePair = (initialState) => { | |
const [[prev,current], setPair] = useState([undefined,initialState]); | |
const setValue = useCallback( | |
next => setPair([current,next]), | |
[current] | |
); | |
return [prev,current,setValue]; | |
} |
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
// A reactjs hook that persists useState() to localStorage. | |
export const useStateStorage = (key, defaultValue) => { | |
const [value, setState] = useState(defaultState); | |
useEffect(()=>{ | |
const store = localStorage.getItem(key); | |
if(store !== null){ | |
try{ | |
setState(JSON.parse(store)); | |
} catch (err) { | |
localStorage.removeItem(key); | |
} | |
} | |
}, [key]); | |
const setValue = useCallback( | |
newValue => { | |
setState(newValue); | |
localStorage.setItem(key, JSON.stringify(newValue)); | |
}, | |
[key] | |
); | |
return [value,setValue]; | |
} |
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
/* | |
This hook makes it easy to dynamically change the appearance | |
of your app using CSS variables. You simply pass in an object | |
containing key/value pairs of the CSS variables you'd like | |
to update and the hook updates each variable in the document's | |
root element. This is useful in situations where you can't | |
define styles inline (no pseudo class support) and there are | |
too many style permutations to include each theme in your stylesheet | |
(such as a web app that lets users customize the look of their profile). | |
It's worth noting that many css-in-js libraries support dynamic styles | |
out of the box, but it's interesting to experiment with how this can | |
be done with just CSS variables and a React Hook. The example below is | |
intentionally very simple, but you could imagine the theme object being | |
stored in state or fetched from an API. Be sure to check out the CodeSandbox | |
demo for a more interesting example and to see the accompanying stylesheet. | |
*/ | |
import { useLayoutEffect } from "react"; | |
import "./styles.scss"; // -> https://codesandbox.io/s/15mko9187 | |
// Usage | |
const theme = { | |
"button-padding": "16px", | |
"button-font-size": "14px", | |
"button-border-radius": "4px", | |
"button-border": "none", | |
"button-color": "#FFF", | |
"button-background": "#6772e5", | |
"button-hover-border": "none", | |
"button-hover-color": "#FFF", | |
}; | |
function App() { | |
useTheme(theme); | |
return ( | |
<div> | |
<button className="button">Button</button> | |
</div> | |
); | |
} | |
// Hook | |
function useTheme(theme) { | |
useLayoutEffect( | |
() => { | |
// Iterate through each value in theme object | |
for (const key in theme) { | |
// Update css variables in document's root element | |
document.documentElement.style.setProperty(`--${key}`, theme[key]); | |
} | |
}, | |
[theme] // Only call again if theme object reference changes | |
); | |
} |
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
// A simple hook that sets the HTML page title in reactjs. | |
export const useTitle = (title) => { | |
useEffect(()=>{ | |
document.title = title; | |
}, [title]); | |
}; |
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
/* | |
Basically, what this hook does is that, it takes a parameter with value true | |
or false and toggles that value to opposite. It's useful when we want to take | |
some action into it's opposite action, for example: show and hide modal, | |
show more/show less text, open/close side menu. | |
*/ | |
import { useCallback, useState } from 'react'; | |
// Usage | |
/* | |
function App() { | |
// Call the hook which returns, current value and the toggler function | |
const [isTextChanged, setIsTextChanged] = useToggle(); | |
return ( | |
<button onClick={setIsTextChanged}>{isTextChanged ? 'Toggled' : 'Click to Toggle'}</button> | |
); | |
} | |
*/ | |
// Hook | |
// Parameter is the boolean, with default "false" value | |
const useToggle = (initialState = false) => { | |
// Initialize the state | |
const [state, setState] = useState(initialState); | |
// Define and memorize toggler function in case we pass down the comopnent, | |
// This function change the boolean value to it's opposite value | |
const toggle = useCallback(() => setState(state => !state), []); | |
return [state, toggle] | |
} |
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
/* | |
This hook makes it easy to see which prop changes are | |
causing a component to re-render. If a function is | |
particularly expensive to run and you know it renders | |
the same results given the same props you can use the React. | |
memo higher order component, as we've done with the Counter | |
component in the below example. In this case if you're still | |
seeing re-renders that seem unnecessary you can drop in the | |
useWhyDidYouUpdate hook and check your console to see which | |
props changed between renders and view their previous/current | |
values. Pretty nifty huh? | |
A huge thanks to Bruno Lemos for the idea and original code. | |
You can also see it in action in the CodeSandbox demo. | |
*/ | |
import { useState, useEffect, useRef } from "react"; | |
// Let's pretend this <Counter> component is expensive to re-render so ... | |
// ... we wrap with React.memo, but we're still seeing performance issues :/ | |
// So we add useWhyDidYouUpdate and check our console to see what's going on. | |
const Counter = React.memo((props) => { | |
useWhyDidYouUpdate("Counter", props); | |
return <div style={props.style}>{props.count}</div>; | |
}); | |
function App() { | |
const [count, setCount] = useState(0); | |
const [userId, setUserId] = useState(0); | |
// Our console output tells use that the style prop for <Counter> ... | |
// ... changes on every render, even when we only change userId state by ... | |
// ... clicking the "switch user" button. Oh of course! That's because the | |
// ... counterStyle object is being re-created on every render. | |
// Thanks to our hook we figured this out and realized we should probably ... | |
// ... move this object outside of the component body. | |
const counterStyle = { | |
fontSize: "3rem", | |
color: "red", | |
}; | |
return ( | |
<div> | |
<div className="counter"> | |
<Counter count={count} style={counterStyle} /> | |
<button onClick={() => setCount(count + 1)}>Increment</button> | |
</div> | |
<div className="user"> | |
<img src={`http://i.pravatar.cc/80?img=${userId}`} /> | |
<button onClick={() => setUserId(userId + 1)}>Switch User</button> | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useWhyDidYouUpdate(name, props) { | |
// Get a mutable ref object where we can store props ... | |
// ... for comparison next time this hook runs. | |
const previousProps = useRef(); | |
useEffect(() => { | |
if (previousProps.current) { | |
// Get all keys from previous and current props | |
const allKeys = Object.keys({ ...previousProps.current, ...props }); | |
// Use this object to keep track of changed props | |
const changesObj = {}; | |
// Iterate through keys | |
allKeys.forEach((key) => { | |
// If previous is different from current | |
if (previousProps.current[key] !== props[key]) { | |
// Add to changesObj | |
changesObj[key] = { | |
from: previousProps.current[key], | |
to: props[key], | |
}; | |
} | |
}); | |
// If changesObj not empty then output to console | |
if (Object.keys(changesObj).length) { | |
console.log("[why-did-you-update]", name, changesObj); | |
} | |
} | |
// Finally update previousProps with current props for next hook call | |
previousProps.current = props; | |
}); | |
} |
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
import { useState, useEffect } from 'react'; | |
/** | |
* A really common need is to get the current size of the browser | |
* window. This hook returns an object containing the window's | |
* width and height. If executed server-side (no window object) | |
* the value of width and height will be undefined. | |
* | |
// Usage | |
function App() { | |
const size = useWindowSize(); | |
return ( | |
<div> | |
{size.width}px / {size.height}px | |
</div> | |
); | |
} | |
*/ | |
export function useWindowSize() { | |
const isClient = typeof window === 'object'; | |
function getSize() { | |
return { | |
width: isClient ? window.innerWidth : undefined, | |
height: isClient ? window.innerHeight : undefined | |
}; | |
} | |
const [windowSize, setWindowSize] = useState(getSize); | |
useEffect(() => { | |
if (!isClient) { | |
return false; | |
} | |
function handleResize() { | |
setWindowSize(getSize()); | |
} | |
window.addEventListener('resize', handleResize); | |
return () => window.removeEventListener('resize', handleResize); | |
}, []); // Empty array ensures that effect is only run on mount and unmount | |
return windowSize; | |
} |
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
// Want to know if the visitor switched tabs in Chrome while using your reactjs app? | |
// Here's a hook that calls a onChanged(boolean) function when the window loses or gains focus. | |
export const useWinFocus = (onChanged, deps) => { | |
const callback = useCallback(onChanged, deps); | |
useEffect(()=>{ | |
const onFocus = () => callback(true); | |
const onBlur = () => callback(false); | |
window.addEventListener('focus', onFocus); | |
window.addEventListener('blur', onBlur); | |
return () => { | |
window.removeEventListener('focus', onFocus); | |
window.removeEventListener('blur', onBlur); | |
} | |
}, [callback]); | |
}; |
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
//reactjs hook that emits when the window changes focus. You can use this | |
//to protect sensitive user information, by hiding data when the window | |
//loses focus and prompting for their | |
//password when it regains focus. | |
export const useWndFocus = () => { | |
const [focus,setFocus] = useState(document.hasFocus()); | |
useEffect(()=>{ | |
const onFocus = () => setFocus(true); | |
const onBlur = () => setFocus(false); | |
window.addEventListener('focus',onFocus); | |
window.addEventListener('blur',onBlur); | |
return () => { | |
window.removeEventListener('focus',onFocus); | |
window.removeEventListener('blue',onBlur); | |
}; | |
}, []); | |
return focus; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment