useState является одним из основных хуков в React. Но с помощью useState вы не можете сохранить состояние постоянным. Когда пользователь обновляет страницу, состояние исчезает. Как же сохранить постоянные данные/состояние в React? Мы можем написать пользовательский хук, который сохраняет данные.
Покажите мне код
usePersist.ts
import { useCallback, useState } from "react";
interface UsePersistProps<T> {
stateName: string;
initialValue: T;
}
const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>): [T, (value: T) => void] => {
const name = `persist/${stateName}`;
const getFromStorage = <T>(name: string, defaultValue?: T) => {
try {
const val = JSON.parse(localStorage.getItem(name) + "");
if (val !== null) {
return val;
} else {
localStorage.setItem(name, JSON.stringify(defaultValue));
}
} catch {
return defaultValue;
}
};
const [state, setState] = useState<T>(getFromStorage<T>(name, initialValue));
const setValue = useCallback(
(value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
console.log(name, value);
},
[name]
);
return [state, setValue];
};
export default usePersist;
Usage
const [persistedState, setPersistedState] = usePersist<string>({
stateName: "myPersistedState",
initialValue: "Hello World",
});
Как?
Хорошо, приведенный выше код может показаться запутанным. Возможно, я что-то напутал, а возможно, это идеальное решение для данной конкретной задачи. Судить вам.
Пользовательский хук сохраняет состояние в localStorage
и возвращает его, когда это необходимо. Вот, в принципе, и все.
Давайте перепишем его шаг за шагом, чтобы лучше его понять.
Шаг 1
Мы должны дать имя для сохранения данных в localStorage
. Мы также можем задать начальное значение для пользовательского хука, как мы это делаем для useState
. Как и в useState
, мы также можем захотеть узнать тип данных, которые мы собираемся сохранить. Для этого мы можем использовать дженерики.
interface UsePersistProps<T> {
stateName: string;
initialValue: T;
}
const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
const name = `persist/${stateName}`;
const setValue = (value: T) => {};
};
export default usePersist;
Шаг 2
Давайте начнем писать логику набора. Сначала сохраним данные в useState
.
import { useState } from "react";
interface UsePersistProps<T> {
stateName: string;
initialValue: T;
}
const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
const name = `persist/${stateName}`;
const [state, setState] = useState<T>(initialValue);
};
export default usePersist;
Перейдем к логике установки. Как вы уже догадались, мы сохраняем данные в localStorage
. Но я также хочу сохранить данные в useState
. Таким образом, нам не придется считывать данные из localStorage, чтобы вернуть их обратно.
const setValue = (value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
};
Все довольно просто, верно? Однако у нас возникнет проблема бесконечного цикла рендеринга, если мы не обернем это внутри useCallback
. React не знает, изменится ли функция setValue
или нет. Но мы знаем. Мы можем пропустить добавление функции в массив зависимостей, когда используем ее внутри useEffect
, но eslint будет нас раздражать.
Дополнительная информация:
https://reactjs.org/docs/hooks-reference.html#usecallback
https://github.com/facebook/react/issues/14920
Давайте обернем ее внутри хука useCallback
и передадим зависимость name
, даже если мы знаем, что не будем менять имя.
const setValue = useCallback(
(value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
},
[name]
);
Шаг 3
Давайте напишем логику получения.
const getFromStorage = () => {
try {
const val = JSON.parse(localStorage.getItem(name) + "");
if (val !== null) {
return val;
} else {
localStorage.setItem(name, JSON.stringify(initialValue));
}
} catch {
return initialValue;
}
};
По сути, мы пытаемся получить данные из localStorage
. Если данных не существует, то мы сохраняем их в localStorage
. Код обернут внутри блока try-catch на случай, если данные не могут быть разобраны. Если это произойдет, код возвращает initialValue
.
Шаг 4
Завершим работу над кодом
Поместите функцию getFromStorage
над useState
.
Передайте вызов функции getFromStorage()
в useState
следующим образом
const [state, setState] = useState<T>(getFromStorage());
Теперь это должно выглядеть следующим образом
import { useCallback, useState } from "react";
interface UsePersistProps<T> {
stateName: string;
initialValue: T;
}
const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>) => {
const name = `persist/${stateName}`;
const getFromStorage = () => {
try {
const val = JSON.parse(localStorage.getItem(name) + "");
if (val !== null) {
return val;
} else {
localStorage.setItem(name, JSON.stringify(initialValue));
}
} catch {
return initialValue;
}
};
const [state, setState] = useState<T>(getFromStorage());
const setValue = useCallback(
(value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
},
[name]
);
};
export default usePersist;
Теперь давайте вернем функции set и get, как это делает React для useState
.
return [state, setValue];
Теперь окончательный код должен выглядеть следующим образом
import { useCallback, useState } from "react";
interface UsePersistProps<T> {
stateName: string;
initialValue: T;
}
const usePersist = <T>({ stateName, initialValue }: UsePersistProps<T>): [T, (value: T) => void] => {
const name = `persist/${stateName}`;
const getFromStorage = () => {
try {
const val = JSON.parse(localStorage.getItem(name) + "");
if (val !== null) {
return val;
} else {
localStorage.setItem(name, JSON.stringify(initialValue));
}
} catch {
return initialValue;
}
};
const [state, setState] = useState<T>(getFromStorage());
const setValue = useCallback(
(value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
},
[name]
);
return [state, setValue];
};
export default usePersist;
Использование
Давайте используем его внутри компонента
function App() {
const [persistentState, setPersistentState] = usePersist<string>({
stateName: "myState",
initialValue: "Hello World",
});
useEffect(() => {
setPersistentState("Hello, I'm persistent");
}, [setPersistentState]);
useEffect(() => {
console.log(persistentState);
}, [persistentState]);
return (
<>
<p>{persistentState}</p>
</>
);
}
Вы можете убедиться в его работоспособности, проверив консоль разработчика. Вы также можете удалить запись в вашем localStorage
.
Вы также можете использовать функцию usePersist в Context API.
Я использую его для переключения между темным и светлым режимами.
import { usePersist } from "hooks";
import { createContext, FC, useEffect, useState } from "react";
interface DarkModeContextState {
darkMode: boolean;
setDarkMode: (darkMode: boolean) => void;
}
const contextDefaultValues: DarkModeContextState = {
darkMode: true,
setDarkMode: () => {},
};
export const DarkModeContext = createContext<DarkModeContextState>(contextDefaultValues);
const DarkModeProvider: FC = ({ children }) => {
const [persistedDarkMode, setPersistedDarkMode] = usePersist<boolean>({
stateName: "darkMode",
initialValue: contextDefaultValues.darkMode,
});
const [darkMode, setDarkMode] = useState<boolean>(persistedDarkMode);
useEffect(() => {
setPersistedDarkMode(darkMode);
}, [darkMode, setPersistedDarkMode]);
return (
<DarkModeContext.Provider
value={{
darkMode,
setDarkMode: (val: boolean) => {
setDarkMode(val);
},
}}
>
{children}
</DarkModeContext.Provider>
);
};
export default DarkModeProvider;
Заключение
Спасибо, что дочитали до этого момента.
Вы можете спросить
«Почему бы вам не использовать пакет для работы с этим?».
Конечно, можно. Но я хотел дать представление о том, как решить довольно простую проблему. Я предпочитаю понимать решение, которое я использую.
«Почему бы нам не устанавливать и получать данные из локального хранилища прямо внутри компонента?».
Это должно работать, но я хотел выбрать более элегантное решение.
Если у вас есть еще вопросы или какие-либо замечания, пожалуйста, дайте мне знать. Надеюсь, это может быть решением вашей проблемы и/или даст вам представление о том, как написать пользовательский хук.