react-localstorage-ts
A small layer over the browser's localstorage, fallbacks to an in-memory store if localstorage is not supported by the browser.
Built with fp-ts
in mind, react-localstorage-ts
gives you a standard way to access objects stored locally.
install
yarn
yarn add react-localstorage-ts
npm
npm install -S react-localstorage-ts
quick start
First create the hooks to read/write the values you just defined:
import * as t from "io-ts"
import { makeHooksFromStorage, createLocalStorage } from "react-localstorage-ts"
import { ThemeFlavourCodec, AuthTokenCodec } from "./codecs"
export const localStorage = createLocalStorage(
{
themeFlavour: ThemeFlavourCodec,
authToken: AuthTokenCodec,
},
{ defaultValues: { themeFlavour: "light" } },
)
export const hooks = makeHooksFromStorage(storage)
then you use them in your react components:
import * as React from "react"
import * as LV from "react-localstorage-ts/LocalValue"
import LightThemeApp from "./components/LightThemeApp"
import DarkThemeApp from "./components/DarkThemeApp"
import { useThemeFlavour } from "./localHooks"
const App: React.FC = () => {
const [themeFlavour, setThemeFlavour] = hooks.useThemeFlavour()
return pipe(
theme,
LV.fold2(
() => {
console.error("wrong value stored in localStorage!")
},
(themeFlavour) => {
switch (themeFlavour) {
case "light": {
return <LightThemeApp />
}
case "dark": {
return <DarkThemeApp />
}
}
},
),
)
}
export default App
LocalValue
A new data structure is defined for items stored in localstorage, LocalValue
. When dealing with a value stored in your localstorage there are three possibilities:
- there is no value in your localstorage (optionality).
- the value is present, but it is wrong (correctness).
- the value is present and it is valid (also correctness).
LocalValue introduces a sum type that represents the optionality/correctness dicotomy:
export interface Absent {
readonly _tag: "Absent"
}
export interface Invalid<E> {
readonly _tag: "Invalid"
readonly errors: E
}
export interface Valid<A> {
readonly _tag: "Valid"
readonly value: A
}
export type LocalValue<E, A> = Absent | Invalid<E> | Valid<A>
It also has instances for some of the most common fp-ts
type-classes, so that you can use it in the same way you usually use other fp-ts
abstractions:
import * as LV from "react-localstorage-ts/LocalValue"
import { useAccessToken } from "./localHooks"
import { goToLoginPage } from "./router"
import App from "./App"
const LoginLayout: React.FC = ({ children }) => {
const [token] = useAccessToken()
React.useEffect(() => {
if (!LV.isValid) {
goToLoginPage()
}
}, [])
return pipe(
token,
LV.fold(
() => "no token in storage",
() => "malformed token in storage",
() => <>{children}</>,
),
)
}
import * as LV from "react-localstorage-ts/LocalValue"
import { goToHomePage } from "./router"
import { useAccessToken } from "./localHooks"
const LoginPage: React.FC = ({ children }) => {
const [token, setToken] = useAccessToken()
React.useEffect(() => {
if (LV.isValid(token)) {
goToHomePage()
}
}, [])
return (
<Form
onSubmit={(formValues) =>
api.getToken(formValues).then((t) => setToken(t))
}
/>
)
}
defining codecs
Given that browsers only allows you to store serialized data in string format, codecs must conform to the shape Codec<E, string, B>
, where E
is the type of the decoding error, string
is the type of the data before decoding and B
is the type of the runtime value.
If you use io-ts
you can simply create a layer to convert io-ts
codecs to Codec
compliant instances:
import { pipe } from "fp-ts/lib/function"
import * as t from "io-ts"
import * as E from "fp-ts/Either"
import { Json, JsonFromString } from "io-ts-types"
import * as LV from "react-localstorage-ts/LocalValue"
import { Codec } from "react-localstorage-ts/Codec"
const adaptIoTsCodec = <A, B>(C: t.Type<B, A>): Codec<t.Errors, A, B> => {
return {
encode: C.encode,
decode: (u: unknown) => LV.fromEither(C.decode(u)),
}
}
export const fromIoTsCodec = <A, B extends Json>(C: t.Type<A, B>) => {
const stringCodec = new t.Type<A, string>(
C.name,
C.is,
(u, c) => {
return pipe(
t.string.validate(u, c),
E.chain((jsonString) => JsonFromString.validate(jsonString, c)),
E.chain((json) => C.validate(json, c)),
)
},
(v) => {
return pipe(v, C.encode, JsonFromString.encode)
},
)
return adaptIoTsCodec(stringCodec)
}
If you want to update your localstorage from outside of a react component while still having your components "react" to the change,
you can use the utilities getLocalValue
, setLocalElement
and removeLocalElement
.
contributing
to commit to this repository there are a few rules:
- your commits must follow the conventional commit standard (it should be enforced by husky
commit-msg
hook). - your code must be formatted using prettier.
- all tests must pass.
release flow
here you can find an explanation of the release flow.