In this lesson, we will learn how to create our own custom hook. We will use a custom hook to store values to localStorage
.
Let's create a folder named hooks
inside your src
folder under the smarter-tasks
project that you created in the last level. Create a file named useLocalStorage.ts
inside the hooks
subfolder.
Action: Create a file named
hooks/useLocalStorage.ts
insidewd301/smarter-tasks/src/hooks
Let's add a helper function to extract value stored in localStorage
.
const getStoredValue = (key: string, defaultValue: any) => {
const savedItem = localStorage.getItem(key);
if (savedItem) {
return JSON.parse(savedItem);
}
return defaultValue;
};
Now, we can write our hook. Let's call it useLocalStorage
. We will use a state
to store the values, and an effect
which would trigger whenever the state
changes. We will then return the state
and setter
function. That is what hooks usually do. Now, this can be used in components to get the value or set the value.
export const useLocalStorage = (key: string, defaultValue: any) => {
const [value, setValue] = useState(() => {
return getStoredValue(key, defaultValue);
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Now, we use any
as the type of value which is being saved. It breaks the type safety which TypeScript provides. We are going to make it type safe. We have already used a pattern before. If you look at TaskApp.tsx
Action: Switch to
TaskApp.tsx
file.
React.useState<TaskAppState>;
We provided TaskAppState
as the type for the state. This is made possible with a concept called generics
. It simply means, when we write a piece of code, we won't limit it to a particular type, instead we provide a placeholder. Then whenever this function is used in other part of the code, a type will be specified, and it will be used. So if you look at the declaration of the useState
,
Action: Command + click on
React.useState
to show the TS declaration file.
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
We can see this weird looking syntax with angle brackets. S
is a placeholder. When we invoke, useState
, we have to provide it. Like, we provided TaskAppState
.
Action: open https://www.typescriptlang.org/docs/handbook/2/generics.html in browser, highlight the examples.
This is how we declare a generic function.
Action: also google how to add generic parameter to arrow function TypeScript. https://stackoverflow.com/a/45576880/2861108
So for an arrow function, we have to place the placeholder after the equal sign. E.g.:
const foo = <T,>(x: T) => x;
Let's make our hook also a generic one. We will first add a placeholder to the getStoredValue
function. The default value will also have the type T
. T is just an arbitrary character short for Type
. You can use a word instead of a single letter. The function also returns a value of type T
.
const getStoredValue = <T>(key: string, defaultValue: T): T => {
const savedItem = localStorage.getItem(key);
if (savedItem) {
return JSON.parse(savedItem);
}
return defaultValue;
};
Now, we will do the same with useLocalStorage
. The return value of the function will be a pair, which has a first element of type T
, and the second item is a setter
function. We can look up the type for it by going to the TypeScript declaration file for react.
export const useLocalStorage = <T,>(
key: string,
defaultValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] => {
const [value, setValue] = useState(() => {
return getStoredValue(key, defaultValue);
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Now, we will replace the state with our custom hook. Let's import it first.
import { useLocalStorage } from "./hooks/useLocalStorage";
Instead of useState
, let's use useLocalStorage
. We have provided the key as tasks
.
const [taskAppState, setTaskAppState] = useLocalStorage<TaskAppState>("tasks", {
tasks: [],
});
Let's test it. We will open the developer console. Now, there are no entries in the localStorage
Action: Open http://localhost:5173 in browser
Adding an item is working fine. It also got stored in localStorage
. Now if we close the tab and reopen it
Action: close the tab, reopen and visit http://localhost:5173
The items are being populated from localStorage
.
Let's remove any unused code from our component.
Action: Remove the
useEffect
example code.
See you in the next lesson.
Reusing logic with custom hooks, accessed on: April 04, 2023, URL:https://beta.reactjs.org/learn/reusing-logic-with-custom-hooks
TypeScript Generics, accessed on: April 04, 2023, URL:https://www.typescriptlang.org/docs/handbook/2/generics.html