-
Notifications
You must be signed in to change notification settings - Fork 0
Local Storage Service and Hook #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
d29f348
feat(core): add storage entity
sadcitizen 1147417
feat(local-storage): implements local storage service
sadcitizen c6e8510
feat(local-storage): implements local storage service
sadcitizen 369d6c6
feat(use-local-storage): wip
sadcitizen cfdb79a
feat(session-storage): implementation
sadcitizen 59ee663
feat(use-session-storage): wip
sadcitizen 0956b6c
Merge branch 'next' into feature/storages
sadcitizen f6d031f
chore: resolve conflicts
sadcitizen 6034c58
Merge branch 'next' into feature/storages
sadcitizen a9891be
chore: generate package-lock.json
sadcitizen 82a9ada
Merge branch 'master' into feature/storages
sadcitizen a18aeb4
feat(local-storage, session-storage): update deps
sadcitizen 29d21a7
chore: update package-lock.json
sadcitizen 6eede5f
feat(useLocalStorage): update contract
sadcitizen e27de45
Merge branch 'master' into feature/storages
sadcitizen 572d91e
feat(local-storage): change error message
sadcitizen 673ffb4
feat(session-storage): change error message
sadcitizen 5aee8b5
feat(types): update storage service type
sadcitizen 920ecda
Revert "feat(types): update storage service type"
sadcitizen dfe53b9
feat(local-storage): prepare to publish
sadcitizen fb5d0a6
feat(session-storage): prepare to publish
sadcitizen aa54c57
feat(types): publish 0.1.1
sadcitizen 0f2445e
feat(local-storage): publish 0.0.1
sadcitizen b68361e
feat(session-storage): change version to 0.0.1
sadcitizen 0060fbf
feat(local-storage): extract methods to static utilities
sadcitizen 8a2421c
feat(local-storage): add hook implementation
sadcitizen 44aa92e
feat(local-storage): update implementation
sadcitizen 234e8af
feat(local-storage): add event listener
sadcitizen 7105592
feat(local-storage): remove debugger;
sadcitizen e43c2fc
Merge branch 'build/turborepo' into feature/storages
pixel-fixer 714b66c
package lock удаление
pixel-fixer a701eec
package lock
pixel-fixer 368c7d1
useSessionStorage
pixel-fixer 2eae97a
Merge branch 'master' into feature/storages
sadcitizen b4a2732
chore: update deps
sadcitizen 86093f6
feat(local-storage): add test
sadcitizen c007714
chore: setup tests
sadcitizen 711401a
feat(local-storage): improve implementation
sadcitizen af0783a
chore: clean up
sadcitizen 98c94eb
chore: clean up
sadcitizen 4f83935
feat(useLocalStorage): add exports
sadcitizen 8dfd823
feat(local-storage): remove method
sadcitizen ea92293
feat(useLocalStorage): useRef -> useMemo
sadcitizen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # `byndyusoft-ui/use-local-storage` | ||
|
|
||
| > A custom hook that uses Local Storage API to persist state. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| npm i @byndyusoft-ui/use-local-storage | ||
| # or | ||
| yarn add @byndyusoft-ui/use-local-storage | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "@byndyusoft-ui/use-local-storage", | ||
| "version": "0.1.0", | ||
| "description": "Byndyusoft UI Local Storage React Hook", | ||
| "keywords": [ | ||
| "byndyusoft", | ||
| "byndyusoft-ui", | ||
| "react", | ||
| "hook", | ||
| "local-storage" | ||
| ], | ||
| "author": "Eugene Abrosimov <sadcitizen@yandex.ru>", | ||
| "homepage": "https://github.com/Byndyusoft/ui/tree/master/hooks/use-local-storage#readme", | ||
| "license": "ISC", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/Byndyusoft/ui.git" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "clean": "rimraf dist", | ||
| "lint": "eslint src --config ../../eslint.config.js" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/Byndyusoft/ui/issues" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "dependencies": { | ||
| "@byndyusoft-ui/local-storage": "*", | ||
| "@byndyusoft-ui/use-event-listener": "*" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import useLocalStorage, { IUseLocalStorageActions, IUseLocalStorageOptions, TUseLocalStorage } from './useLocalStorage'; | ||
|
|
||
| export { IUseLocalStorageActions, IUseLocalStorageOptions, TUseLocalStorage }; | ||
| export default useLocalStorage; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { Meta } from '@storybook/addon-docs'; | ||
| import { Markdown } from '@storybook/blocks'; | ||
| import Readme from '../README.md'; | ||
|
|
||
| <Meta title="hooks/useLocalStorage" /> | ||
|
|
||
| <Markdown>{Readme}</Markdown> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import React, { useCallback, useRef, useState } from 'react'; | ||
| import useLocalStorage from './useLocalStorage'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
|
|
||
| type TTokenTemplateStory = StoryObj<typeof TokenTemplate>; | ||
|
|
||
| const TokenTemplate = (): JSX.Element => { | ||
| const [token, { setValue: setToken, removeValue: removeToken }] = useLocalStorage('access', ''); | ||
| const [inputValue, setInputValue] = useState('Brand new token'); | ||
| const handleClick = useCallback(() => { | ||
| setToken(inputValue); | ||
| }, [setToken, inputValue]); | ||
|
|
||
| const handleClickDelete = () => { | ||
| removeToken(); | ||
| }; | ||
|
|
||
| return ( | ||
| <section> | ||
| <div>Access token: {token ?? 'no token'}</div> | ||
| <p> | ||
| <input value={inputValue} onChange={e => setInputValue(e.target.value)} /> | ||
| </p> | ||
|
|
||
| <button type="button" onClick={handleClick}> | ||
| Добавить токен | ||
| </button> | ||
| <p> | ||
| <button onClick={handleClickDelete}>Удалить токен</button> | ||
| </p> | ||
| </section> | ||
| ); | ||
| }; | ||
| export const Token: TTokenTemplateStory = { | ||
| decorators: [() => <TokenTemplate />] | ||
| }; | ||
|
|
||
| const meta: Meta = { | ||
| title: 'hooks/useLocalStorage' | ||
| }; | ||
|
|
||
| export default meta; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import { act, renderHook } from '@testing-library/react-hooks'; | ||
| import useLocalStorage from './useLocalStorage'; | ||
|
|
||
| describe('hooks/useLocalStorage', () => { | ||
| const KEY = 'ls-test-key'; | ||
|
|
||
| beforeEach(() => { | ||
| window.localStorage.clear(); | ||
| }); | ||
|
|
||
| test('initializes with default value when storage is empty', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| expect(result.current[0]).toBe('default'); | ||
| }); | ||
|
|
||
| test('reads existing value from localStorage on init', () => { | ||
| window.localStorage.setItem(KEY, JSON.stringify('stored')); | ||
|
|
||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| expect(result.current[0]).toBe('stored'); | ||
| }); | ||
|
|
||
| test('setValue updates state and writes to localStorage', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| act(() => { | ||
| result.current[1].setValue('next'); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('next'); | ||
| expect(window.localStorage.getItem(KEY)).toBe(JSON.stringify('next')); | ||
| }); | ||
|
|
||
| test('removeValue clears storage and resets to default', () => { | ||
| window.localStorage.setItem(KEY, JSON.stringify('stored')); | ||
|
|
||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| act(() => { | ||
| result.current[1].removeValue(); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('default'); | ||
| expect(window.localStorage.getItem(KEY)).toBeNull(); | ||
| }); | ||
|
|
||
| test('respects custom serializer and deserializer from options', () => { | ||
| const serialize = vi.fn((value: { foo: string }) => `(${value.foo})`); | ||
| const deserialize = vi.fn((raw: string) => ({ foo: raw.slice(1, -1) })); | ||
|
|
||
| window.localStorage.setItem(KEY, '(baz)'); | ||
|
|
||
| const { result, rerender } = renderHook( | ||
| (props: { initial: { foo: string } }) => | ||
| useLocalStorage<{ foo: string }>(KEY, props.initial, { serialize, deserialize }), | ||
| { | ||
| initialProps: { initial: { foo: 'default' } } | ||
| } | ||
| ); | ||
|
|
||
| expect(deserialize).toHaveBeenCalledWith('(baz)'); | ||
|
|
||
| act(() => { | ||
| result.current[1].setValue({ foo: 'bar' }); | ||
| }); | ||
|
|
||
| expect(window.localStorage.getItem(KEY)).toBe('(bar)'); | ||
|
|
||
| expect(serialize).toHaveBeenCalledWith({ foo: 'bar' }); | ||
| }); | ||
|
|
||
| test('updates value when storage event for the same key and same storage area is dispatched', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| const event = new Event('storage') as StorageEvent; | ||
| (event as any).key = KEY; | ||
| (event as any).newValue = JSON.stringify('from-event'); | ||
| (event as any).storageArea = window.localStorage; | ||
|
|
||
| act(() => { | ||
| window.dispatchEvent(event); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('from-event'); | ||
| }); | ||
|
|
||
| test('do not update value when storage event for the same key and other storage area is dispatched (syncTab = false)', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| const event = new Event('storage') as StorageEvent; | ||
| (event as any).key = KEY; | ||
sadcitizen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| (event as any).newValue = JSON.stringify('from-event'); | ||
| (event as any).storageArea = {} as Storage; | ||
|
|
||
| act(() => { | ||
| window.dispatchEvent(event); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('default'); | ||
| }); | ||
|
|
||
| test('do not update value when storage event for the other key and same storage area is dispatched (syncTab = false)', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default')); | ||
|
|
||
| const event = new Event('storage') as StorageEvent; | ||
| (event as any).key = 'other-key'; | ||
| (event as any).newValue = JSON.stringify('from-event'); | ||
| (event as any).storageArea = window.localStorage; | ||
|
|
||
| act(() => { | ||
| window.dispatchEvent(event); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('default'); | ||
| }); | ||
|
|
||
| test('updates value when storage event for the same key and other storage area is dispatched (syncTab = true)', () => { | ||
| const { result } = renderHook(() => useLocalStorage(KEY, 'default', { syncTabs: true })); | ||
|
|
||
| const event = new Event('storage') as StorageEvent; | ||
| (event as any).key = KEY; | ||
| (event as any).newValue = JSON.stringify('from-event'); | ||
| (event as any).storageArea = {} as Storage; | ||
|
|
||
| act(() => { | ||
| window.dispatchEvent(event); | ||
| }); | ||
|
|
||
| expect(result.current[0]).toBe('from-event'); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { useCallback, useMemo, useState } from 'react'; | ||
| import { TSerializeValue, TDeserializeValue, LocalStorageService } from '@byndyusoft-ui/local-storage'; | ||
| import useEventListener from '@byndyusoft-ui/use-event-listener'; | ||
|
|
||
| export interface IUseLocalStorageActions<TValue> { | ||
| setValue: (value: TValue) => void; | ||
| removeValue: () => void; | ||
| } | ||
|
|
||
| export interface IUseLocalStorageOptions<TValue> { | ||
| serialize?: TSerializeValue<TValue>; | ||
| deserialize?: TDeserializeValue<TValue>; | ||
| syncTabs?: boolean; | ||
| } | ||
|
|
||
| export type TUseLocalStorage<TValue> = [TValue, IUseLocalStorageActions<TValue>]; | ||
|
|
||
| export default function useLocalStorage<TValue>( | ||
| key: string, | ||
| defaultValue: TValue, | ||
| options?: IUseLocalStorageOptions<TValue> | ||
| ): TUseLocalStorage<TValue> { | ||
| const service = useMemo( | ||
| () => | ||
| new LocalStorageService(key, defaultValue, { | ||
| serialize: options?.serialize, | ||
| deserialize: options?.deserialize | ||
| }), | ||
| [key, defaultValue, options] | ||
| ); | ||
|
|
||
| const [storedValue, setStoredValue] = useState<TValue>(() => service.getValue()); | ||
|
|
||
| const setValue = useCallback( | ||
| (nextValue: TValue) => { | ||
| service.setValue(nextValue); | ||
| setStoredValue(nextValue); | ||
| }, | ||
| [key, options] | ||
SmorodinVik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
|
|
||
| const removeValue = useCallback(() => { | ||
| service.removeValue(); | ||
| setStoredValue(defaultValue); | ||
| }, [key, defaultValue]); | ||
|
|
||
| const handleEvent = useCallback( | ||
| (event: StorageEvent) => { | ||
| if (event.key !== key) { | ||
| return; | ||
| } | ||
|
|
||
| if (window.localStorage === event.storageArea || options?.syncTabs) { | ||
| setStoredValue(event.newValue ? service.deserialize(event.newValue) : defaultValue); | ||
| } | ||
| }, | ||
| [key, options] | ||
| ); | ||
|
|
||
| useEventListener('storage', handleEvent); | ||
|
|
||
| const methods = useMemo<IUseLocalStorageActions<TValue>>( | ||
| () => ({ setValue, removeValue }), | ||
| [setValue, removeValue] | ||
| ); | ||
|
|
||
| return [storedValue, methods]; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "extends": "../../tsconfig.json", | ||
| "compilerOptions": { | ||
| "declaration": true, | ||
| "declarationDir": "dist", | ||
| "outDir": "dist", | ||
| "module": "commonjs" | ||
| }, | ||
| "include": [ | ||
| "../../types.d.ts", | ||
| "src" | ||
| ], | ||
| "exclude": [ | ||
| "node_modules" | ||
| ] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.