Giter VIP home page Giter VIP logo

use-persisted-state's Introduction

use-persisted-state

A custom React Hook that provides a multi-instance, multi-tab/browser shared and persistent state.

npm version All Contributors

use-persisted-state is not a hook itself, but is a factory that accepts a storage key and an optional storage provider (default = localStorage) and returns a hook that you can use as a direct replacement for useState.

Features

๐Ÿ’พ Persists the state to localStorage

๐Ÿ–ฅ Syncs between tabs and/or browser windows

๐Ÿ“‘ Shares state w/multiple hooks on a page

Requirement

To use use-persisted-state, you must use [email protected] or greater which includes Hooks.

Installation

$ npm i use-persisted-state

Example

Let's take a look at how you can use use-persisted-state. Here we have an example of a typical up/down counter.

import { useState } from 'react';

const useCounter = initialCount => {
  const [count, setCount] = useState(initialCount);

  return {
    count,
    increment: () => setCount(currentCount => currentCount + 1),
    decrement: () => setCount(currentCount => currentCount - 1),
  };
};

export default useCounter;

Let's replace the import of react with an import from use-persisted-state. And we'll call createPersistedState (the factory function). This will return a useCounterState hook that we can use in place of useState.

The complete code is as follows.

import createPersistedState from 'use-persisted-state';
const useCounterState = createPersistedState('count');

const useCounter = initialCount => {
  const [count, setCount] = useCounterState(initialCount);

  return {
    count,
    increment: () => setCount(currentCount => currentCount + 1),
    decrement: () => setCount(currentCount => currentCount - 1),
  };
};

export default useCounter;

The state is shared with any other hook using the same key, either on the same page, across tabs, or even browser windows.

For example, open two copies of your app in two tabs or even two windows. Any changes to state in one tab will be rendered on the other tab.

You can also close the browser and the next time you run your app, the state will be rendered as it was before you closed your browser.

License

MIT Licensed

Contributors

Thanks goes to these wonderful people (emoji key):


Donavon West

๐Ÿš‡ โš ๏ธ ๐Ÿ’ก ๐Ÿค” ๐Ÿšง ๐Ÿ‘€ ๐Ÿ”ง ๐Ÿ’ป

Karol Majewski

๐Ÿ’ป

Octave Raimbault

๐Ÿ’ป

Dennis Morello

๐Ÿ’ป

Florent

๐Ÿ’ป

Mark Adamson

๐Ÿ’ป

Vitor Dino

๐Ÿ’ป

This project follows the all-contributors specification. Contributions of any kind welcome!

use-persisted-state's People

Contributors

allcontributors[bot] avatar dispix avatar donavon avatar errnesto avatar fridus avatar morellodev avatar mungojam avatar

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-persisted-state's Issues

Initial state

I definitely like the idea and mostly of how it's implemented, but the somewhat big caveat to me as DRY guy is repeating the initial state. I mean with regular useState it does make sense as it's isolated but looking at this like a "shared" state, there should definitely be a single place to declare initialState.

I would propose to expand API like createPersistentState(key, initialState). I know there is another argument for storage, but that's likely to be used rarely, so it can be at 3rd place just fine :) Alternatively, it could make sense to export another function with currying behavior where storage would be specified in the first argument and it would return factory bound to that storage type.

Of course, it would still be possible to specify an initial state directly in a component, but it would be optional. Perhaps it could even show DEV-only warning to keep such state unified.

Add reducer equivalent of the API

This looks great, at first glance the main thing lacking is an equivalent of useReducer for more complex state management. Any interest in extending it a bit?

`global` does not exists in browser

THere is no global variable in browsers. It is window for browsers , global is for node or new globalThis for both.
While some packagers polyfills node's global, this package should not relay on packagers (webpack or either)
It's better to add either polyfill or make some checks

Passing a function to setState is not supported

The official setState hook supports an updater function (e.g setState(state => ({ ...state, change: 'this' })). Right now, if you use this pattern with usePersistedState, it results in undefined being stored in localStorage.

Use case: calling setPeristedState in useEffect without having to add the persistedState to the dependencies array (you don't actually have to, but eslint is throwing warnings) which would cause an infinite loop.

New state isn't persisted if component is hidden before next render

If a component is removed right after setting a new persisted value, the value is never set. It looks like this is because setState from useState is being returned directly. The updated state never gets seen because the component doesn't re-render. That would be fine with a vanilla useState, but isn't when you are using persistent state.

return [state, setState];

I think the fix is to return a wrapped setState function and force the value to be persisted when it is called rather than relying purely on state changing which it doesn't in this case.

Here is a reproducible example. The 'Increment' button works but the 'Increment and Hide' does not.

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

import createPersistedState from 'use-persisted-state';

const useCounterState = createPersistedState('count');

const useCounter = initialCount => {
    const [count, setCount] = useCounterState(initialCount);

    return {
        count,
        increment: () => setCount(currentCount => currentCount + 1)
    };
};


const App = () => {
    const { count } = useCounter(1);
    const [showIncrement, setShowIncrement] = useState(true);

    let ComponentToShow;
    if (showIncrement) {
        ComponentToShow = () => <IncrementComponent close={() => setShowIncrement(false)} />;
    } else {
        ComponentToShow = () => <button onClick={() => setShowIncrement(true)}>Show Again</button>;
    }

    return <div>
        <div>count is {count}</div>
        <ComponentToShow />
    </div>
};

const IncrementComponent = ({ close }) => {
    const { increment } = useCounter(1);

    const incrementAndHide = () => {
        increment()
        close()
    };

    return <div>
        <button onClick={increment}>Increment</button>
        <button onClick={incrementAndHide}>Increment and Hide</button>
    </div>
}

ReactDOM.render(<App />, document.getElementById('root'));

return memoized object if it equals current value

Right now this hook can produce unexpected hooks because it will return a different object even if its value is identical to the previous one.

Considered that we are working with JSON data, it would be safe to do a value equality check any time we parse the new value, and if it matches the previous one, we should return the previous one to preserve its identity.

Syncing sessionStorage across tabs

This is probably unconventional, but I currently have an use-case where I'd like to use sessionStorage as the storageProvider and have the value synced across tabs/windows similar to the localStorage provider.

The use-case is a splash screen that the user clicks through once upon application load, so sessionStorage is ideal in this situation since they shouldn't see it again upon browser refresh, but I'd like this value to also sync across tabs.

Any thoughts on whether or not it's a feasible goal for this hook?

Error when new value is not JSON

SyntaxError: Unexpected token e in JSON at position 1
  at JSON.parse(<anonymous>)
  at current(./node_modules/use-persisted-state/dist/use-persisted-state.m.js:1:766)
  at n(./node_modules/@use-it/event-listener/dist/event-listener.m.js:1:199)
  at n(./node_modules/@sentry/browser/esm/helpers.js:70:1)

See line 12 below:

useEventListener('storage', ({ key: k, newValue }) => {
const newState = JSON.parse(newValue);
if (k === key && state !== newState) {
setState(newState);
}
});

Other modules may use localStorage and not store JSON.

Extra check that always true

from the sources:

const newState = JSON.parse(newValue);
    if (k === key && state !== newState)

state !== newState would always resolve to true since newState is just a newly created object returned from JSON parse

Is this being maintained?

@donavon There hasn't been a new version in 2 years and people have reported bugs in issues. That's a shame since this package has over 44,000 weekly downloads according to npm.

When could we expect to see anymore development? Is this abandoned?

Uncaught SyntaxError: Unexpected token u in JSON at position 0

I had the issue that the persisted value got set to undefined in the local storage. Debugging in chrome showed the key in the localStorage listed, but with value undefined set. Note that the value was set undefined, not to the string "undefined"
For some unknown reason this ends to the json object beeing the string "undefined". Therefore line 6 of createStorage.js fails to catch it and it tries to parse the string 'undefined' as json. What leads to the error.
Is: return json === null || typeof json === "undefined" ? ...
Fix: return json === null || json === 'undefined' || typeof json === "undefined" ? ...

Environment: Chrome, windows 10, Visual Code, React

Is it possible to detect the origin tab of an update?

For example:

const useOpts = createPersistedState('key');

const Comp = props => {
  const [opts, setOpts] = useOpts({
    show: false
  });
  useEffect(() => {
    if (opts.show) {
      console.log('Show');
    } else {
      console.log('Hide');
    }
    if (opts._originDoc === document) {
      console.log('Origin was from this tab');
    } else {
      console.log('Origin of update was from another tab');
    }
  }, [opts.show])
  return (
    <>Test</>
  );
}

Unexpected token c in JSON at position 0

I'll try to post more but I'm getting this when values are loaded between windows

Uncaught SyntaxError: Unexpected token c in JSON at position 0
    at JSON.parse (<anonymous>)
    at Object.current (:8500/Users/burton/projects/polar-app/packages/polar-bookshelf/node_modules/use-persisted-state/dist/use-persisted-state.js:1)
    at e (:8500/Users/burton/projects/polar-app/packages/polar-bookshelf/node_modules/@use-it/event-listener/dist/event-listener.js:1)

Example

It will be nice to add a codesandbox link into README to show how to use it in action.

Development issue

Using the package on development mode from the git repo does not work.

  • Works: yarn run dev. It does not throw any error on console.
  • Works: yarn run test. It does not throw any error on console.
    It means it's built properly, right?

But,

  • Works: import createPersistedState from 'use-persisted-state'.
  • Doesn't Work: import createPersistedState from './use-persisted-state'. It throws the following error,
Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)
  1 | import createPersistedState from './use-persisted-state';
  2 | const useCounterState = createPersistedState('count');
  3 | 
> 4 | const useCounter = initialCount => {
    |                                   ^  5 |   const [count, setCount] = useCounterState(initialCount);
  6 | 
  7 |   return {

I found no contribution guide so maybe I did not do it the right way.

Use with AsyncStorage

Hey, I would like to use use-persisted-state with AsyncStorage available in react-native. The problem is that it returns Promises instead of plain values. Do you think it would be possible to accommodate such use case?

Encryption and decryption of data in localstorage

Need option for the stored data to be encrypted while stored in localstorage so it could not be easily read by anyone, and should be again decrypted back to orginal content when reading from within the app.

Would be good for storing data that should not be directly changed by the user from developers tools

Is it safe to use createPersistedState within the same functional component as the calling component?

For instance, is this safe?

export const MyComp = ({ myKey }) => {
  const usePrefState = createPersistedState(
    `prefix:${myKey}`
  );
  const [show, toggle] = usePrefState(0);

  return (

By "safe" I mean:

  • This approach results in about the same number of read/write operations
  • This approach will not result in any unintended consequences, such as performance issues or data loss due to re-renders.

--

Note: As I was typing up this question, I decided to try an approach with useMemo that would limit the concern. I'll go ahead and leave the above section just for reference, and leave the below code snippet for those with the same question:

export const MyComp = ({ myKey }) => {
  const usePrefState = useMemo(() => createPersistedState(
    `prefix:${myKey}`, []
  ));
  const [show, toggle] = usePrefState(0);

  return (

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.