Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
David Maskasky committed Mar 5, 2024
1 parent 3b024ed commit 634e9cf
Show file tree
Hide file tree
Showing 23 changed files with 9,794 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"buildCommand": "compile",
"sandboxes": ["new", "react-typescript-react-ts"],
"node": "18"
}
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist
/src/vendor
126 changes: 126 additions & 0 deletions .eslintrc.json
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"
}
}
]
}
27 changes: 27 additions & 0 deletions .github/workflows/cd.yml
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 }}
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*~
*.swp
node_modules
/dist
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.md
pnpm-lock.yaml
8 changes: 8 additions & 0 deletions .prettierrc
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
}
119 changes: 117 additions & 2 deletions README.md
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.
40 changes: 40 additions & 0 deletions __tests__/atomWithHistory.test.ts
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
})
})
Loading

0 comments on commit 634e9cf

Please sign in to comment.