Skip to content

Commit

Permalink
feat(use-trave): implement all api
Browse files Browse the repository at this point in the history
  • Loading branch information
unadlib committed Mar 17, 2024
1 parent 4a864a3 commit f3f5114
Show file tree
Hide file tree
Showing 5 changed files with 871 additions and 14 deletions.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,57 @@
# use-travel

A React hook for state time travel with undo, redo, and reset functionalities.

TODO
### Installation

```bash
npm install use-travel mutative
# or
yarn add use-travel mutative
```

### Features

- Undo/Redo/Reset
- Small size for time travel with Patches history
- Customizable history size
- Customizable initial patches
- High performance
- Mark function for custom immutability

### TODO

- [ ] add `archive` functionality

### API

```jsx
import { useTravel } from 'use-travel';

const App = () => {
const [state, setState, controls]} = useTravel(0, {
maxHistory: 10,
initialPatches: [],
});
return (
<div>
<div>{state}</div>
<button onClick={() => setState(state + 1)}>Increment</button>
<button onClick={() => setState(state - 1)}>Decrement</button>
<button onClick={controls.back} disabled={!controls.canUndo()}>Undo</button>
<button onClick={controls.forward} disabled={!controls.canRedo()}>Redo</button>
<button onClick={controls.reset}>Reset</button>
{controls.getHistory().map((state, index) => (
<div key={index}>{state}</div>
))}
{controls.patches.map((patch, index) => (
<div key={index}>{patch}</div>
))}
<div>{controls.position}</div>
<button onClick={() => {
controls.go(1);
}}>Go</button>
</div>
);
}
```
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-travel",
"version": "0.0.1",
"version": "0.1.0",
"description": "A React hook for state time travel with undo, redo, and reset functionalities.",
"main": "dist/index.cjs.js",
"unpkg": "dist/index.umd.js",
Expand Down Expand Up @@ -43,8 +43,10 @@
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@testing-library/react": "^14.2.1",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.5",
"@types/react": "^18.2.66",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"commitizen": "^4.3.0",
Expand All @@ -57,6 +59,8 @@
"jest-environment-jsdom": "^29.5.0",
"mutative": "^1.0.3",
"prettier": "^2.8.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^4.4.0",
"rollup": "^3.20.0",
"rollup-plugin-terser": "^7.0.0",
Expand All @@ -81,6 +85,11 @@
}
},
"peerDependencies": {
"mutative": "^1.0.3"
"@types/react": "^18.0 || ^17.0",
"mutative": "^1.0.3",
"react": "^18.0 || ^17.0"
},
"dependencies": {
"use-mutative": "^1.0.0"
}
}
210 changes: 208 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,209 @@
export const useTravel = (state: any) => {
//
import { useCallback, useEffect, useMemo } from 'react';
import {
type Options as MutativeOptions,
type Patches,
type Draft,
type Immutable,
apply,
rawReturn,
} from 'mutative';
import { useMutative } from 'use-mutative';

type TravelPatches = {
patches: Patches[];
inversePatches: Patches[];
};

type Options<A extends boolean, F extends boolean> = {
maxHistory?: number;
initialPatches?: TravelPatches;
autoArchive?: A;
} & MutativeOptions<true, F>;

type InitialValue<I extends any> = I extends (...args: any) => infer R ? R : I;
type DraftFunction<S> = (draft: Draft<S>) => void;
type Updater<S> = (value: S | (() => S) | DraftFunction<S>) => void;
type Value<S, F extends boolean> = F extends true
? Immutable<InitialValue<S>>
: InitialValue<S>;
type StateValue<S> =
| InitialValue<S>
| (() => InitialValue<S>)
| DraftFunction<InitialValue<S>>;

type Result<S, F extends boolean> = [
Value<S, F>,
Updater<InitialValue<S>>,
{
/**
* The current position in the history
*/
position: number;
/**
* Get the history of the state
*/
getHistory: () => Value<S, F>[];
/**
* The patches of the history
*/
patches: TravelPatches;
/**
* Go back in the history
*/
back: (amount?: number) => void;
/**
* Go forward in the history
*/
forward: (amount?: number) => void;
/**
* Reset the history
*/
reset: () => void;
/**
* Go to a specific position in the history
*/
go: (position: number) => void;
/**
* Check if it's possible to go back
*/
canBack: () => boolean;
/**
* Check if it's possible to go forward
*/
canForward: () => boolean;
}
];

/**
* A hook to travel in the history of a state
*/
export const useTravel = <S, A extends boolean, F extends boolean>(
initialState: S,
{ maxHistory = 10, initialPatches, ...options }: Options<A, F> = {}
) => {
const [position, setPosition] = useMutative(-1);
const [allPatches, setAllPatches] = useMutative(
() =>
(initialPatches ?? {
patches: [],
inversePatches: [],
}) as TravelPatches
);
const [state, setState, patches, inversePatches] = useMutative(initialState, {
...options,
enablePatches: true,
});
useEffect(() => {
if (position === -1 && patches.length > 0) {
setAllPatches((_allPatches) => {
_allPatches.patches.push(patches);
_allPatches.inversePatches.push(inversePatches);
if (maxHistory < _allPatches.patches.length) {
_allPatches.patches = _allPatches.patches.slice(-maxHistory);
_allPatches.inversePatches = _allPatches.inversePatches.slice(
-maxHistory
);
}
});
}
}, [maxHistory, patches, inversePatches, position]);
const cachedPosition = useMemo(
() => (position === -1 ? allPatches.patches.length : position),
[position, allPatches.patches.length]
);
const cachedTravels = useMemo(() => {
const go = (nextPosition: number) => {
const back = nextPosition < cachedPosition;
if (nextPosition > allPatches.patches.length) {
console.warn(`Can't go forward to position ${nextPosition}`);
nextPosition = allPatches.patches.length;
}
if (nextPosition < 0) {
console.warn(`Can't go back to position ${nextPosition}`);
nextPosition = 0;
}
if (nextPosition === cachedPosition) return;
setPosition(nextPosition);
setState(() =>
rawReturn(
apply(
state as object,
back
? allPatches.inversePatches.slice(nextPosition).flat().reverse()
: allPatches.patches.slice(position, nextPosition).flat()
)
)
);
};
return {
position: cachedPosition,
getHistory: () => {
const history = [state];
let currentState = state as any;
for (let i = cachedPosition; i < allPatches.patches.length; i++) {
currentState = apply(
currentState as object,
allPatches.patches[i]
) as S;
history.push(currentState);
}
currentState = state as any;
const inversePatches = allPatches.inversePatches;
const stateIndex =
inversePatches.length === cachedPosition
? inversePatches.length - 1
: inversePatches.length - cachedPosition - 1;
for (let i = stateIndex; i > -1; i--) {
currentState = apply(
currentState as object,
allPatches.inversePatches[i]
) as S;
history.unshift(currentState);
}
return history;
},
patches: allPatches,
back: (amount = 1) => {
go(cachedPosition - amount);
},
forward: (amount = 1) => {
go(cachedPosition + amount);
},
reset: () => {
setPosition(-1);
setAllPatches(
() => initialPatches ?? { patches: [], inversePatches: [] }
);
setState(() => initialState);
},
go,
canBack: () => cachedPosition > 0,
canForward: () => cachedPosition < allPatches.patches.length,
};
}, [
cachedPosition,
allPatches,
setPosition,
setAllPatches,
setState,
initialState,
state,
]);
const cachedSetState = useCallback(
(value: StateValue<S>) => {
setPosition(-1);
if (position !== -1) {
setAllPatches((_allPatches) => {
_allPatches.patches = _allPatches.patches.slice(0, position);
_allPatches.inversePatches = _allPatches.inversePatches.slice(
0,
position
);
});
}
setState(value);
},
[setState, setPosition, position, setAllPatches]
);
return [state, cachedSetState, cachedTravels] as Result<S, F>;
};
Loading

0 comments on commit f3f5114

Please sign in to comment.