Skip to content

Commit

Permalink
feat: jotai/valtio (#405)
Browse files Browse the repository at this point in the history
* feat: jotai/valtio

* update tests, add csb example

* update valtio

* chore: update size snapshot
  • Loading branch information
dai-shi authored Apr 13, 2021
1 parent f79528f commit 0e01c87
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 100 deletions.
116 changes: 16 additions & 100 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"devtools.js": {
"bundled": 1989,
"minified": 1051,
"gzipped": 594,
"valtio.js": {
"bundled": 1111,
"minified": 555,
"gzipped": 320,
"treeshaked": {
"rollup": {
"code": 28,
"import_statements": 28
"code": 37,
"import_statements": 37
},
"webpack": {
"code": 1045
"code": 1054
}
}
},
Expand All @@ -27,17 +27,17 @@
}
}
},
"query.js": {
"bundled": 2957,
"minified": 1267,
"gzipped": 612,
"devtools.js": {
"bundled": 1989,
"minified": 1051,
"gzipped": 594,
"treeshaked": {
"rollup": {
"code": 57,
"import_statements": 49
"code": 28,
"import_statements": 28
},
"webpack": {
"code": 1078
"code": 1045
}
}
},
Expand All @@ -55,49 +55,7 @@
}
}
},
"xstate.js": {
"bundled": 2799,
"minified": 1268,
"gzipped": 629,
"treeshaked": {
"rollup": {
"code": 29,
"import_statements": 29
},
"webpack": {
"code": 1145
}
}
},
"devtools.module.js": {
"bundled": 1989,
"minified": 1051,
"gzipped": 594,
"treeshaked": {
"rollup": {
"code": 28,
"import_statements": 28
},
"webpack": {
"code": 1045
}
}
},
"immer.module.js": {
"bundled": 1526,
"minified": 797,
"gzipped": 388,
"treeshaked": {
"rollup": {
"code": 42,
"import_statements": 42
},
"webpack": {
"code": 1104
}
}
},
"query.module.js": {
"query.js": {
"bundled": 2957,
"minified": 1267,
"gzipped": 612,
Expand All @@ -111,21 +69,7 @@
}
}
},
"optics.module.js": {
"bundled": 1645,
"minified": 805,
"gzipped": 422,
"treeshaked": {
"rollup": {
"code": 32,
"import_statements": 32
},
"webpack": {
"code": 1061
}
}
},
"xstate.module.js": {
"xstate.js": {
"bundled": 2799,
"minified": 1268,
"gzipped": 629,
Expand Down Expand Up @@ -153,20 +97,6 @@
}
}
},
"utils.module.js": {
"bundled": 10423,
"minified": 5036,
"gzipped": 1949,
"treeshaked": {
"rollup": {
"code": 28,
"import_statements": 28
},
"webpack": {
"code": 1224
}
}
},
"index.js": {
"bundled": 19924,
"minified": 9413,
Expand All @@ -180,19 +110,5 @@
"code": 1266
}
}
},
"index.module.js": {
"bundled": 19921,
"minified": 9410,
"gzipped": 3026,
"treeshaked": {
"rollup": {
"code": 14,
"import_statements": 14
},
"webpack": {
"code": 1266
}
}
}
}
50 changes: 50 additions & 0 deletions docs/api/valtio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
This doc describes `jotai/valtio` bundle.

Jotai's state resides in React, but sometimes it would be nice
to intract with the world outside React.
Valtio provides a proxy interface that can be used to store some values
and sync with atoms in jotai.
This only uses the vanilla api of valtio.

## Install

You have to install `valtio` to access this bundle and its functions.

```
npm install valtio
# or
yarn add valtio
```

## atomWithProxy

`atomWithProxy` creates a new atom with valtio proxy.
It's two-way binding and you can change the value from both ends.

```js
import { useAtom } from 'jotai'
import { atomWithProxy } from 'jotai/valtio'
import { proxy } from 'valtio/vanilla'

const proxyState = proxy({ count: 0 })
const stateAtom = atomWithProxy(proxyState)
const Counter: React.FC = () => {
const [state, setState] = useAtom(stateAtom)

return (
<>
count: {state.count}
<button
onClick={() =>
setState((prev) => ({ ...prev, count: prev.count + 1 }))
}>
button
</button>
</>
)
}
```

### Examples

https://codesandbox.io/s/react-typescript-forked-f5u4l
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
"types": "./xstate.d.ts",
"module": "./esm/xstate.js",
"default": "./xstate.js"
},
"./valtio": {
"types": "./valtio.d.ts",
"module": "./esm/valtio.js",
"default": "./valtio.js"
}
},
"files": [
Expand All @@ -68,6 +73,7 @@
"build:optics": "rollup -c --config-optics",
"build:query": "rollup -c --config-query",
"build:xstate": "rollup -c --config-xstate",
"build:valtio": "rollup -c --config-valtio",
"postbuild": "yarn copy",
"eslint": "eslint --fix '{src,tests}/**/*.{js,ts,jsx,tsx}'",
"eslint:ci": "eslint '{src,tests}/**/*.{js,ts,jsx,tsx}'",
Expand Down Expand Up @@ -181,13 +187,15 @@
"shx": "^0.3.3",
"tslib": "^2.1.0",
"typescript": "^4.2.3",
"valtio": "^1.0.3",
"xstate": "^4.17.1"
},
"peerDependencies": {
"immer": "*",
"optics-ts": "*",
"react": ">=16.8",
"react-query": "*",
"valtio": "*",
"xstate": "*"
},
"peerDependenciesMeta": {
Expand All @@ -200,6 +208,9 @@
"react-query": {
"optional": true
},
"valtio": {
"optional": true
},
"xstate": {
"optional": true
}
Expand Down
1 change: 1 addition & 0 deletions src/valtio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { atomWithProxy } from './valtio/atomWithProxy'
44 changes: 44 additions & 0 deletions src/valtio/atomWithProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { subscribe, snapshot } from 'valtio/vanilla'
import { atom } from 'jotai'
import type { SetStateAction, PrimitiveAtom } from '../core/types'

const isObject = (x: unknown): x is object =>
typeof x === 'object' && x !== null

const applyChanges = <T extends object>(proxyObject: T, prev: T, next: T) => {
;(Object.keys(prev) as (keyof T)[]).forEach((key) => {
if (!(key in next)) {
delete proxyObject[key]
} else if (Object.is(prev[key], next[key])) {
// unchanged
} else if (isObject(prev[key]) && isObject(next[key])) {
applyChanges(proxyObject[key] as any, prev[key], next[key])
} else {
proxyObject[key] = next[key]
}
})
;(Object.keys(next) as (keyof T)[]).forEach((key) => {
if (!(key in prev)) {
proxyObject[key] = next[key]
}
})
}

export function atomWithProxy<Value extends object>(proxyObject: Value) {
const baseAtom: PrimitiveAtom<Value> = atom(snapshot(proxyObject) as any)
baseAtom.onMount = (setValue) =>
subscribe(proxyObject, () => {
setValue(snapshot(proxyObject) as Value)
})
const derivedAtom = atom(
(get) => get(baseAtom),
(get, _set, update: SetStateAction<Value>) => {
const newValue =
typeof update === 'function'
? (update as (prev: Value) => Value)(get(baseAtom))
: update
applyChanges(proxyObject, snapshot(proxyObject) as Value, newValue)
}
)
return derivedAtom
}
83 changes: 83 additions & 0 deletions tests/valtio/atomWithProxy.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react'
import { fireEvent, render } from '@testing-library/react'
import { proxy, snapshot } from 'valtio/vanilla'
import { Provider, useAtom } from '../../src/index'
import { atomWithProxy } from '../../src/valtio'

it('count state', async () => {
const proxyState = proxy({ count: 0 })
const stateAtom = atomWithProxy(proxyState)
const Counter: React.FC = () => {
const [state, setState] = useAtom(stateAtom)

return (
<>
count: {state.count}
<button
onClick={() =>
setState((prev) => ({ ...prev, count: prev.count + 1 }))
}>
button
</button>
</>
)
}

const { findByText, getByText } = render(
<Provider>
<Counter />
</Provider>
)

await findByText('count: 0')

fireEvent.click(getByText('button'))
await findByText('count: 1')
expect(proxyState.count).toBe(1)

++proxyState.count
await findByText('count: 2')
expect(proxyState.count).toBe(2)
})

it('nested count state', async () => {
const proxyState = proxy({ nested: { count: 0 }, other: {} })
const otherSnap = snapshot(proxyState.other)
const stateAtom = atomWithProxy(proxyState)
const Counter: React.FC = () => {
const [state, setState] = useAtom(stateAtom)

return (
<>
count: {state.nested.count}
<button
onClick={() =>
setState((prev) => ({
...prev,
nested: { ...prev.nested, count: prev.nested.count + 1 },
}))
}>
button
</button>
</>
)
}

const { findByText, getByText } = render(
<Provider>
<Counter />
</Provider>
)

await findByText('count: 0')

fireEvent.click(getByText('button'))
await findByText('count: 1')
expect(proxyState.nested.count).toBe(1)
expect(otherSnap === snapshot(proxyState.other)).toBe(true)

++proxyState.nested.count
await findByText('count: 2')
expect(proxyState.nested.count).toBe(2)
expect(otherSnap === snapshot(proxyState.other)).toBe(true)
})
Loading

1 comment on commit 0e01c87

@vercel
Copy link

@vercel vercel bot commented on 0e01c87 Apr 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.