Skip to content
Merged
Show file tree
Hide file tree
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 Oct 1, 2022
1147417
feat(local-storage): implements local storage service
sadcitizen Oct 1, 2022
c6e8510
feat(local-storage): implements local storage service
sadcitizen Oct 1, 2022
369d6c6
feat(use-local-storage): wip
sadcitizen Oct 2, 2022
cfdb79a
feat(session-storage): implementation
sadcitizen Oct 2, 2022
59ee663
feat(use-session-storage): wip
sadcitizen Oct 2, 2022
0956b6c
Merge branch 'next' into feature/storages
sadcitizen Oct 5, 2022
f6d031f
chore: resolve conflicts
sadcitizen Oct 5, 2022
6034c58
Merge branch 'next' into feature/storages
sadcitizen Nov 27, 2022
a9891be
chore: generate package-lock.json
sadcitizen Nov 27, 2022
82a9ada
Merge branch 'master' into feature/storages
sadcitizen Jul 5, 2024
a18aeb4
feat(local-storage, session-storage): update deps
sadcitizen Jul 5, 2024
29d21a7
chore: update package-lock.json
sadcitizen Jul 5, 2024
6eede5f
feat(useLocalStorage): update contract
sadcitizen Oct 31, 2024
e27de45
Merge branch 'master' into feature/storages
sadcitizen Oct 31, 2024
572d91e
feat(local-storage): change error message
sadcitizen Oct 31, 2024
673ffb4
feat(session-storage): change error message
sadcitizen Oct 31, 2024
5aee8b5
feat(types): update storage service type
sadcitizen Nov 1, 2024
920ecda
Revert "feat(types): update storage service type"
sadcitizen Nov 1, 2024
dfe53b9
feat(local-storage): prepare to publish
sadcitizen Nov 1, 2024
fb5d0a6
feat(session-storage): prepare to publish
sadcitizen Nov 1, 2024
aa54c57
feat(types): publish 0.1.1
sadcitizen Nov 1, 2024
0f2445e
feat(local-storage): publish 0.0.1
sadcitizen Nov 1, 2024
b68361e
feat(session-storage): change version to 0.0.1
sadcitizen Nov 1, 2024
0060fbf
feat(local-storage): extract methods to static utilities
sadcitizen Dec 1, 2024
8a2421c
feat(local-storage): add hook implementation
sadcitizen Dec 1, 2024
44aa92e
feat(local-storage): update implementation
sadcitizen Dec 1, 2024
234e8af
feat(local-storage): add event listener
sadcitizen Dec 1, 2024
7105592
feat(local-storage): remove debugger;
sadcitizen Dec 1, 2024
e43c2fc
Merge branch 'build/turborepo' into feature/storages
pixel-fixer Feb 28, 2025
714b66c
package lock удаление
pixel-fixer Feb 28, 2025
a701eec
package lock
pixel-fixer Mar 3, 2025
368c7d1
useSessionStorage
pixel-fixer Mar 3, 2025
2eae97a
Merge branch 'master' into feature/storages
sadcitizen Mar 6, 2026
b4a2732
chore: update deps
sadcitizen Mar 6, 2026
86093f6
feat(local-storage): add test
sadcitizen Mar 9, 2026
c007714
chore: setup tests
sadcitizen Mar 9, 2026
711401a
feat(local-storage): improve implementation
sadcitizen Mar 9, 2026
af0783a
chore: clean up
sadcitizen Mar 9, 2026
98c94eb
chore: clean up
sadcitizen Mar 9, 2026
4f83935
feat(useLocalStorage): add exports
sadcitizen Mar 11, 2026
8dfd823
feat(local-storage): remove method
sadcitizen Mar 11, 2026
ea92293
feat(useLocalStorage): useRef -> useMemo
sadcitizen Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion components/flex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"scripts": {
"build": "rollup --config",
"clean": "rimraf dist && rimraf .turbo && rimraf node_modules && rimraf package-lock.json",
"test": "jest --config ../../jest.config.js --roots components/flex/src",
"lint:check": "npm run eslint:check && npm run prettier:check && npm run stylelint:check",
"lint:fix": "npm run eslint:fix && npm run prettier:fix && npm run stylelint:fix",
"eslint:check": "eslint src --config ../../eslint.config.js",
Expand Down
3 changes: 1 addition & 2 deletions hooks/use-intersection-observer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rimraf dist && rimraf .turbo && rimraf node_modules && rimraf package-lock.json",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js --roots hooks/use-intersection-observer/src"
"lint": "eslint src --config ../../eslint.config.js"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
Expand Down
3 changes: 1 addition & 2 deletions hooks/use-isomorphic-layout-effect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rimraf dist && rimraf .turbo && rimraf node_modules && rimraf package-lock.json",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js"
"lint": "eslint src --config ../../eslint.config.js"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
Expand Down
11 changes: 11 additions & 0 deletions hooks/use-local-storage/README.md
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
```
36 changes: 36 additions & 0 deletions hooks/use-local-storage/package.json
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": "*"
}
}
4 changes: 4 additions & 0 deletions hooks/use-local-storage/src/index.ts
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;
7 changes: 7 additions & 0 deletions hooks/use-local-storage/src/useLocalStorage.stories.mdx
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>
42 changes: 42 additions & 0 deletions hooks/use-local-storage/src/useLocalStorage.stories.tsx
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;
133 changes: 133 additions & 0 deletions hooks/use-local-storage/src/useLocalStorage.tests.ts
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;
(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');
});
});
68 changes: 68 additions & 0 deletions hooks/use-local-storage/src/useLocalStorage.ts
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]
);

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];
}
16 changes: 16 additions & 0 deletions hooks/use-local-storage/tsconfig.json
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"
]
}
Loading
Loading