-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
David Maskasky
committed
Mar 5, 2024
1 parent
3b024ed
commit 634e9cf
Showing
23 changed files
with
9,794 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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,5 @@ | ||
{ | ||
"buildCommand": "compile", | ||
"sandboxes": ["new", "react-typescript-react-ts"], | ||
"node": "18" | ||
} |
This file contains 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,2 @@ | ||
/dist | ||
/src/vendor |
This file contains 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,126 @@ | ||
{ | ||
"env": { | ||
"browser": true, | ||
"shared-node-browser": true, | ||
"node": true, | ||
"es6": true | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/recommended", | ||
"prettier", | ||
"plugin:prettier/recommended", | ||
"plugin:react-hooks/recommended", | ||
"plugin:import/errors", | ||
"plugin:import/warnings" | ||
], | ||
"plugins": [ | ||
"@typescript-eslint", | ||
"react", | ||
"prettier", | ||
"react-hooks", | ||
"import", | ||
"jest" | ||
], | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"sourceType": "module", | ||
"ecmaFeatures": { | ||
"jsx": true | ||
} | ||
}, | ||
"rules": { | ||
"eqeqeq": "error", | ||
"no-var": "error", | ||
"prefer-const": "error", | ||
"curly": ["warn", "multi-line", "consistent"], | ||
"no-console": "off", | ||
"import/no-unresolved": ["error", { "commonjs": true, "amd": true }], | ||
"import/export": "error", | ||
"import/no-duplicates": ["error"], | ||
"@typescript-eslint/explicit-module-boundary-types": "off", | ||
"@typescript-eslint/no-unused-vars": [ | ||
"warn", | ||
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } | ||
], | ||
"@typescript-eslint/no-use-before-define": "off", | ||
"@typescript-eslint/no-empty-function": "off", | ||
"@typescript-eslint/no-explicit-any": "off", | ||
"jest/consistent-test-it": [ | ||
"error", | ||
{ "fn": "it", "withinDescribe": "it" } | ||
], | ||
"import/order": [ | ||
"error", | ||
{ | ||
"alphabetize": { "order": "asc", "caseInsensitive": true }, | ||
"groups": [ | ||
"builtin", | ||
"external", | ||
"internal", | ||
"parent", | ||
"sibling", | ||
"index", | ||
"object" | ||
], | ||
"newlines-between": "never", | ||
"pathGroups": [ | ||
{ | ||
"pattern": "react", | ||
"group": "builtin", | ||
"position": "before" | ||
} | ||
], | ||
"pathGroupsExcludedImportTypes": ["builtin"] | ||
} | ||
], | ||
"react/jsx-uses-react": "off", | ||
"react/react-in-jsx-scope": "off", | ||
"sort-imports": [ | ||
"error", | ||
{ | ||
"ignoreDeclarationSort": true | ||
} | ||
] | ||
}, | ||
"settings": { | ||
"react": { | ||
"version": "detect" | ||
}, | ||
"import/extensions": [".js", ".jsx", ".ts", ".tsx"], | ||
"import/parsers": { | ||
"@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] | ||
}, | ||
"import/resolver": { | ||
"node": { | ||
"extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], | ||
"paths": ["src"] | ||
}, | ||
"alias": { | ||
"extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], | ||
"map": [["^jotai-history$", "./src/index.ts"]] | ||
} | ||
} | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": ["src"], | ||
"parserOptions": { | ||
"project": "./tsconfig.json" | ||
} | ||
}, | ||
{ | ||
"files": ["tests/**/*.tsx", "__tests__/**/*"], | ||
"env": { | ||
"jest/globals": true | ||
} | ||
}, | ||
{ | ||
"files": ["./*.js"], | ||
"rules": { | ||
"@typescript-eslint/no-var-requires": "off" | ||
} | ||
} | ||
] | ||
} |
This file contains 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,27 @@ | ||
name: CD | ||
|
||
on: | ||
push: | ||
tags: | ||
- v* | ||
|
||
jobs: | ||
publish: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: pnpm/action-setup@v2 | ||
with: | ||
version: 8.2.0 | ||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: 18 | ||
registry-url: 'https://registry.npmjs.org' | ||
cache: 'pnpm' | ||
cache-dependency-path: '**/pnpm-lock.yaml' | ||
- run: pnpm install --frozen-lockfile | ||
- run: npm test | ||
- run: npm run compile | ||
- run: npm publish | ||
env: | ||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
This file contains 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,21 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
pull_request: | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: pnpm/action-setup@v2 | ||
with: | ||
version: 8.2.0 | ||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: 18 | ||
cache: 'pnpm' | ||
cache-dependency-path: '**/pnpm-lock.yaml' | ||
- run: pnpm install --frozen-lockfile | ||
- run: npm test |
This file contains 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 @@ | ||
*~ | ||
*.swp | ||
node_modules | ||
/dist |
This file contains 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,2 @@ | ||
*.md | ||
pnpm-lock.yaml |
This file contains 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,8 @@ | ||
{ | ||
"semi": false, | ||
"trailingComma": "es5", | ||
"singleQuote": true, | ||
"bracketSameLine": true, | ||
"tabWidth": 2, | ||
"printWidth": 80 | ||
} |
This file contains 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 |
---|---|---|
@@ -1,2 +1,117 @@ | ||
# jotai-history | ||
A Jōtai utility package for advanced state history management | ||
# History | ||
|
||
[jotai-history](https://jotai.org/docs/integrations/history) is a utility package for advanced state history management | ||
|
||
## install | ||
|
||
``` | ||
npm i jotai-history | ||
``` | ||
|
||
## atomWithHistory | ||
|
||
### Signature | ||
|
||
```ts | ||
function atomWithHistory<T>(targetAtom: Atom<T>, limit: number): Atom<T[]> | ||
``` | ||
|
||
This function creates an atom that keeps a history of states for a given `targetAtom`. The `limit` parameter determines the maximum number of history states to keep. | ||
This is useful for tracking the changes over time. | ||
|
||
The history atom tracks changes to the `targetAtom` and maintains a list of previous states up to the specified `limit`. When the `targetAtom` changes, its new state is added to the history. | ||
|
||
### Usage | ||
|
||
```tsx | ||
import { atom } from 'jotai' | ||
import { atomWithHistory } from 'jotai/utils' | ||
const countAtom = atom(0) | ||
const countWithPrevious = atomWithHistory(myAtom, 2) | ||
export function CountComponent() { | ||
const [[count, previousCount], setCount] = useAtom(countWithPrevious) | ||
return ( | ||
<> | ||
<p>Count: {count}</p> | ||
<p>Previous Count: {previousCount}</p> | ||
<button onClick={() => setCount((c) => c + 1)}>Increment</button> | ||
</> | ||
) | ||
} | ||
``` | ||
|
||
## atomWithUndo | ||
|
||
### Signature | ||
|
||
```ts | ||
type Undoable<T> = { | ||
value: T | ||
undo: Function | ||
redo: Function | ||
canUndo: boolean | ||
canRedo: boolean | ||
} | ||
function atomWithUndo<T>( | ||
targetAtom: PrimitiveAtom<T>, | ||
limit: number | ||
): WritableAtom<Undoable<T>, [SetStateAction<T>], void> | ||
``` | ||
|
||
`atomWithHistory` provides undo and redo capabilities for an atom. It keeps track of the value history of `targetAtom` and provides methods to move back and forth through that history. | ||
|
||
The returned object includes: | ||
|
||
- `value`: The current value of the `targetAtom`. | ||
- `undo`: A function to revert to the previous state. | ||
- `redo`: A function to advance to the next state. | ||
- `canUndo`: A boolean indicating if it's possible to undo. | ||
- `canRedo`: A boolean indicating if it's possible to redo. | ||
|
||
### Usage | ||
|
||
```tsx | ||
import { atom } from 'jotai' | ||
import { atomWithUndo } from 'jotai/utils' | ||
const counterAtom = atom(0) | ||
const counterWithUndo = atomWithUndo(counterAtom, 5) | ||
export function CounterComponent() { | ||
const [{ value, undo, redo, canUndo, canRedo }, setValue] = | ||
useAtom(counterWithUndo) | ||
return ( | ||
<> | ||
<p>Count: {value}</p> | ||
<button onClick={() => setValue((c) => c + 1)}>Increment</button> | ||
<button onClick={undo} disabled={!canUndo}> | ||
Undo | ||
</button> | ||
<button onClick={redo} disabled={!canRedo}> | ||
Redo | ||
</button> | ||
</> | ||
) | ||
} | ||
``` | ||
|
||
## Example | ||
https://codesandbox.io/p/sandbox/musing-orla-g6qj3q?file=%2Fsrc%2FApp.tsx%3A10%2C23 | ||
[![Edit musing-orla-g6qj3q](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/musing-orla-g6qj3q?file=%2Fsrc%2FApp.tsx) | ||
<a href="https://codesandbox.io/p/sandbox/musing-orla-g6qj3q?file=%2Fsrc%2FApp.tsx"> | ||
<img alt="Edit musing-orla-g6qj3q" src="https://codesandbox.io/static/img/play-codesandbox.svg"> | ||
</a> | ||
|
||
<iframe src="https://codesandbox.io/embed/g6qj3q?view=Editor+%2B+Preview&module=%2Fsrc%2FApp.tsx" | ||
style="width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;" | ||
title="musing-orla-g6qj3q" | ||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" | ||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" | ||
></iframe> | ||
## Memory Management | ||
|
||
⚠️ Since `atomWithHistory` and `atomWithUndo` keeps a history of states, it's important to manage memory by setting a reasonable `limit`. Excessive history can lead to memory bloat, especially in applications with frequent state updates. |
This file contains 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,40 @@ | ||
import { atom, createStore } from 'jotai/vanilla' | ||
import type { Atom, PrimitiveAtom } from 'jotai/vanilla' | ||
import { atomWithHistory } from '../src/atomWithHistory' | ||
|
||
describe('atomWithHistory', () => { | ||
let store: ReturnType<typeof createStore> | ||
let baseAtom: PrimitiveAtom<number> | ||
let historyAtom: Atom<number[]> | ||
let unsub: () => void | ||
|
||
beforeEach(() => { | ||
store = createStore() | ||
baseAtom = atom(0) // Initial value is 0 | ||
historyAtom = atomWithHistory(baseAtom, 3) // Limit history to 3 entries | ||
unsub = store.sub(historyAtom, () => {}) // Subscribe to trigger onMount | ||
}) | ||
|
||
it('tracks history of changes', () => { | ||
store.set(baseAtom, 1) | ||
store.set(baseAtom, 2) | ||
expect(store.get(historyAtom)).toEqual([2, 1, 0]) // History should track changes | ||
}) | ||
|
||
it('enforces history limit', () => { | ||
store.set(baseAtom, 1) | ||
store.set(baseAtom, 2) | ||
store.set(baseAtom, 3) | ||
store.set(baseAtom, 4) | ||
expect(store.get(historyAtom).length).toBe(3) // Length should not exceed limit | ||
expect(store.get(historyAtom)).toEqual([4, 3, 2]) // Only the most recent 3 states are kept | ||
}) | ||
|
||
it('cleans up history on unmount', () => { | ||
store.set(baseAtom, 1) | ||
expect(store.get(historyAtom)).toEqual([1, 0]) // History before unmount | ||
unsub() // Unsubscribe to unmount | ||
unsub = store.sub(historyAtom, () => {}) // Subscribe to mount | ||
expect(store.get(historyAtom)).toEqual([]) // History should be cleared | ||
}) | ||
}) |
Oops, something went wrong.