Skip to content

colingourlay/react-local-store

Repository files navigation

react-local-store

localStorage-persisted context for your React apps, accessible through Hooks

NPM latest published version Formats: CommonJS, ECMAScript Modules GZip size 🤭 — OMG, so tiny!

Table of contents

Getting started

npm install react-local-store
import React from 'react';
import ReactDOM from 'react-dom';
import { LocalStoreProvider, useLocalStore } from 'react-local-store';

function App() {
  const [state, dispatch] = useLocalStore();

  return (
    <div>
      <h1>{state.title}</h1>
      <input
        type="text"
        defaultValue={state.title}
        onChange={event =>
          dispatch({ type: 'UPDATE_TITLE', payload: event.target.value })
        }
      />
    </div>
  );
}

ReactDOM.render(
  <LocalStoreProvider
    initialState={{
      title: 'react-local-store'
    }}
    reducer={(state, action) => {
      switch (action.type) {
        case 'UPDATE_TITLE':
          return { ...state, title: action.payload };
        default:
          return state;
      }
    }}
  >
    <App />
  </LocalStoreProvider>,
  document.getElementById('root')
);

Want to take this code for a spin right now? Glitch has got you covered. Hit that button down below to fork the example above and have a play around:

remix this

Once you're in there, give this a try:

  • Change the value in the field and see that the heading also updates.
  • Refresh the page and see that your state was persisted.
  • Open the app in another tab to see that its context is not only shared, but synchronised with the current tab.

API

<LocalStoreProvider /> and useLocalStore()

Provide global state to your entire app, enabled React's context API, then access (and update) it using Hooks

import React from 'react';
import ReactDOM from 'react-dom';
import { LocalStoreProvider, useLocalStore } from 'react-local-store';

const ACTION_TYPES = { INCREMENT: 'INCREMENT' };

function reducer(state, action) {
  switch (action.type) {
    case ACTION_TYPES.INCREMENT:
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useLocalStore();

  return (
    <button onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT })}>
      {state.count}
    </button>
  );
}

ReactDOM.render(
  <LocalStoreProvider initialState={{ count: 0 }} reducer={reducer}>
    <App />
  </LocalStoreProvider>,
  document.getElementById('root')
);

remix this

Synced global state

By default, global state change listeners are used (using window.requestIdleCallback or a simple polyfill) so that changes to your store trigger a re-render in every app instance, including those in other browser tabs. If you want to disable the listener, set the sync prop to false in your Provider:

<LocalStoreProvider sync={false}>
  <App />
</LocalStoreProvider>

Custom store names

By default your state will be persisted to localStorage under the key: __REACT_LOCAL_STORE__. If you want to have multiple stores (or use something other than the default), you have a couple of options. The first is to name your stores with LocalStoreProvider's name prop and useLocalStore's optional argument:

/* ... */

function App() {
  const [state, dispatch] = useLocalStore('custom-store-name');

  /* ... */
}

ReactDOM.render(
  <LocalStoreProvider name="custom-store-name" initialState={...} reducer={...}>
    <App />
  </LocalStoreProvider>,
  document.getElementById('root')
);

remix this

The (arguably better) alternative is to use the createLocalStore factory...

createLocalStore()

Writing custom store names across various components in different files can start to get a bit tedious, and isn't very DRY, so you have the option of creating your own preset Providers and Hooks, with the createLocalStore factory.

In store.js:

import { createLocalStore } from 'react-local-store';

const ACTION_TYPES = { INCREMENT: 'INCREMENT' };

function reducer(state, action) {
  switch (action.type) {
    case ACTION_TYPES.INCREMENT:
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

const [LocalStoreProvider, useLocalStore] = createLocalStore({
  name: 'custom-store-name',
  initialState: { count: 0 },
  reducer
});

export { ACTION_TYPES, LocalStoreProvider, useLocalStore };

In index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { ACTION_TYPES, LocalStoreProvider, useLocalStore } from './store';

function App() {
  const [state, dispatch] = useLocalStore();

  return (
    <button onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT })}>
      {state.count}
    </button>
  );
}

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

remix this

Any props you omit when creating your custom store will be expected when you use it. For example, you can create a custom store, only specifying the name, and still supply your own initialState, reducer and (optionally) sync props when creating Provider instances.

Reasons to not use this

  • You're using a pre-Hooks version of React (<16.8)
  • You'll be using state that can't be serialised to JSON (i.e. functions)
  • You update state often in short time periods (localStorage is 😴)
  • You want to access state outside of functional components
  • You don't want to use a reducer to modify state (check out context-storage instead)

About the project

About

localStorage-persisted context for your React apps, accessible through Hooks

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published