Skip to content

Commit

Permalink
feat: atomWithActions & create (#5)
Browse files Browse the repository at this point in the history
* feat: atomWithActions & create

* update CHANGELOG

* little more completeness
  • Loading branch information
dai-shi authored Sep 9, 2024
1 parent 47933c4 commit 1dc08ca
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- feat: atomWithActions & create #5

## [0.4.0] - 2024-05-27

### Changed
Expand Down
9 changes: 9 additions & 0 deletions examples/02_create/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<title>example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
22 changes: 22 additions & 0 deletions examples/02_create/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "example",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"jotai": "latest",
"jotai-zustand": "latest",
"react": "latest",
"react-dom": "latest",
"zustand": "latest"
},
"devDependencies": {
"@types/react": "latest",
"@types/react-dom": "latest",
"typescript": "latest",
"vite": "latest"
},
"scripts": {
"dev": "vite"
}
}
31 changes: 31 additions & 0 deletions examples/02_create/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { create } from 'jotai-zustand';

const useCountStore = create(
{
count: 0,
},
(set) => ({
inc: () => set((prev) => ({ count: prev.count + 1 })),
}),
);

const Counter = () => {
const count = useCountStore(useCallback((state) => state.count, []));
const inc = useCountStore(useCallback((state) => state.inc, []));

return (
<>
count: {count}
<button onClick={inc}>inc</button>
</>
);
};

export default function App() {
return (
<div className="App">
<Counter />
</div>
);
}
10 changes: 10 additions & 0 deletions examples/02_create/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import App from './app';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
14 changes: 14 additions & 0 deletions examples/02_create/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"strict": true,
"target": "es2018",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"allowJs": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"jsx": "react-jsx"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"test:types": "tsc -p . --noEmit",
"test:types:examples": "tsc -p examples --noEmit",
"test:spec": "vitest run",
"examples:01_typescript": "DIR=01_typescript vite"
"examples:01_typescript": "DIR=01_typescript vite",
"examples:02_create": "DIR=02_create vite"
},
"keywords": [
"jotai",
Expand Down
60 changes: 60 additions & 0 deletions src/atomWithActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { atom } from 'jotai/vanilla';

export function atomWithActions<State extends object, Actions extends object>(
initialState: State,
createActions: (
set: (
partial: Partial<State> | ((prev: State) => Partial<State>),
replace?: boolean,
) => void,
get: () => State,
) => Actions,
) {
const stateAtom = atom(initialState);
if (process.env.NODE_ENV !== 'production') {
stateAtom.debugPrivate = true;
}
const actionsAtom = atom(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_get, { setSelf }: any) => {
const actions = createActions(
(partial, replace) => setSelf({ type: 'set', partial, replace }),
() => setSelf({ type: 'get' }),
);
return actions;
},
(
get,
set,
arg:
| { type: 'get' }
| {
type: 'set';
partial: Partial<State> | ((prev: State) => Partial<State>);
replace: boolean | undefined;
},
) => {
const state = get(stateAtom);
if (arg.type === 'get') {
return state;
}
const { partial, replace } = arg;
const nextState =
typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
set(
stateAtom,
replace ? (nextState as State) : Object.assign({}, state, nextState),
);
}
},
);
if (process.env.NODE_ENV !== 'production') {
actionsAtom.debugPrivate = true;
}
const derivedAtom = atom((get) => ({
...get(stateAtom),
...get(actionsAtom),
}));
return derivedAtom;
}
28 changes: 28 additions & 0 deletions src/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMemo } from 'react';
import { atom, createStore } from 'jotai/vanilla';
import { useAtomValue } from 'jotai/react';

import { atomWithActions } from './atomWithActions.js';

export function create<State extends object, Actions extends object>(
initialState: State,
createActions: (
set: (partial: Partial<State> | ((prev: State) => Partial<State>)) => void,
get: () => State,
) => Actions,
) {
const store = createStore();
const theAtom = atomWithActions(initialState, createActions);
const useStore = <Slice>(selector: (state: State & Actions) => Slice) => {
const derivedAtom = useMemo(
() => atom((get) => selector(get(theAtom))),
[selector],
);
return useAtomValue(derivedAtom, { store });
};
const useStoreWithGetState = useStore as typeof useStore & {
getState: () => State & Actions;
};
useStoreWithGetState.getState = () => store.get(theAtom);
return useStoreWithGetState;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { atomWithStore } from './atomWithStore.js';
export { atomWithActions } from './atomWithActions.js';
export { create } from './create.js';

0 comments on commit 1dc08ca

Please sign in to comment.