Giter VIP home page Giter VIP logo

use-local-storage-state's Introduction

use-local-storage-state

React hook that persist data in localStorage

Downloads Gzipped Size Build Status

Install

React 18 and above:

npm install use-local-storage-state

⚠️ React 17 and below. For docs, go to the react-17 branch.

npm install use-local-storage-state@17

Why

  • Actively maintained for the past 4 years — see contributors page.
  • Production ready.
  • React 18 concurrent rendering support.
  • SSR support.
  • Handles the Window storage event and updates changes across browser tabs, windows, and iframe's. Disable with storageSync: false.
  • In-memory fallback when localStorage throws an error and can't store the data. Provides a isPersistent API to let you notify the user their data isn't currently being stored.
  • Aiming for high-quality with my open-source principles.

Usage

import useLocalStorageState from 'use-local-storage-state'

export default function Todos() {
    const [todos, setTodos] = useLocalStorageState('todos', {
        defaultValue: ['buy avocado', 'do 50 push-ups']
    })
}
Todo list example + CodeSandbox link

You can experiment with the example here.

import React, { useState } from 'react'
import useLocalStorageState from 'use-local-storage-state'

export default function Todos() {
    const [todos, setTodos] = useLocalStorageState('todos', {
        defaultValue: ['buy avocado']
    })
    const [query, setQuery] = useState('')

    function onClick() {
        setQuery('')
        setTodos([...todos, query])
    }

    return (
        <>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <button onClick={onClick}>Create</button>
            {todos.map(todo => (
                <div>{todo}</div>
            ))}
        </>
    )
}
Notify the user when localStorage isn't saving the data using the isPersistent property

There are a few cases when localStorage isn't available. The isPersistent property tells you if the data is persisted in localStorage or in-memory. Useful when you want to notify the user that their data won't be persisted.

import React, { useState } from 'react'
import useLocalStorageState from 'use-local-storage-state'

export default function Todos() {
    const [todos, setTodos, { isPersistent }] = useLocalStorageState('todos', {
        defaultValue: ['buy avocado']
    })

    return (
        <>
            {todos.map(todo => (<div>{todo}</div>))}
            {!isPersistent && <span>Changes aren't currently persisted.</span>}
        </>
    )
}
Removing the data from localStorage and resetting to the default

The removeItem() method will reset the value to its default and will remove the key from the localStorage. It returns to the same state as when the hook was initially created.

import useLocalStorageState from 'use-local-storage-state'

export default function Todos() {
    const [todos, setTodos, { removeItem }] = useLocalStorageState('todos', {
        defaultValue: ['buy avocado']
    })

    function onClick() {
        removeItem()
    }
}
Why my component renders twice?

If you are hydrating your component (for example, if you are using Next.js), your component might re-render twice. This is behavior specific to React and not to this library. It's caused by the useSyncExternalStore() hook. There is no workaround. This has been discussed in the issues: #56.

If you want to know if you are currently rendering the server value you can use this helper function:

function useIsServerRender() {
  return useSyncExternalStore(() => {
    return () => {}
  }, () => false, () => true)
}

API

useLocalStorageState(key: string, options?: LocalStorageOptions)

Returns [value, setValue, { removeItem, isPersistent }] when called. The first two values are the same as useState(). The third value contains two extra properties:

  • removeItem() — calls localStorage.removeItem(key) and resets the hook to it's default state
  • isPersistentboolean property that returns false if localStorage is throwing an error and the data is stored only in-memory

key

Type: string

The key used when calling localStorage.setItem(key) and localStorage.getItem(key).

⚠️ Be careful with name conflicts as it is possible to access a property which is already in localStorage that was created from another place in the codebase or in an old version of the application.

options.defaultValue

Type: any

Default: undefined

The default value. You can think of it as the same as useState(defaultValue).

options.storageSync

Type: boolean

Default: true

Setting to false doesn't subscribe to the Window storage event. If you set to false, updates won't be synchronized across tabs, windows and iframes.

options.serializer

Type: { stringify, parse }

Default: JSON

JSON does not serialize Date, Regex, or BigInt data. You can pass in superjson or other JSON-compatible serialization library for more advanced serialization.

Related

use-local-storage-state's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

use-local-storage-state's Issues

Next.js build error: targets must start with "./"

unhandledRejection Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config <projectRoot>/node_modules/use-local-storage-state/package.json imported from <projectRoot>/.next/server/pages/_document.js; targets must start with "./"
    at new NodeError (node:internal/errors:405:5)
    at invalidPackageTarget (node:internal/modules/esm/resolve:387:10)
    at resolvePackageTargetString (node:internal/modules/esm/resolve:443:11)
    at resolvePackageTarget (node:internal/modules/esm/resolve:520:12)
    at packageExportsResolve (node:internal/modules/esm/resolve:633:27)
    at packageResolve (node:internal/modules/esm/resolve:870:14)
    at moduleResolve (node:internal/modules/esm/resolve:936:20)
    at defaultResolve (node:internal/modules/esm/resolve:1129:11)
    at nextResolve (node:internal/modules/esm/loader:163:28)
    at ESMLoader.resolve (node:internal/modules/esm/loader:835:30) {
  type: 'Error',
  code: 'ERR_INVALID_PACKAGE_TARGET'
}

ENV:

package.json type: module
packageManager: [email protected]
next: ^13.4.16
use-local-storage-state: ^19.0.2

Hooks sets localStorage value to string 'undefined' rather than doing nothing.

const [storedData, setStoredData, { removeItem }] = useLocalStorageState("testing");
const value = localStorage.getItem('testing')
console.log(typeof value);
console.log(value)

this outputs

string
undefined

this should ideally output

object
null

Why is the hook updating local storage when the client code never instructs it to do so?

Unable to use with SSR via Next.js

Trying to use this hook with Next.js and due to server side rendering it errors out on first load.

Do you think there is a use case to check for localStorage and fallback to state when not available?

Return isPossiblyHydrating

There's no good way to be certain whether we're still hydrating or not, except for also keeping track of first render in our apps, which is not good. It would make sense to receive that is a value so we can use that within our "loading" states.

Handle error when localStorage value is not JSON parseable

If a localStorage value is manually set (not through the hook) and is not valid JSON syntax, this library will throw an error when attempting to parse it:

Uncaught SyntaxError: Unexpected token a in JSON at position 0

Ideally, the hook should handle this error gracefully, in the case that another piece of code overrides the value or it is edited manually through dev tools.

Places to error check:

setValue(e.newValue === null ? defaultValue : JSON.parse(e.newValue))

return storageValue === null ? defaultValue : JSON.parse(storageValue)

Hydration error with ssr:true using NextJS

Using next 12.1.6 and react 18.1.0 I get a hydration error when using use-local-storage-state, even though I'm using the ssr:true option.

I'm setting it up like that:

const [introductionFinished, setIntroductionFinished] = useLocalStorageState(
  "introductionFinished",
  {
    ssr: true,
    defaultValue: false,
  }
);

And I'm using it like that to show a div when introductionFinished is true:

{introductionFinished ? (
  <div className="row">
    <div className="col">
      <Link href="/introduction-part-1">
        <a>Show introduction again</a>
      </Link>
    </div>
  </div>
) : (
  ""
)}

Here's my error log:

next-dev.js?3515:25 Warning: Expected server HTML to contain a matching <div> in <main>.
    at div
    at main
    at div
    at Home (webpack-internal:///./pages/index.js:72:97)
    at MyApp (webpack-internal:///./pages/_app.js:46:27)
    at ErrorBoundary (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:20746)
    at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/client.js:8:23395)
    at Container (webpack-internal:///./node_modules/next/dist/client/index.js:323:9)
    at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:825:26)
    at Root (webpack-internal:///./node_modules/next/dist/client/index.js:949:27)
window.console.error @ next-dev.js?3515:25
printWarning @ react-dom.development.js?ac89:86
error @ react-dom.development.js?ac89:60
warnForInsertedHydratedElement @ react-dom.development.js?ac89:10496
didNotFindHydratableInstance @ react-dom.development.js?ac89:11425
warnNonhydratedInstance @ react-dom.development.js?ac89:14266
tryToClaimNextHydratableInstance @ react-dom.development.js?ac89:14400
updateHostComponent$1 @ react-dom.development.js?ac89:20711
beginWork @ react-dom.development.js?ac89:22447
beginWork$1 @ react-dom.development.js?ac89:27381
performUnitOfWork @ react-dom.development.js?ac89:26513
workLoopSync @ react-dom.development.js?ac89:26422
renderRootSync @ react-dom.development.js?ac89:26390
performConcurrentWorkOnRoot @ react-dom.development.js?ac89:25694
workLoop @ scheduler.development.js?bcd2:266
flushWork @ scheduler.development.js?bcd2:239
performWorkUntilDeadline @ scheduler.development.js?bcd2:533
react-dom.development.js?ac89:14388 Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
    at throwOnHydrationMismatch (react-dom.development.js?ac89:14388:1)
    at tryToClaimNextHydratableInstance (react-dom.development.js?ac89:14401:1)
    at updateHostComponent$1 (react-dom.development.js?ac89:20711:1)
    at beginWork (react-dom.development.js?ac89:22447:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js?ac89:4161:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?ac89:4210:1)
    at invokeGuardedCallback (react-dom.development.js?ac89:4274:1)
    at beginWork$1 (react-dom.development.js?ac89:27405:1)
    at performUnitOfWork (react-dom.development.js?ac89:26513:1)
    at workLoopSync (react-dom.development.js?ac89:26422:1)
    at renderRootSync (react-dom.development.js?ac89:26390:1)
    at performConcurrentWorkOnRoot (react-dom.development.js?ac89:25694:1)
    at workLoop (scheduler.development.js?bcd2:266:1)
    at flushWork (scheduler.development.js?bcd2:239:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js?bcd2:533:1)
throwOnHydrationMismatch @ react-dom.development.js?ac89:14388
tryToClaimNextHydratableInstance @ react-dom.development.js?ac89:14401
updateHostComponent$1 @ react-dom.development.js?ac89:20711
beginWork @ react-dom.development.js?ac89:22447
callCallback @ react-dom.development.js?ac89:4161
invokeGuardedCallbackDev @ react-dom.development.js?ac89:4210
invokeGuardedCallback @ react-dom.development.js?ac89:4274
beginWork$1 @ react-dom.development.js?ac89:27405
performUnitOfWork @ react-dom.development.js?ac89:26513
workLoopSync @ react-dom.development.js?ac89:26422
renderRootSync @ react-dom.development.js?ac89:26390
performConcurrentWorkOnRoot @ react-dom.development.js?ac89:25694
workLoop @ scheduler.development.js?bcd2:266
flushWork @ scheduler.development.js?bcd2:239
performWorkUntilDeadline @ scheduler.development.js?bcd2:533
next-dev.js?3515:25 Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.
window.console.error @ next-dev.js?3515:25
printWarning @ react-dom.development.js?ac89:86
error @ react-dom.development.js?ac89:60
errorHydratingContainer @ react-dom.development.js?ac89:11440
recoverFromConcurrentError @ react-dom.development.js?ac89:25802
performConcurrentWorkOnRoot @ react-dom.development.js?ac89:25706
workLoop @ scheduler.development.js?bcd2:266
flushWork @ scheduler.development.js?bcd2:239
performWorkUntilDeadline @ scheduler.development.js?bcd2:533
react-dom.development.js?ac89:14388 Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
    at throwOnHydrationMismatch (react-dom.development.js?ac89:14388:1)
    at tryToClaimNextHydratableInstance (react-dom.development.js?ac89:14401:1)
    at updateHostComponent$1 (react-dom.development.js?ac89:20711:1)
    at beginWork (react-dom.development.js?ac89:22447:1)
    at beginWork$1 (react-dom.development.js?ac89:27381:1)
    at performUnitOfWork (react-dom.development.js?ac89:26513:1)
    at workLoopSync (react-dom.development.js?ac89:26422:1)
    at renderRootSync (react-dom.development.js?ac89:26390:1)
    at performConcurrentWorkOnRoot (react-dom.development.js?ac89:25694:1)
    at workLoop (scheduler.development.js?bcd2:266:1)
    at flushWork (scheduler.development.js?bcd2:239:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js?bcd2:533:1)
throwOnHydrationMismatch @ react-dom.development.js?ac89:14388
tryToClaimNextHydratableInstance @ react-dom.development.js?ac89:14401
updateHostComponent$1 @ react-dom.development.js?ac89:20711
beginWork @ react-dom.development.js?ac89:22447
beginWork$1 @ react-dom.development.js?ac89:27381
performUnitOfWork @ react-dom.development.js?ac89:26513
workLoopSync @ react-dom.development.js?ac89:26422
renderRootSync @ react-dom.development.js?ac89:26390
performConcurrentWorkOnRoot @ react-dom.development.js?ac89:25694
workLoop @ scheduler.development.js?bcd2:266
flushWork @ scheduler.development.js?bcd2:239
performWorkUntilDeadline @ scheduler.development.js?bcd2:533
react-dom.development.js?ac89:20658 Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
    at updateHostRoot (react-dom.development.js?ac89:20658:1)
    at beginWork (react-dom.development.js?ac89:22444:1)
    at beginWork$1 (react-dom.development.js?ac89:27381:1)
    at performUnitOfWork (react-dom.development.js?ac89:26513:1)
    at workLoopSync (react-dom.development.js?ac89:26422:1)
    at renderRootSync (react-dom.development.js?ac89:26390:1)
    at recoverFromConcurrentError (react-dom.development.js?ac89:25806:1)
    at performConcurrentWorkOnRoot (react-dom.development.js?ac89:25706:1)
    at workLoop (scheduler.development.js?bcd2:266:1)
    at flushWork (scheduler.development.js?bcd2:239:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js?bcd2:533:1)
updateHostRoot @ react-dom.development.js?ac89:20658
beginWork @ react-dom.development.js?ac89:22444
beginWork$1 @ react-dom.development.js?ac89:27381
performUnitOfWork @ react-dom.development.js?ac89:26513
workLoopSync @ react-dom.development.js?ac89:26422
renderRootSync @ react-dom.development.js?ac89:26390
recoverFromConcurrentError @ react-dom.development.js?ac89:25806
performConcurrentWorkOnRoot @ react-dom.development.js?ac89:25706
workLoop @ scheduler.development.js?bcd2:266
flushWork @ scheduler.development.js?bcd2:239
performWorkUntilDeadline @ scheduler.development.js?bcd2:533

Debouncing updates produces weird behaviour

Using the hook returned from createLocalStorageStateHook produces weird behavior when debouncing updates.

Using a controlled input, with debouncing using a useEffect, the updates eventually propagates to all components (not always all) using the state.

Using an uncontrolled input, with normal debouncing, the updates seemingly propagates randomly to the components using the state.

LocalStorage is always updated.

We've simplified our case and created a reproducible example in this codesandbox:
https://codesandbox.io/s/use-local-storage-state-debounce-u3p7m?file=/src/App.tsx

I've quickly gone through the source for use-local-storage-state, and can't find anything to indicate this should happen. I'm wondering if we've hit some limitation of React or something.

useEffect do not fire when cart state is updated

FIrst. Really love use-local-storage-state. Good work! 💯

Issue: useEffect do not fire when cart state is updated. Any idea why?

useCart.js

import { createLocalStorageStateHook } from 'use-local-storage-state'

const useCart = createLocalStorageStateHook('cart', {})

export default useCart

cart.js (simplified)

import useCart from '../utils/useCart'

const [cart, setCart] = useCart()

 useEffect(() => {
        //fetch product details
    }, [cart])

React 18 support (created by library author)

I will use this issue to track the progress of implementing React 18 support for this hook. For now, I know of one bug that exists when the new concurrent renderer is enabled.

I will post any progress here.

usage with reducer

I'm not entirely sure this is something use-local-storage-state or just plain React related, but maybe someone can help me out here.
I would like to manage my state with a reducer and keep that in sync with the localStorage.

I've stumbled up this article witch brought me to the idea of a "wrap" reducer so make sure any change on the state is synced to the localStorage.

So I altered the example with the approach mentioned to this: https://codesandbox.io/s/use-local-storage-state-with-reducer-cjs8iw?file=/src/App.js

But there is an issue. It is possible to set additional items in the localStorage, but when you refresh te page, they aren't displayed. When you have a look at the console, you will see 2 log statements. The second one has all the todo items, but they aren't set to my reducer state somehow. Any thoughts on this?

[Suggestion] Add github repo link to package.json

VS Code has nice integreation with homepage prop defined in package.json.

If homepage link is defined in package.json, it will show it on hover:

Снимок экрана 2021-12-12 в 21 27 53

But sadly, this repo doesn't have a homepage link:

Снимок экрана 2021-12-12 в 21 28 06

How about we add this info?
The following code should do the trick:

...
"homepage": "https://github.com/astoilkov/use-local-storage-state",
...

need guidance for using sessionStorage also

For some of my usecase, I also need support for using sessionStorage also, but this library seems to focus only only on local-storage. Can you please guide me how can I convert it to use both local and session storage based on some parameters. I can fork the repo and change it in that repo, so that this library can focus on local storage

Add support to removeItem

How about adding a third item in the tuple that invokes .removeItem, and then being able to clean the entry? In cases where undefined is the initial value, this is commonly desirable IMHO.

I may add a PR if you will :)

[Proposal] Set target to ES5

Have you considered changing the build target to ES5 instead of ES6 so that this library works across more browsers?

Alternatively, you can ship 2 different builds (one with target=ES5 and one with target=ES6), I've seen some other libraries do this and then as a client, I could do import useLocalStorageState from "use-local-storage-state/es5".

Returned value should be unknown type

The value in [value, setValue] = useLocalStorageState('key') can be updated outside of the closure: dev tools, different app versions, different parts of the app with the same key, etc. To avoid runtime errors, value should therefore be typed as unknown so that assertions (or coercions) are explicitly made before treating the output type the same as the input type.

Endless loop on update and useEffect

function customHook(initial = '') {
    const [savedSearch, saveSearch] = useLocalStorageState('search', { search: initial })
    const [search, setSearch] = useState(savedSearch.search)

    useEffect(() => {
        saveSearch({ search })
        mockUpdateCallback(search)
    }, [search, saveSearch])

    return [search, setSearch]
}

Doing something like above completely breaks in an endless loop. This was not a problem in 4.x.x. The saveSearch function does not appear to be the same version. Removing the saveSearch from the dependency will work, but that's not the right thing to do.

React 18: value only is available on 2nd render cycle

I believe this is by design, but can you elaborate why the value is only available on the 2nd render, which is triggered by use-local-storage-state itself?

import { useState, useRef } from 'react'
import useLocalStorageState from 'use-local-storage-state'

const App = ({ Component, pageProps }) => {
  const [triggerRender, setTriggerRender] = useState('a')

  const renderCounter = useRef(0)
  renderCounter.current = renderCounter.current + 1

  const [test, setTest] = useLocalStorageState('test', {
    defaultValue: 'DEFAULT',
  })

  console.log('cycle:' + renderCounter.current)
  console.log('test value:' + test)

  return (
      <a onClick={() => setTest('TEST')}>set Localstorage state</a>
  )
}

export default App

Async storage lookup/default value issue

@astoilkov: A React app can have a query parameter (e.g. ?appId=123) which is persisted using this use-local-storage-state hook.
A default value is also set (-1). When the state is -1, an error is thrown (no App ID is set, the app needs an App ID to function).
The problem is that the use-local-storage-state is asynchronous: In the first render the value is still the default value (-1), in the 2nd render the value is the one retrieved from local storage.
This means that in the first render the app already shows an error message, as the error will also stop further renders.

How can I get around this?

state is not persisted to LS during initial render with defaultValue

const [locale, setLocale] = useLocalStorageState<'en-US' | 'es'>(
  'locale', 'en-US',
)

The above will not actually persist the default value to the local storage key until setLocale is called. I would expect the library to initialize the LS key with the default value if it doesn't already exist.

Opt-out of cross-tab syncing?

This library is fantastic, thank you!

I've come across a use-case where I need to opt-out of the cross-tab syncing, would it be possible to get a config option for this?

Here's the part I'm talking about:

https://github.com/astoilkov/use-local-storage-state/blob/master/src/useLocalStorageStateBase.ts#L80-L93

My use-case

I'm using use-local-storage-state to track recent URL paths visited.

For example, I'll push each path onto an array such as: ['/about', '/blog', '/contact'], etc.

I then want to update the "Recent Path" on page load, so I do it in a useEffect:

import { useEffect } from 'react'
import { createLocalStorageStateHook } from 'use-local-storage-state'

const useLocalStorage = createLocalStorageStateHook('recent-paths', [])

// `path` is derived from the current URL
export const useRecentPathsTracker = (path) => {
  const [recentPaths, setRecentPaths] = useLocalStorage()
  useEffect(() => {
    // Make sure `path` is de-duplicated and prefixed to the start of the array
    setRecentPaths([
      path,
      ...(recentPaths || []).filter(recentPath => recentPath !== path),
    ])
  }, [path, recentPaths, setRecentPaths])
}

This works fine in a single tab, but when I open 2 tabs that point to different paths (for example; example.com/blog & example.com/about), the following happens:

  1. Open example.com/blog in Tab 1
  2. The useEffect calls setRecentPaths(['/blog'])
  3. Open example.com/about in Tab 2
  4. The useEffect sets setRecentPaths(['/about', '/blog'])
  5. Calling setRecentPaths in Tab 2 updates the window's storage item, which triggers an event in Tab 1
  6. Tab 1's useEffect sees a new value for recentPaths, so runs again and calls setRecentPaths(['/blog', '/about'])
    • Note the blog & about have swapped
  7. Calling setRecentPaths in Tab 1 updates the window's storage item, which triggers an event in Tab 2
  8. Tab 2's useEffect sees a new value for recentPaths, so runs again and calls setRecentPaths(['/about', '/blog'])
    • Note the blog & about have swapped back again
  9. And so on...

I don't actually need the cross-tab syncing for this usage, so would be happy with something like a { sync: false } option:

const useLocalStorage = createLocalStorageStateHook('recent-paths', [], { sync: false })

The workaround for now is to use the functional form of the setRecentPaths call:

export const useRecentPathsTracker = (path) => {
  const [, setRecentPaths] = useLocalStorage()
  useEffect(() => {
    // NOTE: We use the functional version of state setting here to avoid an
    // infinite loop when two tabs are open with different values for
    // `path`. This is a problem because `use-local-storage-state` will sync the
    // value across tabs by subscribing to the `session` window event.  If we
    // were to add `recentPaths` to the dependency list of `useEffect`, it would
    // trigger a re-evaluation of the effect when either tab altered value and
    // so the tabs would fight about setting their own path to be the first in
    // the list.
    // But by using the functional style, it's not a dependency, and our effect
    // is only executed when this page's `path` changes, which is what we expect
    setRecentPaths(recentPaths => {
      return [
        path,
        ...(recentPaths || []).filter(recentPath => recentPath !== path),
      ]
    })
  }, [path, setRecentPaths]) // Only run once per page load
}

The end result is the setRecentPaths is only called once per mount, which is what I expected initially.

[v14] How to use a value from another hook as a default value?

For example, in v13 I had the following code:

export default function App() {
  const [darkMode, setDarkMode] = useLocalStorageState(
    "darkMode",
    useMediaQuery("(prefers-color-scheme: dark)"),
  );
  // ...
}

In v14, I needed to create the local storage hook first at the top level like this:

const useDarkMode = createLocalStorageHook("darkMode", {
  defaultValue: useMediaQuery("(prefers-color-scheme: dark)"), // <-- React Hook cannot be called at the top level.
});

export default function App() {
  const [darkMode, setDarkMode] = useDarkMode();
  // ...
}

What's the workaround? Could the API be changed to accept a default value through the generated hook instead?

const useDarkMode = createLocalStorageHook("darkMode");

export default function App() {
  const [darkMode, setDarkMode] = useDarkMode(useMediaQuery("(prefers-color-scheme: dark)")); // <-- Not sure if this makes sense but would be similar to the `React.useState` hook.
  // ...
}

setState() doesn't always update the localStorage value

Thanks for this useful library! I ran into a bit of a puzzle, and I'm hoping you can help. I'm using a hook created via createLocalStorageStateHook() to write to local storage, then immediately navigating to a new page to read the value from another instance of the hook.

What's happening is that, absent another render of my component, the state is not being written to local storage, so the new page unexpectedly gets empty state.

I have a reproduction which clears local storage on boot, has a button to write and navigate, and then shows whether the write was successful. Initially I had a difficult time reproducing this. Oddly, the problem only manifests when I invoke Apollo Client's useQuery() in the writer component.

While this suggests the problem could be related to Apollo Client, the ways I'm able to patch around the issue suggest that there might also be an issue in useLocalStorageState (or possibly React).

Here is the reproduction: https://codesandbox.io/s/vigorous-banzai-mhr9z?file=/src/App.js

I've found three ways to work around the problem:

  1. Patching useLocalStorageState() as follows:
            // old 
            setState((state) => {
                const isCallable = (
                    value: unknown,
                ): value is (value: T | undefined) => T | undefined => typeof value === 'function'
                const value = isCallable(newValue) ? newValue(state.value) : newValue

                return {
                    value,
                    isPersistent: storage.set(key, value),
                }
            })
            // new
            setState({
                value: newValue as T,
                isPersistent: storage.set(key, newValue),
            })
  1. Commenting out the call to useQuery().
  2. Wrapping the setIsReading(true) navigation transition in setImmediate(). This causes the Writer to re-render before navigating away, which fixes the problem.

So the problem seems to be something in Apollo Client preventing useLocalStorageState's asynchronous setter from firing at the time the component unmounts.

I have to wonder: does React guarantee that async dispatchers will run at the time a component is unmounting?

There is some chance of something nefarious in Apollo Client being the root cause here, however it seems more likely that either this is a bug in React, or that this is React's expected behavior, which would mean this is an interop bug in useLocalStorageState.

I think the next step here is to try to understand whether or not this is a bug in React.

Edge-Case, problem with refreshing from localStorage if key changes

Example, I read the users active tenantId from localStorage. The localStorage key contains the users ID, since if another user signs in on the same machine, they will probably have a different active tenantId.

const [tenantId, setTenantId] = useLocalStorageState<string>(
    `${user?.id}_tid`
);

If the user changes without the whole component being reloaded (unloaded and reloaded) OR if the user is undefined because he's still being loaded, it seems like useLocalStorageState will not fetch the localStorage value again for the changed key. tenantId will always turn out to be undefined (in my case, because on first render, the user is undefined).

As a fix, I'm showing a loader while the user is undefined, and I'll use useLocalStorageState only if there is a user. This is probably more clean anyway. But I was surprised that useLocalStorageState didn't work as I expected.

What do you think?

ps. Great library, keep it up!

Default value ignores/overwrites previous localStorage state

Hey there,

I am confused by the behavior of the defaultValue prop. I would imagine that say for the key of 'color', setting a defaultValue of 'red', would only set the attribute to 'red' if no other value was set for color already (in localStorage in my case).

Here's an example of what I find confusing:
image

https://codesandbox.io/s/purple-morning-1z2or?file=/src/App.js

This is confusing behavior IMO. I don't think the pre-existing localStorage keys should be clobbered by something that is named a defaultValue.

Thoughts @astoilkov ?

Session Storage support?

Is there any plans to let the user choose between localstorage and session storage. Or even between using the default value over the stored value?

I have a use case that I would only like to use the stored value during the same user session.

Thoughts on missing localStorage?

In certain contexts you may encounter browsers which have disabled localStorage.

If you're blocking cookies in Safari, for instance, simply trying to access localStorage (even for a typeof check) will throw an error.

In other cases/browsers, attempting to actually set an item will yield a quota exceeded error (they set the max quota size to 0 bytes).

An actual check for whether or not localStorage is "available and usable" would look something like:

const hasLocalStorage = (() => {
  try {
      if (typeof localStorage === 'undefined') {
          return false
      }

      // Can we actually use it? "quota exceeded" errors in Safari, etc
      const mod = '__lscheck'
      localStorage.setItem(mod, mod)
      localStorage.removeItem(mod)
  } catch (err) {
      return false
  }

  return true
})()

Even with such a check, the "solution" isn't obvious - what should a library such as this one do in this case? Throw? Warn? Pretend like it isn't happening?

Given that you often use this as a "drop-in" replacement for setState, I would at least expect it to fall back to using in-memory state - but in that case, how far do you go? Try to implement postMessage calls for cross-tab/window polyfilling of the missing storage events?

I'm raising this mostly to get your perspective, but I also feel that perhaps we should try to implement something that prevents this hook from potentially blocking the page from being rendered should the user have disabled localStorage.

Can cause hydration mismatch with SSR

First off, really nice hook! One issue I'm seeing for SSR'ed apps is if the stored value affects DOM structure then there's going to be a hydration mismatch warning (for example Warning: Expected server HTML to contain a matching <div> in <div>).

One workaround:

export default function Todos() {
    const [query, setQuery] = useState('')
    const [todos, setTodos] = useLocalStorageState('todos', ['buy milk'])
 
    // Keep track of whether app has mounted (hydration is done)
    const [hasMounted, setHasMounted] = useState(false);
    useEffect(() => { 
        setHasMounted(true) 
    }, []);

    function onClick() {
        setTodos([...todos, query])
    }

    return (
        <>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <button onClick={onClick}>Create</button>
   
            {/* Only render todos after mount */}
            {hasMounted && todos.map(todo => (<div>{todo}</div>))}
        </>
    )
}

Maybe we can include a section in the readme that covers this? I'm not sure if there's an elegant way for the to hook to handle this. It could wait until mount to return the value from storage, but then that means an extra re-render for non-SSR apps. Thoughts?

More discussion:
https://twitter.com/gabe_ragland/status/1232055916033273858
https://www.joshwcomeau.com/react/the-perils-of-rehydration/

State in local storage not restored after page reload with SSR

I'm using Next.js with SSR.

After saving my form data in local storage and refreshing the page, my form renders with the default values and not the values saved in local storage, although I have the option ssr enabled.

Here's the relevant part of my code:

export default function createClient() {
  const [
    formInitialValues,
    setFormInitialValues,
    { isPersistent, removeItem },
  ] = useLocalStorageState("awaji", {
    ssr: true,
    defaultValue: {
      lastName: "",
      firstName: "",
      gender: "male",
      dateOfBirth: "",
      initialCredit: 10000,
    },
  });

  const onSubmit = useCallback((values, actions) => {
    console.log(values);
    setFormInitialValues(values);
  }, []);

// rest of the code...

Is there something I'm doing wrong ?

expose storage object

Hi, it would be very handy in my case to be able to consistently read from the local storage outside of React scope. Could you please export the storage object?

Setter in useEffect dependencies array always changes

Hello, not sure if this should be taken as a bug or just a question.

Whenever I'm updating the localStorage state inside an effect, I cannot put the setter in the dependencies array. Otherwise, it causes infinite renders.

My workaround is to exclude the setter from the deps array. However, this is a bit unfortunate due to the react-hooks/exhaustive-deps eslint rule.

How do you deal with this? do you just ignore the exhaustive-deps warning?

You can see an example here: https://codesandbox.io/s/blissful-faraday-37g5wg?file=/src/App.js

Thanks!

Seems to be trying to fetch from local storage before window is defined in React/Next SSR environment

I'm using use-local-storage-stare in a custom hook to store a UUID in local storage. I'm calling my hook in several places in the app and I can see that when the window is undefined that I get different UUIDs from my default setting until window is defined and then I get the correct UUID from storage returned. Here is my implementation:

`import useLocalStorageState from "use-local-storage-state";
import { v4 as uuid } from "uuid";

const LOCAL_STORAGE_KEY = "RESONANCE_DEVICE_ID";

const useDeviceId = (): string | undefined => {
const [deviceId] = useLocalStorageState(LOCAL_STORAGE_KEY, {
defaultValue: uuid(),
});

return deviceId;

};

export default useDeviceId;
`

and I'm on React 18.2.0, Next 12.2.2 , use-local-storage-state 18.1.0
logging on Server (note different id being generated by default UUID() since window is still undefined at this point (best guess).
image

logging on client:
can see that the id from storage comes through after first 2 defaults get created.
image

Question, is there a way to disable until window is defined? An option something like { useSSR: false } or similar?

Thank you!

Unable to access data stored on localstorage on NextJS after reloading the page

The problem only happens in the nextjs project on my machine (in the codesandbox only a warning appears on the console)

image

a minimal example of the problem, but as I said, in the codesandbox it only generates a warning, but in the project on localhost this photo error appears: https://codesandbox.io/s/loving-thompson-dlx6j?file=/pages/index.js

Edit: now that i understand the reason for just being a warning in condesandbox, i am using optional chaining ..., but the problem is still simulable, even if there is a value in localstorage it is inaccessible after a reload on the page

Server side rendering

It would be useful if the hook could test for the presence of the window object to enable server-side rendering more easily. For example:

i. test for window object
ii. if doesn't exist, return defaultValue

Any thoughts? This might still leave a potential issue with updateValue?

Additional type exports

Hi there, and thanks for this great library! Have just upgraded from v6 to v15 and it seems to be working well.

Since createLocalStorageStateHook() has been removed and I was using it in one place, I replaced it with my own function, which I needed to annotate with my own types. I ended up checking this in:

export type UpdateState<T> = (newValue: T | ((value: T) => T)) => void
export type LocalStorageState<T> = [T, UpdateState<T>, LocalStorageProperties]
export interface LocalStorageProperties {
  isPersistent: boolean
  removeItem: () => void
}

Would you consider using / exporting these types from the package? I'd be happy to open a PR.

Enhancment: use window.name as fallback

Hey, just wanted to discuss the Idea of using window.name as a fallback instead of a global variable.

window.name would have the advantage of being persistent for the whole javascript session (in particular for a page reload; but not across tabs) and is still available even if cookies are turned off.

If you think that would be a better solution I would be happy to create a merge request.

Here is some more information about window.name:
https://www.thomasfrank.se/sessionvars.html

Migrating from `createLocalStorageStateHook`

Hey, so I am seeing a few usages of createLocalStorageStateHook in our codebase and I noticed the latest version does not have this function?

I also went through the releases and didn't see a mention of it being removed as a breaking change. Could I get any pointers on how to migrate the existing usages?

"ReferenceError: localStorage is not defined" when upgrading to v14.0.0

I'm writing a Next.js SSR project and I'm using this package for storing dark mode preference in local storage.
After upgrading to v14.0.0, I changed my code into something like this:

export default function App({ Component, pageProps }) {
	const [userDarkMode, setUserDarkMode] = createLocalStorageHook('userDarkMode', {ssr: true, defaultValue: 'auto'})();
	...
}

This results in the ReferenceError mentioned in the title.

More logs:

ReferenceError: localStorage is not defined
--
20:06:20.441 | at useLocalStorageState (/vercel/path0/node_modules/use-local-storage-state/src/useLocalStorageState.js:21:9)
20:06:20.441 | at useLocalStorage (/vercel/path0/node_modules/use-local-storage-state/src/createLocalStorageHook.js:13:126)
20:06:20.442 | at App (/vercel/path0/.next/server/pages/_app.js:72:7)

Seems like localStorage.getItem(key) === null leads to this problem.

Am I using the new version of this package in the right way?

Persist in localStorage, but do not upgrade React state

Hi @astoilkov thanks for this great library.

I ran into an edge case and was wondering if that's a feature you may wanna support in the future:

In my case, I want to call setValue for storing the value in local storage, but without updating React's state. In other words, when calling setValue, I do not want a state update and thus no re-render. Scenario: I only want to use useLocalStorageState to write to the local storage and to pick up the value from the storage for the initial rendering, in between I do not care about the value anymore.

This could probably be solved with another property in the options object, e.g. { onlyWrite: true } which would prevent the internal setState call. However, I can also understand if that's something the library is no supposed to offer, because it's after all a solution for syncing the local storage to React's state.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.