Как персистентно хранить состояние в React? [usePersist].

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;
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Спасибо, что дочитали до этого момента.

Вы можете спросить

«Почему бы вам не использовать пакет для работы с этим?».
Конечно, можно. Но я хотел дать представление о том, как решить довольно простую проблему. Я предпочитаю понимать решение, которое я использую.

«Почему бы нам не устанавливать и получать данные из локального хранилища прямо внутри компонента?».
Это должно работать, но я хотел выбрать более элегантное решение.

Если у вас есть еще вопросы или какие-либо замечания, пожалуйста, дайте мне знать. Надеюсь, это может быть решением вашей проблемы и/или даст вам представление о том, как написать пользовательский хук.

Оцените статью
Procodings.ru
Добавить комментарий