Showing preview only (475K chars total). Download the full file or copy to clipboard to get everything.
Repository: pmndrs/valtio
Branch: main
Commit: 383ccf185997
Files: 203
Total size: 429.7 KB
Directory structure:
gitextract_vdkwfl3k/
├── .codesandbox/
│ └── ci.json
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── bug-report.yml
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── compressed-size.yml
│ ├── preview-release.yml
│ ├── publish.yml
│ ├── test-multiple-builds.yml
│ ├── test-multiple-versions.yml
│ ├── test-old-typescript.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs/
│ ├── api/
│ │ ├── advanced/
│ │ │ ├── ref.mdx
│ │ │ ├── snapshot.mdx
│ │ │ ├── subscribe-ops.mdx
│ │ │ └── subscribe.mdx
│ │ ├── basic/
│ │ │ ├── proxy.mdx
│ │ │ └── useSnapshot.mdx
│ │ ├── hacks/
│ │ │ ├── getVersion.mdx
│ │ │ └── internals.mdx
│ │ └── utils/
│ │ ├── derive.mdx
│ │ ├── devtools.mdx
│ │ ├── proxyMap.mdx
│ │ ├── proxySet.mdx
│ │ ├── proxyWithHistory.mdx
│ │ ├── subscribeKey.mdx
│ │ ├── unstable_deepProxy.mdx
│ │ └── watch.mdx
│ ├── guides/
│ │ ├── async.mdx
│ │ ├── component-state.mdx
│ │ ├── computed-properties.mdx
│ │ └── migrating-to-v2.mdx
│ ├── how-tos/
│ │ ├── how-to-avoid-rerenders-manually.mdx
│ │ ├── how-to-easily-access-the-state-from-anywhere-in-the-application.mdx
│ │ ├── how-to-organize-actions.mdx
│ │ ├── how-to-persist-states.mdx
│ │ ├── how-to-reset-state.mdx
│ │ ├── how-to-split-and-compose-states.mdx
│ │ ├── how-to-update-values-inside-arrays.mdx
│ │ ├── how-to-use-with-context.mdx
│ │ ├── how-valtio-works.mdx
│ │ └── some-gotchas.mdx
│ ├── introduction/
│ │ └── getting-started.mdx
│ ├── introduction.mdx
│ ├── readme.md
│ └── resources/
│ ├── community.mdx
│ ├── learn.mdx
│ └── libraries.mdx
├── eslint.config.mjs
├── examples/
│ ├── README.md
│ ├── counter/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ ├── prism.css
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── editor-proxyWithHistory/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── photo-booth-vanillajs/
│ │ ├── index.html
│ │ ├── package.json
│ │ └── src/
│ │ ├── index.css
│ │ └── main.js
│ ├── starter/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── subscribe/
│ │ └── index.html
│ ├── todo/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ ├── prism.css
│ │ │ ├── store.ts
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ └── todo-with-proxyMap/
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── AddTodoInput.tsx
│ │ ├── App.tsx
│ │ ├── Filter.tsx
│ │ ├── TodoItem.tsx
│ │ ├── TodoList.tsx
│ │ ├── main.tsx
│ │ ├── react-app-env.d.ts
│ │ ├── store.ts
│ │ └── styles.css
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── package.json
├── pnpm-workspace.yaml
├── rollup.config.mjs
├── src/
│ ├── index.ts
│ ├── react/
│ │ ├── utils/
│ │ │ └── useProxy.ts
│ │ └── utils.ts
│ ├── react.ts
│ ├── types.d.ts
│ ├── utils.ts
│ ├── vanilla/
│ │ ├── utils/
│ │ │ ├── deepClone.ts
│ │ │ ├── deepProxy.ts
│ │ │ ├── devtools.ts
│ │ │ ├── proxyMap.ts
│ │ │ ├── proxySet.ts
│ │ │ ├── subscribeKey.ts
│ │ │ └── watch.ts
│ │ └── utils.ts
│ └── vanilla.ts
├── tests/
│ ├── async.test.tsx
│ ├── basic.test.tsx
│ ├── class.test.tsx
│ ├── deepClone.test.tsx
│ ├── deepProxy.test.tsx
│ ├── devtools.test.tsx
│ ├── getter.test.tsx
│ ├── mapset.test.tsx
│ ├── memoryleaks.test.ts
│ ├── optimization.test.tsx
│ ├── performance.test.tsx
│ ├── proxyMap.bench.ts
│ ├── proxyMap.test.tsx
│ ├── proxySet.test.tsx
│ ├── ref.test.tsx
│ ├── setup.ts
│ ├── snapshot.test.ts
│ ├── subscribe.test.tsx
│ ├── useProxy.test.tsx
│ ├── utils.tsx
│ └── watch.test.tsx
├── tsconfig.json
├── vitest.config.mts
└── website/
├── .eslintrc.json
├── .gitignore
├── README.md
├── _utils/
│ ├── file_helpers.ts
│ └── index.ts
├── components/
│ ├── LandingPage/
│ │ ├── AnimatedShapes.tsx
│ │ ├── CodeExample.tsx
│ │ ├── GettingStarted.tsx
│ │ ├── state.ts
│ │ └── useFloatAnimation.tsx
│ ├── MDXRenderer/
│ │ ├── MDXRenderer.tsx
│ │ └── index.ts
│ ├── SEO/
│ │ ├── SEO.tsx
│ │ └── index.ts
│ ├── ToggleTheme/
│ │ ├── ToggleTheme.tsx
│ │ └── index.ts
│ └── layouts/
│ ├── BasicLayout/
│ │ ├── BasicLayout.tsx
│ │ └── index.ts
│ ├── DocLayout/
│ │ ├── DocLayout.tsx
│ │ └── index.ts
│ ├── Header/
│ │ ├── Header.tsx
│ │ └── index.ts
│ └── index.ts
├── hooks/
│ ├── index.ts
│ ├── useCodesandboxTheme.ts
│ ├── useIsomorphicLayoutEffect.ts
│ └── useTheme.ts
├── lib/
│ ├── mdx.ts
│ └── remarkCodeSandboxURLUpdater.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── docs/
│ │ └── [...slug].tsx
│ └── index.tsx
├── postcss.config.js
├── state/
│ ├── index.ts
│ └── useThemeState.ts
├── styles/
│ ├── landing-page.css
│ ├── prism-theme.css
│ └── tailwind.css
├── tailwind.config.js
├── tsconfig.json
└── types.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .codesandbox/ci.json
================================================
{
"packages": ["dist"],
"sandboxes": [
"new",
"react-typescript-react-ts",
"simple-react-browserify-x9yni",
"simple-snowpack-react-o1gmx",
"react-parcel-onewf"
],
"node": "18"
}
================================================
FILE: .github/DISCUSSION_TEMPLATE/bug-report.yml
================================================
labels: ['bug']
body:
- type: markdown
attributes:
value: If you don't have a reproduction link, please choose a different category.
- type: textarea
attributes:
label: Bug Description
description: Describe the bug you encountered
validations:
required: true
- type: input
attributes:
label: Reproduction Link
description: A link to a [TypeScript Playground](https://www.typescriptlang.org/play), a [StackBlitz Project](https://stackblitz.com/) or something else with a minimal reproduction.
validations:
required: true
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://daishi.gumroad.com/l/learn-valtio'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Assigned issue
about: This is to create a new issue that already has an assignee. Please open a new discussion otherwise.
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Bug Reports
url: https://github.com/pmndrs/valtio/discussions/new?category=bug-report
about: Please post bug reports here.
- name: Questions
url: https://github.com/pmndrs/valtio/discussions/new?category=q-a
about: Please post questions here.
- name: Other Discussions
url: https://github.com/pmndrs/valtio/discussions/new/choose
about: Please post ideas and general discussions here.
================================================
FILE: .github/pull_request_template.md
================================================
## Related Bug Reports or Discussions
Fixes #
## Summary
## Check List
- [ ] `pnpm run fix` for formatting and linting code and docs
================================================
FILE: .github/workflows/compressed-size.yml
================================================
name: Compressed Size
on: [pull_request]
jobs:
compressed_size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- uses: preactjs/compressed-size-action@49c7ff02f46adc39a83c24e91f6110ba8138a19d # v3
with:
pattern: './dist/**/*.{js,mjs}'
================================================
FILE: .github/workflows/preview-release.yml
================================================
name: Preview Release
on: [push, pull_request]
jobs:
preview_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- run: pnpm dlx pkg-pr-new publish './dist' --compact --template './examples/*'
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
release:
types: [published]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- run: npm publish
working-directory: dist
================================================
FILE: .github/workflows/test-multiple-builds.yml
================================================
name: Test Multiple Builds
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test_multiple_builds:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
build: [cjs, esm]
env: [development] # [development, production]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- name: Patch for DEV-ONLY
if: ${{ matrix.env == 'development' }}
run: |
sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it('/" tests/*.ts tests/*.tsx
sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it.skip('/" tests/*.ts tests/*.tsx
- name: Patch for PRD-ONLY
if: ${{ matrix.env == 'production' }}
run: |
sed -i~ "s/it[.a-zA-Z]*('\[PRD-ONLY\]/it('/" tests/*.ts tests/*.tsx
sed -i~ "s/it[.a-zA-Z]*('\[DEV-ONLY\]/it.skip('/" tests/*.ts tests/*.tsx
- name: Patch for CJS
if: ${{ matrix.build == 'cjs' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts
- name: Patch for ESM
if: ${{ matrix.build == 'esm' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/esm\1.mjs')/" vitest.config.mts
sed -i~ "1s/^/import.meta.env.MODE='${NODE_ENV}';/" tests/*.ts tests/*.tsx
env:
NODE_ENV: ${{ matrix.env }}
- name: Test ${{ matrix.build }} ${{ matrix.env }}
run: |
pnpm run test:spec
env:
NODE_ENV: ${{ matrix.env }}
================================================
FILE: .github/workflows/test-multiple-versions.yml
================================================
name: Test Multiple Versions
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test_multiple_versions:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
react:
- 18.0.0
- 18.1.0
- 18.2.0
- 18.3.1
- 19.0.0
- 19.1.0
- 19.2.0
- 19.3.0-canary-e0cc7202-20260227
- 0.0.0-experimental-e0cc7202-20260227
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: pnpm install
- name: Install legacy testing-library
if: ${{ startsWith(matrix.react, '16.') || startsWith(matrix.react, '17.') }}
run: pnpm add -D @testing-library/react@12.1.4
- name: Patch for React 16
if: ${{ startsWith(matrix.react, '16.') }}
run: |
sed -i~ '1s/^/import React from "react";/' tests/*.tsx
sed -i~ 's/"jsx": "react-jsx"/"jsx": "react"/' tsconfig.json
sed -i~ 's/import\.meta\.env[?]\.MODE/"DEVELOPMENT".toLowerCase()/' src/*.ts src/*/*.ts src/*/*/*.ts
- name: Test ${{ matrix.react }}
run: |
pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }}
pnpm run test:spec
================================================
FILE: .github/workflows/test-old-typescript.yml
================================================
name: Test Old TypeScript
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test_old_typescript:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
typescript:
- 5.9.3
- 5.8.3
- 5.7.3
- 5.6.3
- 5.5.4
- 5.4.5
- 5.3.3
- 5.2.2
- 5.1.6
- 5.0.4
- 4.9.5
- 4.8.4
- 4.7.4
- 4.6.4
- 4.5.5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: pnpm install
- run: pnpm run build
- name: Patch for all TS
run: |
sed -i~ 's/"isolatedDeclarations": true,//' tsconfig.json
- name: Patch for v4/v3 TS
if: ${{ startsWith(matrix.typescript, '4.') || startsWith(matrix.typescript, '3.') }}
run: |
sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json
- name: Patch for Old TS
if: ${{ matrix.typescript == '5.5.4' || matrix.typescript == '5.4.5' || matrix.typescript == '5.3.3' || matrix.typescript == '5.2.2' || matrix.typescript == '5.1.6' || matrix.typescript == '5.0.4' || matrix.typescript == '4.9.5' || matrix.typescript == '4.8.4' || matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }}
run: |
sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json
sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json
sed -i~ 's/"valtio": \["\.\/src\/index\.ts"\],/"valtio": [".\/dist\/index.d.ts"],/' tsconfig.json
sed -i~ 's/"valtio\/\*": \["\.\/src\/\*\.ts"\]/"valtio\/*": [".\/dist\/*.d.ts"]/' tsconfig.json
sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json
- name: Patch for Older TS
if: ${{ matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }}
run: |
pnpm json -I -f package.json -e "this.resolutions={}; this.resolutions['@types/node']='18.13.0';"
pnpm add -D @types/node@18.13.0
pnpm add -D vitest@3.2.4 @vitest/coverage-v8@3.2.4 @vitest/ui@3.2.4
- name: Install old TypeScript
run: pnpm add -D typescript@${{ matrix.typescript }}
- name: Test ${{ matrix.typescript }}
run: pnpm run test:types
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- run: pnpm install
- run: pnpm run test:format
- run: pnpm run test:types
- run: pnpm run test:lint
- run: pnpm run test:spec
- run: pnpm run build # we don't have any other workflows to test build
================================================
FILE: .gitignore
================================================
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# production
dist
build
# dotenv environment variables file
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
.DS_Store
.idea
# examples
examples/**/*/package-lock.json
examples/**/*/yarn.lock
examples/**/*/pnpm-lock.yaml
examples/**/*/bun.lockb
================================================
FILE: .prettierignore
================================================
dist
pnpm-lock.yaml
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## General Guideline
### Reporting Issues
If you have found what you think is a bug, please [start a discussion](https://github.com/pmndrs/valtio/discussions/new?category=bug-report).
For any usage questions, please [start a discussion](https://github.com/pmndrs/valtio/discussions/new?category=q-a).
### Suggesting New Features
If you are here to suggest a feature, first [start a discussion](https://github.com/pmndrs/valtio/discussions/new?category=ideas) if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented.
### Committing
We are applying [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) here. In short, that means a commit has to be one of the following types:
Your commit type must be one of the following:
- **feat**: A new feature.
- **fix**: A bug fix.
- **refactor**: A code change that neither fixes a bug nor adds a feature.
- **chore**: Changes to the build process, configuration, dependencies, CI/CD pipelines, or other auxiliary tools and libraries.
- **docs**: Documentation-only changes.
- **test**: Adding missing or correcting existing tests.
If you are unfamiliar with the usage of conventional commits,
the short version is to simply specify the type as a first word,
and follow it with a colon and a space, then start your message
from a lowercase letter, like this:
```
feat: add a 'foo' type support
```
You can also specify the scope of the commit in the parentheses after a type:
```
fix(react): change the 'bar' parameter type
```
### Development
If you would like to contribute by fixing an open issue or developing a new feature you can use this suggested workflow:
#### General
1. Fork this repository.
2. Create a new feature branch based off the `main` branch.
3. Follow the [Core](#Core) and/or the [Documentation](#Documentation) guide below and come back to this once done.
4. Run `pnpm run fix:format` to format the code.
5. Git stage your required changes and commit (review the commit guidelines below).
6. Submit the PR for review.
##### Core
1. Run `pnpm install` to install dependencies.
2. Create failing tests for your fix or new feature in the [`tests`](./tests/) folder.
3. Implement your changes.
4. Run `pnpm run build` to build the library. _(Pro-tip: `pnpm run build-watch` runs the build in watch mode)_
5. Run the tests by running `pnpm run test` and ensure that they pass.
6. You can use `pnpm link` to sym-link this package and test it locally on your own project. Alternatively, you may use CodeSandbox CI's canary releases to test the changes in your own project. (requires a PR to be created first)
7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line.
### Pull Requests
Please try to keep your pull request focused in scope and avoid including unrelated commits.
After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or request improvements, therefore, please check ✅ ["Allow edits from maintainers"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) on your PR.
## Valtio-specific Guideline
##### Documentation
1. Navigate to the [`website`](./website/) folder. (e.g., `cd website`).
2. Run `pnpm install` to install dependencies in the `website` folder.
3. Run `pnpm run dev` to start the dev server.
4. Navigate to [`http://localhost:3000`](http://localhost:3000) to view the documents.
5. Navigate to the [`docs`](./docs/) folder and make necessary changes to the documents.
6. Add your changes to the documents and see them live reloaded in the browser.
7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line.
Thank you for contributing! :heart:
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<img src="logo.svg" alt="valtio">
<br />
<br />
<code>npm install valtio</code> makes proxy-state simple
[](https://github.com/pmndrs/valtio/actions/workflows/test.yml?query=branch%3Amain)
[](https://bundlephobia.com/result?p=valtio)
[](https://www.npmjs.com/package/valtio)
[](https://www.npmjs.com/package/valtio)
[](https://discord.gg/poimandres)
#### Wrap your state object
Valtio turns the object you pass it into a self-aware proxy.
```jsx
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
```
#### Mutate from anywhere
You can make changes to it in the same way you would to a normal js-object.
```jsx
setInterval(() => {
++state.count
}, 1000)
```
#### React via useSnapshot
Create a local snapshot that catches changes. Rule of thumb: read from snapshots in render function, otherwise use the source. The component will only re-render when the parts of the state you access have changed, it is render-optimized.
```jsx
// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
```
<details>
<summary>Note for TypeScript users: Return type of useSnapshot can be too strict.</summary>
The `snap` variable returned by `useSnapshot` is a (deeply) read-only object.
Its type has `readonly` attribute, which may be too strict for some use cases.
To mitigate typing difficulties, you might want to loosen the type definition:
```ts
declare module 'valtio' {
function useSnapshot<T extends object>(p: T): T
}
```
See [#327](https://github.com/pmndrs/valtio/issues/327) for more information.
</details>
<details>
<summary>Note: useSnapshot returns a new proxy for render optimization.</summary>
Internally, `useSnapshot` calls `snapshot` in valtio/vanilla,
and wraps the snapshot object with another proxy to detect property access.
This feature is based on [proxy-compare](https://github.com/dai-shi/proxy-compare).
Two kinds of proxies are used for different purposes:
- `proxy()` from `valtio/vanilla` is for mutation tracking or write tracking.
- `createProxy()` from `proxy-compare` is for usage tracking or read tracking.
</details>
<details>
<summary>Use of <code>this</code> is for expert users.</summary>
Valtio tries best to handle `this` behavior
but it's hard to understand without familiarity.
```js
const state = proxy({
count: 0,
inc() {
++this.count
},
})
state.inc() // `this` points to `state` and it works fine
const snap = useSnapshot(state)
snap.inc() // `this` points to `snap` and it doesn't work because snapshot is frozen
```
To avoid this pitfall, the recommended pattern is not to use `this` and prefer arrow function.
```js
const state = proxy({
count: 0,
inc: () => {
++state.count
},
})
```
</details>
If you are new to this, it's highly recommended to use
[eslint-plugin-valtio](https://github.com/pmndrs/eslint-plugin-valtio).
#### Subscribe from anywhere
You can access state outside of your components and subscribe to changes.
```jsx
import { subscribe } from 'valtio'
// Subscribe to all state changes
const unsubscribe = subscribe(state, () =>
console.log('state has changed to', state),
)
// Unsubscribe by calling the result
unsubscribe()
```
You can also subscribe to a portion of state.
```jsx
const state = proxy({ obj: { foo: 'bar' }, arr: ['hello'] })
subscribe(state.obj, () => console.log('state.obj has changed to', state.obj))
state.obj.foo = 'baz'
subscribe(state.arr, () => console.log('state.arr has changed to', state.arr))
state.arr.push('world')
```
To subscribe to a primitive value of state, consider `subscribeKey` in utils.
```jsx
import { subscribeKey } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
subscribeKey(state, 'count', (v) =>
console.log('state.count has changed to', v),
)
```
There is another util `watch` which might be convenient in some cases.
```jsx
import { watch } from 'valtio/utils'
const state = proxy({ count: 0 })
const stop = watch((get) => {
console.log('state has changed to', get(state)) // auto-subscribe on use
})
```
#### Suspend your components
Valtio is compatible with React 19 `use` hook. This eliminates all the async back-and-forth, you can access your data directly while the parent is responsible for fallback state and error handling.
```jsx
import { use } from 'react' // React 19
// import { use } from 'react18-use' // React 18
const state = proxy({ post: fetch(url).then((res) => res.json()) })
function Post() {
const snap = useSnapshot(state)
return <div>{use(snap.post).title}</div>
}
function App() {
return (
<Suspense fallback={<span>waiting...</span>}>
<Post />
</Suspense>
)
}
```
It still suffers from "de-opt", which prevents `useTransition` to work well. To mitigate it, there is a third-party library [use-valtio](https://github.com/valtiojs/use-valtio).
#### Holding objects in state without tracking them
This may be useful if you have large, nested objects with accessors that you don't want to proxy. `ref` allows you to keep these objects inside the state model.
See [#61](https://github.com/pmndrs/valtio/issues/61) and [#178](https://github.com/pmndrs/valtio/issues/178) for more information.
```js
import { proxy, ref } from 'valtio'
const state = proxy({
count: 0,
dom: ref(document.body),
})
```
#### Update transiently (for often occurring state-changes)
You can read state in a component without causing re-render.
```jsx
function Foo() {
const { count, text } = state
// ...
```
Or, you can have more control with subscribing in useEffect.
```jsx
function Foo() {
const total = useRef(0)
useEffect(() => subscribe(state.arr, () => {
total.current = state.arr.reduce((p, c) => p + c)
}), [])
// ...
```
#### Update synchronously
By default, state mutations are batched before triggering re-render.
Sometimes, we want to disable the batching.
The known use case of this is `<input>` [#270](https://github.com/pmndrs/valtio/issues/270).
```jsx
function TextBox() {
const snap = useSnapshot(state, { sync: true })
return (
<input value={snap.text} onChange={(e) => (state.text = e.target.value)} />
)
}
```
#### Dev tools
You can use [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools) for plain objects and arrays.
```jsx
import { devtools } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
const unsub = devtools(state, { name: 'state name', enabled: true })
```
<details>
<summary>Manipulating state with Redux DevTools</summary>
The screenshot below shows how to use Redux DevTools to manipulate state. First select the object from the instances drop down. Then type in a JSON object to dispatch. Then click "Dispatch". Notice how it changes the state.
<br/>
<img width="564" alt="image" src="https://user-images.githubusercontent.com/6372489/141134955-26e9ffce-1e2a-4c8c-a9b3-d9da739610fe.png">
</details>
#### Use it vanilla
Valtio is not tied to React, you can use it in vanilla-js.
```jsx
import { proxy, subscribe, snapshot } from 'valtio/vanilla'
// import { ... } from 'valtio/vanilla/utils'
const state = proxy({ count: 0, text: 'hello' })
subscribe(state, () => {
console.log('state is mutated')
const obj = snapshot(state) // A snapshot is an immutable object
})
```
#### `useProxy` util
While the separation of proxy state and its snapshot is important,
it's confusing for beginners.
We have a convenient util to improve developer experience. useProxy returns shallow proxy state and its snapshot, meaning you can only mutate on root level.
```js
import { useProxy } from 'valtio/utils'
const state = proxy({ count: 1 })
const Component = () => {
// useProxy returns a special proxy that can be used both in render and callbacks
// The special proxy has to be used directly in a function scope. You can't destructure it outside the scope.
const $state = useProxy(state)
return (
<div>
{$state.count}
<button onClick={() => ++$state.count}>+1</button>
</div>
)
}
```
#### Computed properties
You can define computed properties with object getters.
```js
const state = proxy({
count: 1,
get doubled() {
return this.count * 2
},
})
```
Consider it as an advanced usage, because the behavior of `this` is sometimes confusing.
For more information, check out [this guide](./docs/guides/computed-properties.mdx).
#### `proxySet` util
This is to create a proxy which mimic the native Set behavior. The API is the same as Set API
```js
import { proxySet } from 'valtio/utils'
const state = proxySet([1, 2, 3])
//can be used inside a proxy as well
//const state = proxy({
// count: 1,
// set: proxySet()
//})
state.add(4)
state.delete(1)
state.forEach((v) => console.log(v)) // 2,3,4
```
#### `proxyMap` util
This is to create a proxy which emulate the native Map behavior. The API is the same as Map API
```js
import { proxyMap } from 'valtio/utils'
const state = proxyMap([
['key', 'value'],
['key2', 'value2'],
])
state.set('key', 'value')
state.delete('key')
state.get('key') // ---> value
state.forEach((value, key) => console.log(key, value)) // ---> "key", "value", "key2", "value2"
```
#### Compatibility
Valtio v2 works with React 18 and up.
It only depends on `react` and works with any
renderers such as `react-dom`, `react-native`, `react-three-fiber`, and so on.
Valtio works on Node.js, Next.js and other frameworks.
Valtio also works without React. See [vanilla](#use-it-vanilla).
#### Plugins
- [eslint-plugin-valtio](https://github.com/pmndrs/eslint-plugin-valtio)
#### Recipes
Valtio is unopinionated about best practices.
The community is working on recipes.
- [How to organize actions](https://github.com/pmndrs/valtio/blob/main/docs/how-tos/how-to-organize-actions.mdx)
- [How to persist states](https://github.com/pmndrs/valtio/blob/main/docs/how-tos/how-to-persist-states.mdx)
- [How to use with context](https://github.com/pmndrs/valtio/blob/main/docs/how-tos/how-to-use-with-context.mdx)
- [How to split and compose states](https://github.com/pmndrs/valtio/blob/main/docs/how-tos/how-to-split-and-compose-states.mdx)
================================================
FILE: docs/api/advanced/ref.mdx
================================================
---
title: 'ref'
section: 'API'
subSection: 'Advanced'
description: 'Create a ref object'
---
# `ref`
## A `ref` allows unproxied state in a Valtio proxy
A `ref` is useful in the rare instances you need to nest an object in a `proxy` that is not wrapped in an inner proxy and, therefore, is not tracked.
```js
const store = proxy({
users: [
{ id: 1, name: 'Juho', uploads: ref([]) },
]
})
})
```
Once an object is wrapped in a `ref`, it should be mutated without reassigning the object or rewrapping in a new `ref`.
```js
// ✅ do mutate
store.users[0].uploads.push({ id: 1, name: 'Juho' })
// ✅ do reset
store.users[0].uploads.splice(0)
// ❌ don't reassign
store.users[0].uploads = []
```
A `ref` should also not be used as the only state in a proxy, making the proxy usage pointless.
## Codesandbox demo
https://codesandbox.io/s/valtio-file-load-demo-oo2yzn
================================================
FILE: docs/api/advanced/snapshot.mdx
================================================
---
title: 'snapshot'
section: 'API'
subSection: 'Advanced'
description: 'Create a snapshot of current state'
---
# `snapshot`
`snapshot` takes a proxy and returns an immutable object, unwrapped from the proxy.
Immutability is achieved by _efficiently_ deep copying & freezing the object (see the [Copy on Write](#copy-on-write) section for details).
Briefly, in sequential `snapshot` calls, when the values in the proxy have not changed, the previous snapshot's object reference is returned. This allows for shallow comparison in render functions, preventing spurious renders.
Snapshots also throw promises, making them work with React Suspense.
```js
import { proxy, snapshot } from 'valtio'
const store = proxy({ name: 'Mika' })
const snap1 = snapshot(store) // an efficient copy of the current store values, unproxied
const snap2 = snapshot(store)
console.log(snap1 === snap2) // true, no need to re-render
store.name = 'Hanna'
const snap3 = snapshot(store)
console.log(snap1 === snap3) // false, should re-render
```
## Copy on Write
Even though snapshots are a deep copy of the entire state, they use a lazy copy-on-write mechanism for updates, so in practice they are quick to maintain.
For example, if we have a nested object of:
```js
const author = proxy({
firstName: 'f',
lastName: 'l',
books: [{ title: 't1' }, { title: 't2' }],
})
const s1 = snapshot(author)
```
The first `snapshot` call creates four new instances:
- one for the author,
- one for the books array, and
- two for the book objects.
When we mutate the 2nd book, and take a new `snapshot`:
```js
author.books[1].title = 't2b'
const s2 = snapshot(author)
```
Then `s2` will have a new copy of the 2nd book, but reuse the existing snapshot of the unchanged 1st book.
```js
console.log(s1 === s2) // false
console.log(s1.books === s2.books) // false
console.log(s1.books[0] === s2.books[0]) // true
console.log(s1.books[1] === s2.books[1]) // false
```
Even though this example only reused one of the four existing snapshot instances, it shows that the cost of maintaining snapshots is based on the _depth_ of your state tree (which is typically low, like author to book to book reviews is three levels), and not the _breadth_ (1000s of books).
## Classes
Snapshots maintain the original objects' prototypes, so methods and getters work, and correctly evaluate against the snapshot's frozen state.
```js
import { proxy, snapshot } from 'valtio'
class Author {
firstName = 'f'
lastName = 'l'
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
const state = proxy(new Author())
const snap = snapshot(state)
// the snapshot has the Author prototype
console.log(snap instanceof Author) // true
state.firstName = 'f2'
// Invocations use the snapshot's state, e.g. this is still 'f' because
// inside `fullName`, `this` will be the frozen snapshot instance and not
// the mutable state proxy
console.log(snap.fullName()) // 'f l'
```
Note that the results of getters and methods are not cached, and are re-evaluated on every call.
This should be fine, because the expectation is that they execute very quickly (faster than the overhead of caching them would be worth) and are also deterministic, so the return value is based only on the already-frozen snapshot state.
## Vanilla JavaScript
In VanillaJS, `snapshot` is not necessary to access proxied object values, inside or outside of subscribe. However, it is useful, for example, to keep a serializable list of un-proxied objects or check if objects have changed. It also resolves promises.
<br />
<blockquote className="tip">
💡 Tip
If you are using valtio outside of react, import from `valtio/vanilla`
```js
import { proxy, snapshot } from 'valtio/vanilla'
```
</blockquote>
================================================
FILE: docs/api/advanced/subscribe-ops.mdx
================================================
---
title: 'subscribe-ops'
section: 'API'
subSection: 'Advanced'
description: 'Fine-grained mutation tracking with operational transformations (Ops)'
---
# Subscribe Ops
By default, Valtio's `subscribe` notify you that _something_ has changed in the state proxy. However, you can opt-in to receiving **Ops** (Operations), which are detailed descriptions of exactly what was modified.
## What are Ops?
Ops are granular mutation records. When a proxy is updated, Valtio can generate a description of the change as a tuple.
### Op Types
- **`set`**: `[op: 'set', path: Path, value: unknown, prevValue: unknown]`
- Triggered when a property is assigned a new value.
- **`delete`**: `[op: 'delete', path: Path, prevValue: unknown]`
- Triggered when a property is deleted.
- **`resolve`**: `[op: 'resolve', path: Path, value: unknown]`
- Triggered when a promise in the state is fulfilled.
- **`reject`**: `[op: 'reject', path: Path, error: unknown]`
- Triggered when a promise in the state is rejected.
The `Path` is an array of strings or symbols representing the nested location of the property (e.g., `['user', 'profile', 'name']`).
## How to use Ops
The "ops" feature is an **opt-in** feature because it introduces a small performance overhead for tracking and allocating these operation objects.
### 1. Enabling Ops
You must explicitly enable op-tracking using the unstable API:
```javascript
import { unstable_enableOp } from 'valtio'
// Enable globally
unstable_enableOp(true)
```
### 2. Receiving Ops in `subscribe`
Once enabled, the `subscribe` callback receives an array of these operations as its first argument.
```javascript
import { proxy, subscribe, unstable_enableOp } from 'valtio'
unstable_enableOp(true)
const state = proxy({ count: 0, text: 'hello' })
subscribe(state, (ops) => {
ops.forEach((op) => {
const [action, path, value, prevValue] = op
console.log(`Action: ${action} at ${path.join('.')}`)
console.log(`New value:`, value)
console.log(`Previous value:`, prevValue)
})
})
state.count++
// Output:
// Action: set at count
// New value: 1
// Previous value: 0
```
> **Note**: If `unstable_enableOp(true)` is not called, the `ops` argument will be an empty array or `undefined`.
## Use Cases
While standard `subscribe` is sufficient for most React UI updates, Ops are useful for specific advanced scenarios:
1. **Network Synchronization**: Instead of sending the entire state over the wire, you can send only the `ops` (patches). This significantly reduces bandwidth consumption in distributed applications.
2. **Undo/Redo History**: Use the `prevValue` provided in `set` and `delete` ops to easily revert state changes.
3. **Audit Logs & Debugging**: Track a sequence of user-driven mutations for analytics or time-travel debugging.
4. **Devtools Integration**: Powering custom development tools that need to visualize state transitions.
## Performance Considerations
Enabling Ops has a small overhead cost. For every mutation, Valtio must:
- Detect the change type.
- Construct the `Path` array.
- Allocate the `Op` tuple.
In high-frequency update scenarios (e.g., animations, canvas interactions moving hundreds of objects per frame), this can lead to:
- Increased garbage collection (GC) pressure due to object allocations.
- A measurable drop in frame rates (FPS).
**Recommendation**: Only enable `unstable_enableOp` if your application actually consumes the granular `ops` data.
================================================
FILE: docs/api/advanced/subscribe.mdx
================================================
---
title: 'subscribe'
section: 'API'
subSection: 'Advanced'
description: 'Subscribe to a current state/object'
---
# `subscribe`
## Subscribe from anywhere
You can access state outside your components and subscribe to changes.
```jsx
import { proxy, subscribe } from 'valtio'
const state = proxy({ count: 0 })
// Subscribe to all changes to the state proxy (and its child proxies)
const unsubscribe = subscribe(state, () =>
console.log('state has changed to', state),
)
// Unsubscribe by calling the result
unsubscribe()
```
You can also subscribe to a portion of state.
```jsx
const state = proxy({ obj: { foo: 'bar' }, arr: ['hello'] })
subscribe(state.obj, () => console.log('state.obj has changed to', state.obj))
state.obj.foo = 'baz'
subscribe(state.arr, () => console.log('state.arr has changed to', state.arr))
state.arr.push('world')
```
## Codesandbox demo in VanillaJS
https://codesandbox.io/s/valtio-photo-booth-demo-forked-xp8hs?file=/src/main.js
## Advanced: Listening to "Ops"
The `subscribe` callback can also receive **Ops** (Operations), which are detailed records of exactly what changed (e.g., which property was set or deleted). This is useful for advanced scenarios like synchronization or undo/redo.
Because tracking these operations has a small performance cost, they are disabled by default. For more details on how to enable and use them, check out [Subscribe Ops](./subscribe-ops).
================================================
FILE: docs/api/basic/proxy.mdx
================================================
---
title: 'proxy'
section: 'API'
subSection: 'Basic'
description: 'Create a proxy object.'
---
# `proxy`
The `proxy` tracks changes to the original object and all nested objects, notifying listeners when an object is modified.
```js
import { proxy } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
```
## Mutate from anywhere
You can make changes to it in the same way you would to a normal js-object.
```js
setInterval(() => {
++state.count
}, 1000)
```
## Optimizations: noop and batching
Updates that set the value of a property to the same value are ignored. Subscribers will not be informed of a version change.
```js
const state = proxy({ count: 0 })
state.count = 0 // has no effect
```
Multiple changes in the same event loop tick will be batched together. Subscribers will be notified of a single version change.
```js
const state = proxy({ count: 0, text: 'hello' })
// subscribers will be notified once after both mutations
state.count = 1
state.text = 'world'
```
## Nested proxies
Proxies can be nested in other `proxy` objects and updated as a whole.
```jsx
import { proxy, useSnapshot } from 'valtio'
const personState = proxy({ name: 'Timo', role: 'admin' })
const authState = proxy({ status: 'loggedIn', user: personState })
authState.user.name = 'Nina'
```
## Promises in proxies
See [`async`](../../guides/async) for more details.
```jsx
import { proxy } from 'valtio'
const bombState = proxy({
explosion: new Promise((resolve) => setTimeout(() => resolve('Boom!'), 3000)),
})
```
## Gotchas
If you reassign the proxy to an entirely new object, it will stop working because you are replacing the proxied object with a new object reference.
```jsx
let state = proxy({ user: { name: 'Timo' } })
subscribe(state, () => {
console.log(state.user.name)
})
// will not notify subscribers
state = { user: { name: 'Nina' } }
// instead
let state = proxy({ user: { name: 'Timo' } })
subscribe(state, () => {
console.log(state.user.name) // logs "Nina"
})
// will notify subscribers
state.user.name = 'Nina'
```
Not everything can be proxied. Generally, you are safe if it is serializable. Classes can also be proxied. But avoid special objects.
```jsx
// these won't work - changes to these objects won't cause updates
// to store state that is unproxied see the docs on ref
const state = proxy({
chart: d3.select('#chart'),
component: React.createElement('div'),
map: new Map(), // see proxyMap
storage: localStorage,
})
// this will work
class User {
first = null
last = null
constructor(first, last) {
this.first = first
this.last = last
}
greet() {
return `Hi ${this.first}!`
}
get fullName() {
return `${this.first} ${this.last}`
}
}
const state = proxy(new User('Timo', 'Kivinen'))
```
================================================
FILE: docs/api/basic/useSnapshot.mdx
================================================
---
title: 'useSnapshot'
section: 'API'
subSection: 'Basic'
description: 'Create a local snapshot that catches changes.'
---
# `useSnapshot`
Create a local `snapshot` that catches changes.
Normally, Valtio's snapshots (created via <a href="/docs/api/advanced/snapshot">`snapshot()`</a>) are recreated on _any_ change to a proxy, or any of its child proxies.
However `useSnapshot` wraps the Valtio snapshot in an access-tracking proxy. This is to make sure your component is render optimized, i.e. it will only re-render if keys that it (or its child components) specifically accessed has changed, and not on every single change to the proxy.
## Usage
### Read from snapshots in render, use the proxy in callbacks
Snapshots are read-only to render the JSX from their consistent view of the data.
Mutations, and also any reads in callbacks that make mutations, need to be made via the proxy, so that the callback reads & writes the latest value.
```jsx
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button
onClick={() => {
// also read from the state proxy in callbacks
if (state.count < 10) {
++state.count
}
}}
>
+1
</button>
</div>
)
}
```
### Parent/Child Components
If you have a parent component use `useSnapshot`, it can pass snapshots to child components and the parent & children will re-render when the snapshot changes.
For example:
```jsx
const state = proxy({
books: [
{ id: 1, title: 'b1' },
{ id: 2, title: 'b2' },
],
})
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book) => (
<Book key={book.id} book={book} />
))}
</div>
)
}
function BookView({ book }) {
// book is a snapshot
return <div>{book.title}</div>
}
```
If book 2's title is changed, a new `snap` is created and the `AuthorView` and `BookView` components will re-render.
Note if `BookView` is `React.memo`d, the 1st `BookView` will not re-render, b/c the 1st `Book` snapshot will be the same instance, as only the 2nd `Book` was mutated (the root `Author` snapshot will also be updated since the list of `books` has changed).
### Child Components Making Mutations
The above approach works if `BookView` is read-only; if your child component needs to make mutations, then you'll need to pass the proxy:
```jsx
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book, i) => (
<Book key={book.id} book={state.books[i]} />
))}
</div>
)
}
function BookView({ book }) {
// book is the proxy, so we can re-snap it + mutate it
const snap = useSnapshot(book)
return <div onClick={() => book.updateTitle()}>{snap.title}</div>
}
```
Or you can pass both the snapshot and proxy together, if you don't want to call `useSnapshot` in the child component:
```jsx
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book, i) => (
<Book key={book.id} bookProxy={state.books[i]} bookSnapshot={book} />
))}
</div>
)
}
```
There should be no performance difference between these two approaches.
### Read only what you need
Every object inside your proxy also becomes a proxy (if you don't use <a href="/docs/advanced/ref">`ref()`</a>). So you can also use them to create
a local snapshot.
```jsx
function ProfileName() {
const snap = useSnapshot(state.profile)
return <div>{snap.name}</div>
}
```
## Gotchas
Beware of replacing the child proxy with something else, breaking your `snapshot`. This will replace the reference of the proxy with what you assign it to which removes the proxy's traps. You can see an example below.
```js
console.log(state)
{
profile: {
name: 'valtio'
}
}
childState = state.profile
console.log(childState)
{
name: 'valtio'
}
state.profile.name = 'react'
console.log(childState)
{
name: 'react'
}
state.profile = { name: 'new name' }
console.log(childState)
{
name: 'react'
}
console.log(state)
{
profile: {
name: 'new name'
}
}
```
`useSnapshot()` depends on the original reference of the child proxy so if you replace it with a new one, the component
that is subscribed to the old proxy won't receive new updates because it is still subscribed to the old one.
In this case, we recommend one of the approaches below. In neither example do you need to worry about re-renders because it is render-optimized.
```jsx
const snap = useSnapshot(state)
return <div>{snap.profile.name}</div>
```
```jsx
const { profile } = useSnapshot(state)
return <div>{profile.name}</div>
```
## Dev Mode Debug Values
In dev mode, `useSnapshot` uses React's `useDebugValue` to output a list of fields that were accessed during rendering, i.e. which specific fields will trigger re-render when the tracking proxy changes.
<br />
<blockquote className="important">
!! There are two disclaimers to using the debug value
1. Due to the way `useSnapshot` uses a proxy to recorded accesses _after_ `useSnapshot` has returned, the fields listed in `useDebugValue` are technically from the _previous_ render.
2. Object getter and class getter calls are not included in the `useDebugValue` output, but don't worry, they are actually correctly tracked internally and correctly trigger re-renders when changed.
</blockquote>
<br />
<br />
## Codesandbox demo
https://codesandbox.io/s/ping-pong-with-valtio-wb25s?file=/src/App.js
================================================
FILE: docs/api/hacks/getVersion.mdx
================================================
---
title: 'getVersion'
section: 'API'
subSection: 'Hacks'
description: ''
---
# `getVersion`
In Valtio, updates to proxied objects are tracked internally with a version number.
Every mutation to a proxy increases a global version number, and assigns the just mutated proxy, and any parent proxies (which automatically subscribe to their child proxies), to the latest version number.
This is how `snapshot` knows whether a new snapshot is necessary: has my proxy's version number changed since the last snapshot?
Given its importance to valtio's internal behavior, the `getVersion` helper can be used to check if a proxied object has been updated, but this is not typically useful or recommended to use in application code because `snapshot` and `useSnapshot` already handle version tracking internally.
```js
getVersion(proxyObject)
```
================================================
FILE: docs/api/hacks/internals.mdx
================================================
---
title: 'Internals'
section: 'API'
subSection: 'Hacks'
description: ''
---
Valtio exposes some internal capabilities.
This isn't for app developers. Use it at your own risk.
# `unstable_getInternalStates`
This function exposes the internal states. Modifying such states may result in wrong behaviors. You need to understand the [source code](https://github.com/pmndrs/valtio/blob/main/src/vanilla.ts) thoroughly to use it.
# `unstable_replaceInternalFunction`
This function exposes a way to replace some internal functions. It can easily break things. You need to understand the [source code](https://github.com/pmndrs/valtio/blob/main/src/vanilla.ts) thoroughly to use it.
================================================
FILE: docs/api/utils/derive.mdx
================================================
---
title: 'derive'
section: 'API'
subSection: 'Utils'
description: 'create a new proxy derived from others'
---
## `derive`
> **⚠️ Deprecated**
>
> This package is no longer maintained. Please migrate to
> [valtio-reactive](https://github.com/valtiojs/valtio-reactive).
## Installation
```bash
npm install derive-valtio
```
#### create a new proxy derived from others
You can subscribe to some proxies using `get` to create snapshots used to compute new values.
```js
import { derive } from 'derive-valtio'
// create a base proxy
const state = proxy({
count: 1,
})
// create a derived proxy
const derived = derive({
doubled: (get) => get(state).count * 2,
})
// alternatively, attach derived properties to an existing proxy
derive(
{
tripled: (get) => get(state).count * 3,
},
{
proxy: state,
},
)
```
## `underive`
## Stop evaluating
In some cases you may want to unsubscribe after deriving a proxy. To do so, use the `underive` util. You may also pass keys to indicate which properties you want to unsubscribe. If you specify `delete` option, it will delete the properties
and you can attach new derived properties.
```js
import { derive, underive } from 'derive-valtio'
const state = proxy({
count: 1,
})
const derivedState = derive({
doubled: (get) => get(state).count * 2,
})
underive(derivedState)
```
## Usage patterns
### Re-computes when unrelated properties change
With `derive`, the computation will occur if any property of the base proxy changes. Example:
```javascript
const baseProxy = proxy({
counter1: 0,
counter2: 0,
counter3: 0,
counter4: 0,
})
const countersOneAndTwoSelectors = derive({
sum: (get) => get(baseProxy).counter1 + get(baseProxy).counter2,
})
```
In this example if `baseProxy.counter3` or `baseProxy.counter4` are changed, `countersOneAndTwoSelectors` will re-compute all of the keys in it.
### `get` sub-objects due to update of unrelated proxies on parent proxy
As noted on this page, re-computation occurs when unrelated properties change. It is possible to use `get` on sub-objects. This has the benefit of not re-computing when properties of the parent object are updated. Example:
```javascript
const baseProxy = proxy({
counter1And2: {
counter1: 0,
counter2: 0,
},
counter3: 0,
counter4: 0,
})
const countersOneAndTwoSelectors = derive({
sum: (get) =>
get(baseProxy.counter1And2).counter1 + get(baseProxy.counter1And2).counter2,
})
```
Now even if `counter3` and `counter4` are updated, it will not cause re-computation of `countersOneAndTwoSelectors`.
================================================
FILE: docs/api/utils/devtools.mdx
================================================
---
title: 'devtools'
section: 'API'
subSection: 'Utils'
description: 'Use the Redux DevTools extension with Valtio'
---
# devtools
#### Dev tools
You can use [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools) for plain objects and arrays.
```jsx
import { devtools } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
const unsub = devtools(state, { name: 'state name', enabled: true })
```
<details>
<summary>Manipulating state with Redux DevTools</summary>
The screenshot below shows how to use Redux DevTools to manipulate state. First select the object from the instances drop down. Then type in a JSON object to dispatch. Then click "Dispatch". Notice how it changes the state.
<img width="564" alt="image" src="https://user-images.githubusercontent.com/6372489/141134955-26e9ffce-1e2a-4c8c-a9b3-d9da739610fe.png"/>
</details>
#### Use it with vanilla JS
Valtio is not tied to React, you can use it in vanillaJS.
```jsx
import { proxy, subscribe, snapshot } from 'valtio/vanilla'
const state = proxy({ count: 0, text: 'hello' })
subscribe(state, () => {
console.log('state is mutated')
const obj = snapshot(state) // A snapshot is an immutable object
})
```
#### Use it with TypeScript
It's recommended to install and import types from `@redux-devtools/extension` to get types correctly.
```ts
import type {} from '@redux-devtools/extension'
import { devtools } from 'valtio/utils'
```
================================================
FILE: docs/api/utils/proxyMap.mdx
================================================
---
title: 'proxyMap'
section: 'API'
subSection: 'Utils'
description: ''
---
# `proxyMap`
## Reasoning
Native `Maps` store their data in internal slots which are not observable. This means that `valtio` cannot track changes to the data inside of a native `Map`. `proxyMap` is a utility that allows you to create a proxy that mimics the behavior of a `Map` while still allowing valtio to track changes to the data.
## When to use `proxyMap`
`proxyMap` is useful when you need the flexibility of a `Map` but still want to track changes to the data. It can be useful if you don't know the structure of the data you'll be working with and this data may have non-primitive values as keys (e.g. objects, arrays, etc.). In this case, you can use `proxyMap` to create a proxy that mimics the behavior of a `Map` while still allowing valtio to track changes to the data. If your data can be represented as a simple object, you should use `proxy` with a simple object instead. It is more performant and easier to use.
## Use a js Map with Valtio
This utility creates a proxy which mimics the native Map behavior. The API is the same as the Map API.
```js
import { proxyMap } from 'valtio/utils'
const state = proxyMap()
state.size // ---> 0
state.set(1, 'hello')
state.size // ---> 1
state.delete(1)
state.size // ---> 0
```
## Nesting
It can be used inside a `proxy` as well.
```js
import { proxyMap } from 'valtio/utils'
const state = proxy({
count: 1,
map: proxyMap(),
})
```
When using an object as a key, you can wrap it with `ref` so it's not proxied. This is useful if you want to preserve the key equality
```js
import { proxyMap } from 'valtio/utils'
// with ref
const key = ref({})
state.set(key, 'hello')
state.get(key) //hello
// without ref
const key = {}
state.set(key, 'value')
state.get(key) //undefined
```
## `isProxyMap`
If you want to check if an object is a proxyMap, you can use the `isProxyMap` function.
```js
import { proxy, ref } from 'valtio'
import { proxyMap, isProxyMap } from 'valtio/utils'
const state = proxy({
nativeMap: ref(new Map(/*...*/)),
proxyMap: proxyMap(),
})
isProxyMap(state.nativeMap) // false
isProxyMap(state.proxyMap) // true
```
## Codesandbox demo
https://codesandbox.io/s/github/pmndrs/valtio/tree/main/examples/todo-with-proxyMap
================================================
FILE: docs/api/utils/proxySet.mdx
================================================
---
title: 'proxySet'
section: 'API'
subSection: 'Utils'
description: ''
---
# `proxySet`
## Reasoning
Native `Sets` store their data in internal slots which are not observable. This means that `valtio` cannot track changes to the data inside of a native `Set`. `proxySet` is a utility that allows you to create a proxy that mimics the behavior of a `Set` while still allowing valtio to track changes to the data.
## When to use `proxySet`
`proxySet` is useful when you need the functionality of a `Set` but still want to track changes to the data. `proxySet` can be useful if you're wanting to store unique values or if you want to perform mathematical `Set` operations on the data, such as union, intersection, or difference. `proxySet` supports all of the new methods introduced to `Set`:
- `intersection`
- `union`
- `difference`
- `symmetricDifference`
- `isSubsetOf`
- `isSupersetOf`
- `isDisjointFrom`
You can see a full list of the methods supported by `proxySet` in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
If your data can be represented as a simple array or object, and you have no need for the additional functionality provided by `proxySet`, you should use `proxy` with a simple array or object instead. It is more performant and easier to use.
## Use a js `Set` with Valtio
This utility creates a proxy which mimics the native `Set` behavior. The API is the same as the native `Set` API.
```js
import { proxySet } from 'valtio/utils'
const state = proxySet([1, 2, 3])
state.add(4)
state.delete(1)
state.forEach((v) => console.log(v)) // ---> 2,3,4
```
## Nesting
It can be used inside a `proxy` as well.
```js
import { proxySet } from 'valtio/utils'
const state = proxy({
count: 1,
set: proxySet(),
})
```
## `isProxySet`
If you want to check if an object is a proxyMap, you can use the `isProxySet` function.
```js
import { proxy, ref } from 'valtio'
import { proxySet, isProxySet } from 'valtio/utils'
const state = proxy({
nativeSet: ref(new Set(/*...*/)),
proxySet: proxySet(),
})
isProxySet(state.nativeSet) // false
isProxySet(state.proxySet) // true
```
================================================
FILE: docs/api/utils/proxyWithHistory.mdx
================================================
---
title: 'proxyWithHistory'
section: 'API'
subSection: 'Utils'
description: ''
---
# `proxyWithHistory`
## Keep a history of snapshots
This is a utility function to create a proxy with snapshot history.
```js
import { proxyWithHistory } from 'valtio-history'
const state = proxyWithHistory({ count: 0 })
console.log(state.value) // ---> { count: 0 }
state.value.count += 1
console.log(state.value) // ---> { count: 1 }
state.undo()
console.log(state.value) // ---> { count: 0 }
state.redo()
console.log(state.value) // ---> { count: 1 }
```
## Codesandbox demo
https://codesandbox.io/s/valtio-history-example-v0-m353xc?file=/src/App.tsx
================================================
FILE: docs/api/utils/subscribeKey.mdx
================================================
---
title: 'subscribeKey'
section: 'API'
subSection: 'Utils'
description: ''
---
# `subscribeKey`
To subscribe to a primitive value in proxy state, consider `subscribeKey`.
```js
import { subscribeKey } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
subscribeKey(state, 'count', (v) =>
console.log('state.count has changed to', v),
)
```
## Codesandbox demo
https://codesandbox.io/s/dynamic-ui-with-valtio-9gme46?file=/src/ComplexCounter.tsx
================================================
FILE: docs/api/utils/unstable_deepProxy.mdx
================================================
---
title: 'unstable_deepProxy'
section: 'API'
subSection: 'Utils'
description: ''
---
# `unstable_deepProxy`
You can use this utility to recursively go through an object and proxy all proxiable objects with the exception of anything marked with `ref()`. `Map`s will become `proxyMap`s and `Set`s will become `proxySet`s.
```js
import {
unstable_deepProxy as deepProxy,
isProxyMap,
isProxySet,
} from 'valtio/utils'
const obj = {
mySet: new Set(),
myMap: new Map(),
sub: {
foo: 'bar',
},
}
const clonedProxy = deepProxy(obj)
console.log(isProxyMap(clonedProxy.myMap)) // true
console.log(isProxySet(clonedProxy.mySet)) // true
```
================================================
FILE: docs/api/utils/watch.mdx
================================================
---
title: 'watch'
section: 'API'
subSection: 'Utils'
description: 'watch for changes.'
---
# `watch`
> **⚠️ Deprecated**
>
> This util is no longer maintained. Please migrate to
> [valtio-reactive](https://github.com/valtiojs/valtio-reactive).
## Subscription via a getter
This utility supports subscribing to multiple proxy objects (unlike `subscribe` which listens to only a single proxy). Proxy objects are subscribed with a `get` function passed to the callback.
Any changes to the proxy object (or its child proxies) will rerun the callback.
Also note the callback will run once immediately when `watch` is called, even if the proxies have not been yet mutated, to establish the initial subscriptions.
```js
import { proxy } from 'valtio'
import { watch } from 'valtio/utils'
const userState = proxy({ user: { name: 'Juuso' } })
const sessionState = proxy({ expired: false })
watch((get) => {
// `get` adds `sessionState` to this callback's watched proxies
get(sessionState)
const expired = sessionState.expired
// Or call it inline
const name = get(userState).user.name
console.log(`${name}'s session is ${expired ? 'expired' : 'valid'}`)
})
// 'Juuso's session is valid'
sessionState.expired = true
// 'Juuso's session is expired'
```
## Cleanup
You may return a cleanup function that runs both:
- Before each re-invocation of the callback (i.e. due to a watched proxy changing)
- When the `watch` itself is stopped (by calling the cleanup function returned by `watch`)
```js
watch((get) => {
const expired = get(sessionState).expired
const name = get(userState).user.name
console.log(`${name}'s session is ${expired ? 'expired' : 'valid'}`)
return () => {
if (expired) {
console.log('Cleaning up')
}
}
})
// Output from 1st immediate invocation of the callbcak
// 'Juuso's session is valid'
// Changing a dependency will invoke the cleanup callback first,
// but the captured `expired` is false, so we only see output from the
// 2nd invocation of the callback.
sessionState.expired = true
// 'Juuso's session is expired'
// Changing a dependency will again invoke the cleanup callback,
// and `expired` is true now, so we output from both our cleanup
// function as well as the 3rd invocation of the callback.
setTimeout(() => {
userState.user.name = 'Anonymous'
}, 200)
// 200ms -> 'Anonymous's session is expired'
// 'Cleaning up' logged
```
## Gotchas
If you remove the setTimeout in the example above, `'Juuso's session is expired'` will become `'Anonymous's session is expired'` and it will be logged twice. Valtio will batch updates by default. You may pass `{sync: true}` as a second argument to `watch` to disable batching.
## No usage tracking
`watch` currently does not implement the usage tracking of `useSnapshot`, so the callback will rerun anytime a watched proxy (or child proxy) has any fields mutated, regardless of the fields accessed within the `callback`'s code.
And the return value of `get` is just the proxy itself, not a snapshot.
This is because `watch` is a vanilla primitive built on top of `subscribe`. It is potentially possible for `watch`, or a new `watch`-like method, to implement usage tracking in the future, see [this discussion](https://github.com/pmndrs/valtio/discussions/640) if interested.
================================================
FILE: docs/guides/async.mdx
================================================
---
title: 'Async'
section: 'Advanced'
description: 'Working with promises and Suspense'
---
# `async`
## Promises
Promises may be values in a proxied object. They will be resolved in calls to `snapshot`.
```jsx
// vanillajs example
const countDiv: HTMLElement | null = document.getElementById('count')
if (countDiv) countDiv.innerText = '0'
const store = proxy({
count: new Promise((r) => setTimeout(() => r(1), 1000)),
})
subscribe(store, () => {
const value = snapshot(store).count
if (countDiv && typeof value === 'number') {
countDiv.innerText = String(value)
store.count = new Promise((r) => setTimeout(() => r(value + 1), 1000))
}
})
```
## Suspend your React components
Valtio is compatible with React 19 `use` hook. This eliminates all the async back-and-forth, you can access your data directly while the parent is responsible for fallback state and error handling.
```jsx
import { use } from 'react' // React 19
// import { use } from 'react18-use' // React 18
const state = proxy({ post: fetch(url).then((res) => res.json()) })
function Post() {
const snap = useSnapshot(state)
return <div>{use(snap.post).title}</div>
}
function App() {
return (
<Suspense fallback="Loading...">
<Post />
</Suspense>
)
}
```
It still suffers from "de-opt", which prevents `useTransition` to work well. To mitigate it, there is a third-party library [use-valtio](https://github.com/valtiojs/use-valtio).
## Codesandbox Pokemon fetch demo
https://codesandbox.io/s/valtio-pokemon-fetch-x1lkbj?file=/src/App.tsx
## Codesandbox auth demo
https://codesandbox.io/s/valtio-async-1pstl1?file=/src/App.tsx
================================================
FILE: docs/guides/component-state.mdx
================================================
---
title: 'Component State'
section: 'Advanced'
description: 'Isolate component state with useRef'
---
# Component State
To isolate component state for reusability, Valtio must live in the React lifecycle. You can wrap a `proxy` in a ref, passing it with props or context.
```jsx
import { createContext, useContext } from 'react'
import { proxy, useSnapshot } from 'valtio'
const MyContext = createContext()
const MyProvider = ({ children }) => {
const state = useRef(proxy({ count: 0 })).current
return <MyContext.Provider value={state}>{children}</MyContext.Provider>
}
const MyCounter = () => {
const state = useContext(MyContext)
const snap = useSnapshot(state)
return (
<>
{snap.count} <button onClick={() => ++state.count}>+1</button>
</>
)
}
```
## Codesandbox example
https://codesandbox.io/s/valtio-component-ye5tbg?file=/src/App.tsx
## Alternatives
If you are not happy with `useRef` usage, consider:
- [use-constant](https://www.npmjs.com/package/use-constant)
- [bunshi](https://www.bunshi.org/recipes/valtio/)
- You may also create custom hooks wrapping useContext and optionally useSnapshot.
### Bunshi example
https://codesandbox.io/s/77r53c?file=/molecules.ts
================================================
FILE: docs/guides/computed-properties.mdx
================================================
---
title: 'Computed Properties'
section: 'Advanced'
description: 'Use object getters and setters'
---
# Computed Properties
In Valtio you can use object & class getters and setters to create computed properties.
<br />
<blockquote className="note">
ℹ️ Note
Getters in JavaScript are a more advanced feature of the language, so Valtio recommends using them with caution. That said, if you are a more advanced JavaScript programmer, they should work as you expect; see the "Note about using `this`" section below.
</blockquote>
<br />
<br />
## Simple object getter
```js
const state = proxy({
count: 1,
get doubled() {
return this.count * 2
},
})
console.log(state.doubled) // 2
// Getter calls on the snapshot work as expected
const snap = snapshot(state)
console.log(snap.doubled) // 2
// When count changes in the state proxy
state.count = 10
// Then snapshot's computed property does not change
console.log(snap.doubled) // 2
```
When you call `state.doubled` on the `state` proxy, it is not cached, and will be re-calculated on every call (if you must cache this result, see the section below on `proxy-memoize`).
However, when you make a snapshot, calls to `snap.doubled` are effectively cached, because the value of an object getter is copied during the snapshot process.
<br />
<blockquote className="note">
ℹ️ Note
In the current implementation a computed property should only reference \*\*\_sibling\*\*\* properties, otherwise you'll encounter weird bugs. For example:
</blockquote>
<br />
```js
const user = proxy({
name: 'John',
// OK - can reference sibling props via `this`
get greetingEn() {
return 'Hello ' + this.name
},
})
```
```js
const state = proxy({
// could be nested
user: {
name: 'John',
// OK - can reference sibling props via `this`
get greetingEn() {
return 'Hello ' + this.name
},
},
})
```
```js
const state = proxy({
user: {
name: 'John',
},
greetings: {
// WRONG - `this` points to `state.greetings`.
get greetingEn() {
return 'Hello ' + this.user.name
},
},
})
```
```js
const user = proxy({
name: 'John',
})
const greetings = proxy({
// WRONG - `this` points to `greetings`.
get greetingEn() {
return 'Hello ' + this.name
},
})
```
A workaround to it is to attach the related object as a property.
```js
const user = proxy({
name: 'John',
})
const greetings = proxy({
user, // attach the `user` proxy object
// OK - can reference user props via `this`
get greetingEn() {
return 'Hello ' + this.user.name
},
})
```
Another method would be to create a separate proxy and
synchronize with `subscribe`.
```js
const user = proxy({
name: 'John',
})
const greetings = proxy({
greetingEn: 'Hello ' + user.name,
})
subscribe(user, () => {
greetings.greetingEn = 'Hello ' + user.name
})
```
Or with `watch`.
```js
const user = proxy({
name: 'John',
})
const greetings = proxy({})
watch((get) => {
greetings.greetingEn = 'Hello ' + get(user).name
})
```
## Object getter and setter
Setters are also supported:
```js
const state = proxy({
count: 1,
get doubled() {
return state.count * 2
},
set doubled(newValue) {
state.count = newValue / 2
},
})
// Setter calls on the state work as expected
state.doubled = 4
console.log(state.count) // 2
// Getter calls on the snapshot work as expected
const snap = snapshot(state)
console.log(snap.doubled) // 4
// And setter calls on the snapshot fail as expected
// compile error: Cannot assign to 'doubled' because it is a read-only property.
// runtime error: TypeError: Cannot assign to read only property 'doubled' of object '#<Object>'
snap.doubled = 2
```
As with getters, setter calls within `set doubled` (i.e. `this.count = newValue / 2`) are themselves invoked against the `state` proxy, so the new `count` value will be correctly updated (and subscribers/snapshots notified of the new change).
If you make a snapshot, all properties become readonly, so `snap.doubled = 2` will be a compile error, and will also fail at runtime because `snapshot` objects are frozen.
## Class getters and setters
Class getters and setters work effectively like object getters and setters:
```js
class Counter {
count = 1
get doubled() {
return this.count * 2
}
set doubled(newValue) {
this.count = newValue / 2
}
}
const state = proxy(new Counter())
const snap = snapshot(state)
// Changing the state works as expected
state.doubled = 4
console.log(state.count) // 2
// And the snapshot value doesn't change
console.log(snap.doubled) // 2
```
Similar to object getters, class getters on the `state` proxy are not cached.
However, unlike object getters, class getters on the `snapshot` object are not cached, and are re-evaulated on every access to `snap.doubled`. As mentioned in the `snapshot` docs, this should be fine because the expectation is that getters are as cheap to evaluate as they would be to cache.
Also unlike object setters on snapshots (which immediately fail at runtime when called), class setters on snapshots will technically start evaluating, but any mutations they do internally (i.e. `this.count = newValue / 2`) will then fail at runtime because `this` will be a snapshot instance and the snapshots are frozen by `Object.freeze`.
## State usage tracking with `proxy-memoize`
If you need to cache getter results even for the `state` proxy itself, you can use Valtio's sister project [proxy-memoize](https://github.com/dai-shi/proxy-memoize).
`proxy-memoize` uses a similar usage-based tracking approach as Valtio's `snapshot` function, so it will only re-calculate the getter if fields accessed by the getter logic have actually changed.
```js
import { memoize } from 'proxy-memoize'
const memoizedDoubled = memoize((snap) => snap.count * 2)
const state = proxy({
count: 1,
text: 'hello',
get doubled() {
return memoizedDoubled(snapshot(state))
},
})
```
With this implementation, when `text` property changes (but `count` has not), the memoized function won't be re-executed.
## Note about using `this`
You can use `this` inside of getters and setters, however you should be familiar with how JS `this` works: basically that `this` is whatever object you invoked the call against.
So if you call `state.doubled`, then `this` will be the `state` proxy.
And if you call `snap.doubled`, then `this` will be the snapshot object (except for object getters & setters, where the current value is copied during the snapshot process, so object getters & setters are never invoked on snapshots).
Despite this nuance, you should be able to use `this` as you would expect, and things will "just work".
================================================
FILE: docs/guides/migrating-to-v2.mdx
================================================
---
title: 'How to Migrate to v2 from v1'
---
# How to Migrate to v2 from v1
## Changes in v2
React 19 officially introduces the `use` hook to handle promises.
Valtio v1 internally handled promises, which is no longer recommended.
In Valtio v2, promises are not handled internally,
and developers should explicitly use the `use` hook to manage promises.
Note: If you are still using React 18, you can use [the `use` hook shim](https://github.com/dai-shi/react18-use).
Valtio v2 also introduces two subtle changes in its design choices:
First, the behavior of `proxy(obj)` has changed. In v1, it was a pure function and deeply copied `obj`. In v2, it is an impure function and deeply modifies `obj`. Generally, reusing `obj` is not recommended. Unless you reuse `obj`, nothing will break.
Second, the behavior of `useSnapshot()` has been altered. Although it is a subtle change, it is less optimized to ensure compatibility with `useMemo` and the upcoming React compiler. The change may lead to extra re-renders in some edge cases, but it might not be noticeable.
Other notable changes to keep things updated and fresh include:
- Removal of all deprecated features
- Requirement of React version 18 and above
- Requirement of TypeScript version 4.5 and above
- The build target updated to ES2018
## Migration for breaking changes
### Resolving promises
```js
// v1
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ data: fetch(...).then((res) => res.json()) })
const Component = () => {
const snap = useSnapshot(state)
return <>{JSON.stringify(snap.data)}</>
}
```
```js
// v2
import { use } from 'react'
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ data: fetch(...).then((res) => res.json()) })
const Component = () => {
const snap = useSnapshot(state)
return <>{JSON.stringify(use(snap.data))}</>
// If `data` is not an object, you can directly embed it in JSX.
// return <>{snap.data}</>
}
```
### Impure `proxy(obj)`
If you don't reuse the object you pass to the proxy, nothing will break.
```js
import { proxy } from 'valtio'
// This works in both v1 and v2
const state = proxy({ count: 1, obj: { text: 'hi' } })
// This works in both v1 and v2
state.obj = { text: 'hello' }
```
That's the recommended way to use `proxy`.
For some reason, if you reuse the object, you need to use `deepClone` explicitly in v2 to keep the same behavior as v1.
```js
// v1
import { proxy } from 'valtio'
const initialObj = { count: 1, obj: { text: 'hi' } }
const state = proxy(initialObj)
// and do something later with `initialObj`
const newObj = { text: 'hello' }
state.obj = newObj
// and do something later with `newObj`
```
```js
// v2
import { proxy } from 'valtio'
import { deepClone } from 'valtio/utils'
const initialObj = { count: 1, obj: { text: 'hi' } }
const state = proxy(deepClone(initialObj))
// and do something later with `initialObj`
const newObj = { text: 'hello' }
state.obj = deepClone(newObj)
// and do something later with `newObj`
```
### `useLayoutEffect` server warning
If you're using React 18 with SSR, add this conditional to prevent excessive warnings.
```js
import { snapshot, useSnapshot as useSnapshotOrig } from 'valtio'
const isSSR = typeof window === 'undefined'
export const useSnapshot = isSSR ? (p) => snapshot(p) : useSnapshotOrig
// render with `useSnapshot` as usual
```
## Links
- https://github.com/pmndrs/valtio/discussions/703
- https://github.com/pmndrs/valtio/pull/810
================================================
FILE: docs/how-tos/how-to-avoid-rerenders-manually.mdx
================================================
---
title: 'How to avoid rerenders manually'
---
# How to avoid rerenders manually
## `useSnapshot` optimizes re-renders automatically
This is the basic usage.
```jsx
const Component = () => {
const { count } = useSnapshot(state) // this is reactive
return <>{count}</>
}
```
## Reading state is valid but not recommended for general use cases
```jsx
const Component = () => {
const { count } = state // this is not reactive
return <>{count}</>
}
```
This will not trigger re-render, but it doesn't follow the react rule like with any other global variables.
## Subscribe and set local state conditionally
```jsx
const Component = () => {
const [count, setCount] = useState(state.count)
useEffect(
() =>
subscribe(state, () => {
if (state.count % 2 === 0) {
// conditionally update local state
setCount(state.count)
}
}),
[],
)
return <>{count}</>
}
```
This should work mostly.
Theoretically, state can be changed before the subscription. A fix would be the following.
```jsx
const Component = () => {
const [count, setCount] = useState(state.count)
useEffect(() => {
const callback = () => {
if (state.count % 2 === 0) {
// conditionally update local state
setCount(state.count)
}
}
const unsubscribe = subscribe(state, callback)
callback()
return unsubscribe
}), [])
return <>{count}</>
}
```
For some use cases, using [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) could be easier.
================================================
FILE: docs/how-tos/how-to-easily-access-the-state-from-anywhere-in-the-application.mdx
================================================
---
title: 'How to easily access the state from anywhere in the application'
---
# How to easily access the state from anywhere in the application
When working with large applications organizing code in separate files and directories is the go-to way and the Valtio **state** is no exception. In some ways you may want to put the state object in its own file. After being separated in its own file we need a way to access it easily from anywhere in our application.
## Access the state using Path Aliases
Imagine that the state is put in `/src/state.js` and you are working with a file in `/src/really/deep/nested/file/myfile.js` the importing of the state will be something like this:
`import state from '../../../../state';` which can cause too much brain calculation specially if it is used in different places inside the application.
A solution to that is using **Path Aliases** which maps path to a simpler string and the import will look like something similar to that throughout the whole application:
`import { state } from '@state';`
## Using JS Config and Babel Config
1. Create the file `/src/state` and put the Valtio **state** into it:
```js
import { proxy, useSnapshot, subscribe } from 'valtio'
const state = proxy({
foos: [],
bar: { ... },
boo: false
})
export { state, useSnapshot, subscribe }
```
2. Create the file `/jsconfig.json` (or `/tsconfig.json` if you're using typescript):
```json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@state/*": ["./state/*"],
"@mypath/*": ["./my/deep/path*"],
"@anotherpath/*": ["./my/another/deep/path*"]
}
},
"exclude": ["node_modules"]
}
```
<br />
<blockquote className="tip">
💡 Using TypeScript? Here are some links for reference
https://www.totaltypescript.com/tsconfig-cheat-sheet<br />
https://github.com/tsconfig/bases<br />
https://www.typescriptlang.org/tsconfig/
</blockquote>
<br />
<br />
3. Add the **Module Resolver** plugin the plugins in your `babel.config.js`:
```js
module.exports = {
// ...
plugins: [
// The other existing plugins
[
'module-resolver',
{
root: ['./src'],
extensions: ['.js', '.jsx', '.json', '.svg', '.png'],
alias: {
'@state': './src/state',
},
},
],
// ...
],
}
```
4. Install the the Babel Plugin Module Resolver:
- Using NPM: `npm install babel-plugin-module-resolver`
- Using Yarn: `yarn add babel-plugin-module-resolver`
5. Restart the application server
<br />
That's it you will now be able to do `import
{(state, useSnapshot, subscribe)} from '@state';` from anywhere inside your
application.
## Using a third party library
You can use a third party library to create aliases and achieve the same result.
Example of libraries:
- [Module Alias](https://www.npmjs.com/package/module-alias)
================================================
FILE: docs/how-tos/how-to-organize-actions.mdx
================================================
---
title: 'How to organize actions'
---
# How to organize actions
Valtio is unopinionated about organizing actions.
Here's some recipes to show various patterns are possible.
## Action functions defined in module
<br />
<blockquote className="note">
ℹ️ Note
This way is preferred as it is better for code splitting.
</blockquote>
<br />
<br />
```js
import { proxy } from 'valtio'
export const state = proxy({
count: 0,
name: 'foo',
})
export const inc = () => {
++state.count
}
export const setName = (name) => {
state.name = name
}
```
## Action object defined in module
```js
import { proxy } from 'valtio'
export const state = proxy({
count: 0,
name: 'foo',
})
export const actions = {
inc: () => {
++state.count
},
setName: (name) => {
state.name = name
},
}
```
## Action methods defined in state
```js
export const state = proxy({
count: 0,
name: 'foo',
inc: () => {
++state.count
},
setName: (name) => {
state.name = name
},
})
```
## Action methods using `this`
```js
export const state = proxy({
count: 0,
name: 'foo',
inc() {
++this.count
},
setName(name) {
this.name = name
},
})
```
## Using class
```js
class State {
count = 0
name = 'foo'
inc() {
++this.count
}
setName(name) {
this.name = name
}
}
export const state = proxy(new State())
```
================================================
FILE: docs/how-tos/how-to-persist-states.mdx
================================================
---
title: 'How to persist states'
---
# How to persist states
## persist with localStorage
If your state is JSON serializable, it should be pretty straightforward.
```js
const state = proxy(
JSON.parse(localStorage.getItem('foo')) || {
count: 0,
text: 'hello',
},
)
subscribe(state, () => {
localStorage.setItem('foo', JSON.stringify(state))
})
```
If you have non serializable values, attach them after deserialization and exclude them for serialization.
**_[valtio-persist](https://github.com/Noitidart/valtio-persist) is a library that can help with this._**
================================================
FILE: docs/how-tos/how-to-reset-state.mdx
================================================
---
title: 'How to reset state'
---
# How to reset state
In some cases, you might want to reset the state in your proxy instance to its initial values. For example, you are storing form values or some other ephemeral UI state that you want to reset. It turns out this is quite simple to do!
```js
import { proxy } from 'valtio'
import { deepClone } from 'valtio/utils'
const initialObj = {
text: 'hello',
arr: [1, 2, 3],
obj: { a: 'b' },
}
const state = proxy(deepClone(initialObj))
const reset = () => {
const resetObj = deepClone(initialObj)
Object.keys(resetObj).forEach((key) => {
state[key] = resetObj[key]
})
}
```
Note that we're using the `deepClone()` utility function from `valtio/utils` to copy the initial object in _both_ the `reset` function and the `state` proxy. Using deepClone in the proxy function is a new requirement in v2. Valtio no longer clones the initial state by default. If you reuse the object you pass into the proxy function, you may get unexpected results.
Alternatively, you can store the object in another object, which make the reset logic easier:
```js
const state = proxy({ obj: initialObj })
const reset = () => {
state.obj = deepClone(initialObj)
}
```
<br />
<blockquote className="note">
ℹ️ Note
Using `structuredClone()`
<br />
In 2022, there was a new global function added called `structuredClone` that is widely available in most modern browsers. You can use `structuredClone` in the same way as `deepClone` above, however `deepClone` is preferred as it will be aware of any `ref`s in your state.
</blockquote>
> Note: deepClone will convert proxyMap and proxySet back to plain objects. If you have an object that has these within its tree, consider using `unstable_deepProxy` instead.
================================================
FILE: docs/how-tos/how-to-split-and-compose-states.mdx
================================================
---
title: 'How to split and compose states'
---
# How to split and compose states
## You can split states
Creating a state with nested object.
```js
const state = proxy({
obj1: { a: 1 },
obj2: { b: 2 },
})
```
You can then split the state into pieces. They are both proxies.
```js
const obj1State = state.obj1
const ojb2State = state.obj2
```
## You can combine states
You can create states and then combine them.
```js
const obj1State = proxy({ a: 1 })
const obj2State = proxy({ a: 2 })
const state = proxy({
obj1: obj1State,
obj2: obj2State,
})
```
This works equivalently to the previous example.
## You can create circular states
While there would be less use cases, you could create a circular structure.
```js
const state = proxy({
obj: { foo: 3 },
})
state.obj.bar = state.obj // 🤯
```
================================================
FILE: docs/how-tos/how-to-update-values-inside-arrays.mdx
================================================
---
title: 'How to update values inside arrays'
---
# How to update values inside arrays
Avoid unnecessary re-renders when updating values inside arrays.
## Basic approach (triggers full list re-render)
In this example, iterating over the snapshot items causes the entire list to re-render whenever any item changes.
```js
import { proxy, useSnapshot } from 'valtio'
const state = proxy({
title: 'My Counter list',
items: [
{ id: 1, count: 0 },
{ id: 2, count: 0 },
],
})
function Counter({ item }) {
return (
<div>
<span>{item.count}</span>
<button onClick={() => item.count++}>+1</button>
</div>
)
}
function CounterList() {
const snap = useSnapshot(state)
return (
<div>
<h1>{snap.title}</h1>
{/* Touching snap.items causes the whole list to re-render when any child updates */}
{snap.items.map((item) => (
<Counter key={item.id} item={item} />
))}
</div>
)
}
```
## Optimized approach (only changed items re-render)
Pass the proxy item (not snapshot) to children and let each child subscribe to its own item.
Do not access the entire array from the snapshot in the parent component.
```js
import { proxy, useSnapshot } from 'valtio'
const state = proxy({
title: 'My Counter list',
items: [
{ id: 1, count: 0 },
{ id: 2, count: 0 },
],
})
function Counter({ item }) {
const snap = useSnapshot(item)
return (
<div>
<span>{snap.count}</span>
<button onClick={() => item.count++}>+1</button>
</div>
)
}
function CounterList() {
const snap = useSnapshot(state)
return (
<div>
<h1>{snap.title}</h1>
{/* Only the length is accessed from snap, so only updated children re-render */}
{Array.from({ length: snap.items.length }, (_, index) => (
{/* Note that we are passing the proxy object (state) instead of the snapshot (snap) to the child component */}
<Counter key={state.items[index].id} item={state.items[index]} />
))}
</div>
)
}
```
## Why this works
Because the proxy (in this case: `state.items[index]`) is a proxy object instead of a snapshot object, accessing it does not track usage. This prevents the parent component from "subscribing" to the item's internal changes, effectively isolating updates to the child component.
When you call `snap.items.map()`, you access every item in the array, causing Valtio to re-render whenever _any_ item changes. By only accessing `snap.items.length`, Valtio only re-renders when the array length changes. Using `Array.from({ length: n }, (_, i) => ...)` creates an array by index without touching snapshot items, letting you access the proxy directly.
================================================
FILE: docs/how-tos/how-to-use-with-context.mdx
================================================
---
title: 'How to use with context'
---
# How to use with context
To make a valtio state only live in React lifecycle, you can create a state in a ref, and you can pass it with props or context.
## A basic pattern with context
```jsx
import { createContext, useContext } from 'react'
import { proxy, useSnapshot } from 'valtio'
const MyContext = createContext()
const MyProvider = ({ children }) => {
const state = useRef(proxy({ count: 0 })).current
return <MyContext.Provider value={state}>{children}</MyContext.Provider>
}
const MyCounter = () => {
const state = useContext(MyContext)
const snap = useSnapshot(state)
return (
<>
{snap.count} <button onClick={() => ++state.count}>+1</button>
</>
)
}
```
## Alternatives
If you are not happy with `useRef` usage, consider:
- [use-constant](https://www.npmjs.com/package/use-constant)
- [bunshi](https://www.bunshi.org/recipes/valtio/)
- You can create custom hooks to `useContext` and optionally `useSnapshot`
### Bunshi example
https://codesandbox.io/s/77r53c?file=/molecules.ts
================================================
FILE: docs/how-tos/how-valtio-works.mdx
================================================
---
title: 'How valtio works'
---
# How valtio works
Ref: https://github.com/pmndrs/valtio/issues/171
This is to describe the high level abstraction of valtio.
## Articles
- [How Valtio Proxy State Works (Vanilla Part)](https://blog.axlight.com/posts/how-valtio-proxy-state-works-vanilla-part/)
- [How Valtio Proxy State Works (React Part)](https://blog.axlight.com/posts/how-valtio-proxy-state-works-react-part/)
## Examples
### `proxy()` by examples
```js
import { proxy, subscribe } from 'valtio'
const s1 = proxy({})
subscribe(s1, () => {
console.log('s1 is changed!')
})
s1.a = 1 // s1 is changed!
++s1.a // s1 is changed!
delete s1.a // s1 is changed!
s1.b = 2 // s1 is changed!
s1.b = 2 // (not changed)
s1.obj = {} // s1 is changed!
s1.obj.c = 3 // s1 is changed!
const s2 = s1.obj
subscribe(s2, () => {
console.log('s2 is changed!')
})
s1.obj.d = 4 // s1 is changed! and s2 is changed!
s2.d = 5 // s1 is changed! and s2 is changed!
const s3 = proxy({})
subscribe(s3, () => {
console.log('s3 is changed!')
})
s1.o = s3
s3.p = 'hello' // s1 is changed! and s3 is changed!
s2.q = s3
s3.p = 'hi' // s1 is changed! s2 is changed! and s3 is changed!
s1.x = s1
s1.a += 1 // s1 is changed!
```
### `snapshot()` by examples
```js
import { proxy, snapshot } from 'valtio'
const p = proxy({})
const s1 = snapshot(p) // is {} but not wrapped by a proxy
const s2 = snapshot(p)
s1 === s2 // is true because p wasn't changed
p.a = 1 // mutate the proxy
const s3 = snapshot(p) // is { a: 1 }
p.a = 1 // mutation bails out and proxy is not updated
const s4 = snapshot(p)
s3 === s4 // is still true
p.a = 2 // mutate it
const s5 = snapshot(p) // is { a: 2 }
p.a = 1 // mutate it back
const s6 = snapshot(p) // creates a new snapshot
s3 !== s6 // is true (different snapshots, even though they are deep equal)
p.obj = { b: 2 } // attaching a new object, which will be wrapped by a proxy
const s7 = snapshot(p) // is { a: 1, obj: { b: 2 } }
p.a = 2 // mutating p
const s8 = snapshot(p) // is { a: 2, obj: { b: 2 } }
s7 !== s8 // is true because a is different
s7.obj === s8.obj // is true because obj is not changed
```
### `useSnapshot()` by examples
```jsx
import { proxy, useSnapshot } from 'valtio'
const s1 = proxy({
counter: 0,
text: 'Good morning from valtio',
foo: {
boo: 'baz'
}
})
const MyComponent = () => {
// Using destructuring
const { text, counter } = useSnapshot(state)
// Multilevel destructiong works as well
const { text, counter, { foo }} = useSnapshot(state)
// Assigning to a snapshot obeject
const snap = useSnapshot(state)
return (() => {
<div id="main">
<h1>{ `${foo} - ${text}` }</h1>
{/* - or - */}
<h1>{ `${snap.foo.bar} = `${snap.text}}</h1>
<div>
<input
type="input"
{/* we use snapshot for reading */}
value={text}
{/* the line above equivalent to this */}
value={snap.text}
{/* we use the proxy (s1) for mutations */}
onChange={e => {
s1.text = e.target.value
}}
/>
</div>
<div>
{ counter }
<button onClick={() => s1.counter++}> + </button>
<button onClick={() => s1.counter--}> - </button>
</di>
</div>
})
}
```
## Unorganized Notes
### two kinds of proxies
valtio has two kinds of proxies, for write and read. We intentionally separate them for hooks and concurrent react.
`proxy()` creates a proxy object to detect mutation, "proxy for write"
`snapshot()` creates an immutable object from the proxy object
`useSnapshot()` wraps the snapshot object again with another proxy (with `proxy-compare`) to detect property access, "proxy for read"
### snapshot creation is optimized
```js
const state = proxy({ a: { aa: 1 }, b: { bb: 2 } })
const snap1 = snapshot(state)
console.log(snap1) // ---> { a: { aa: 1 }, b: { bb: 2 } }
++state.a.aa
const snap2 = snapshot(state)
console.log(snap2) // ---> { a: { aa: 2 }, b: { bb: 2 } }
snap1.b === snap2.b // this is `true`, it doesn't create a new snapshot because no properties are changed.
```
### Some notes about valtio implementation in deep
valtio's proxy has only one goal: create an immutable snapshot object
some design principles:
1. snapshot is created on demand
2. changes are tracked only with version number
3. subscription is used for notifying update (version)
4. version number is hidden as implementation detail
5. proxies are basically used only for version and subscription
6. snapshot creation is optimized with version number
some notes about the implementation:
1. proxy can be nested (created at the initialization)
2. proxy can have circular structure (globalVersion to detect it)
some notes about promise handling:
1. proxy can have a promise but does nothing
2. when creating a snapshot, it will store the resolved value
3. if it's not resolved, a special object will throw a promise/error
================================================
FILE: docs/how-tos/some-gotchas.mdx
================================================
---
title: 'Some gotchas'
---
# Some gotchas
## `useSnapshot(state)` without property access will always trigger re-render
Ref: https://github.com/pmndrs/valtio/issues/209#issuecomment-896859395
Suppose we have this state (or store).
```js
const state = proxy({
obj: {
count: 0,
text: 'hello',
},
})
```
If using the snapshot with accessing count,
```js
const snap = useSnapshot(state)
snap.obj.count
```
it will re-render only if `count` changes.
If the property access is obj,
```js
const snap = useSnapshot(state)
snap.obj
```
then, it will re-render if `obj` changes. This includes `count` changes and `text` changes.
Now, we can subscribe to the portion of the state.
```js
const snapObj = useSnapshot(state.obj)
snapObj
```
This is technically same as the previous one. It doesn't touch the property of `snapObj`, so it will re-render if `obj` changes.
In summary, if a snapshot object (nested or not) is not accessed with any properties, it assumes the entire object is accessed, so any change inside the object will trigger re-render.
## Using `React.memo` with object props may result in unexpected behavior (v1 only)
⚠️ This behavior is fixed in v2.
The `snap` variable returned by `useSnapshot(state)` is tracked for render optimization.
If you pass the `snap` or some objects in `snap` to a component with `React.memo`,
it may not work as expected because `React.memo` can skip touching object properties.
Side note: [react-tracked](https://react-tracked.js.org) has a special `memo` exported as a workaround.
We have some options:
<ol type="a">
<li>Do not use `React.memo`.</li>
<li>
Do not pass objects to components with `React.memo` (pass primitive values
instead).
</li>
<li>
Pass in the proxy of that element, and then `useSnapshot` on that proxy.
</li>
</ol>
### Example of (b)
```jsx
const ChildComponent = React.memo(
({
title, // string or any primitive values are fine.
description, // string or any primitive values are fine.
// obj, // objects should be avoided.
}) => (
<div>
{title} - {description}
</div>
),
)
const ParentComponent = () => {
const snap = useSnapshot(state)
return (
<div>
<ChildComponent
title={snap.obj.title}
description={snap.obj.description}
/>
</div>
)
}
```
### Example of (c)
```jsx
const state = proxy({
objects: [
{ id: 1, label: 'foo' },
{ id: 2, label: 'bar' },
],
})
const ObjectList = React.memo(() => {
const stateSnap = useSnapshot(state)
return stateSnap.objects.map((object, index) => (
<Object key={object.id} objectProxy={state.objects[index]} />
))
})
const Object = React.memo(({ objectProxy }) => {
const objectSnap = useSnapshot(objectProxy)
return objectSnap.bar
})
```
## When to use `state` and when to use `snap` in functional components
- snap should be used in render function, every other cases state.
- callback functions are not in the render body and therefore state must be used.
```javascript
const Component = () => {
// this is in render body
const handleClick = () => {
// this is NOT in render body
}
return <button onClick={handleClick}>button</button>
}
```
- deps in useEffect should be used extracting primitive values from snap. For example: `const { num, string, bool } = snap.watchObj`.
- changing a state value based on other state values (without involving values like props in a component), should preferably done outside react.
```javascript
subscribe(state.subscribeData, async () => {
state.results = await load(state.someData)
})
```
## Controlled inputs may lose caret position without `sync: true`
Ref: https://github.com/pmndrs/valtio/issues/270
When using Valtio state with controlled `<input>` elements, you might notice the text caret jumping to the end while typing in the middle of the existing text.
This happens because Valtio batches state updates causing React to re-render after the input event. React resets the DOM value and loses the caret position.
**Use `{ sync: true }` to update synchronously and preserve the caret:**
```jsx
function Input() {
const snap = useSnapshot(state, { sync: true })
return (
<input
value={snap.text}
onChange={(e) => {
state.text = e.target.value
}}
/>
)
}
```
`sync: true` disables batching so React re-renders within the same event loop tick, skipping the DOM update and preserving the caret position.
## Issue with `array` `proxy`
The following use case can occur unexpected results on `arr` subscription:
```javascript
const byId = {}
arr.forEach((item) => {
byId[item.id] = item
})
arr.splice(0, arr.length)
arr.push(newValue())
someUpdateFunc(byId)
Object.keys(byId).forEach((key) => arr.push(byId[key]))
```
[Issues](https://github.com/pmndrs/valtio/issues/712) may arise when handling the array proxy reference in the subsequent steps:
<ol type="a">
<li>Subscribe array proxy</li>
<li>Use the proxy as snapshot</li>
<li>Assign temp variable for updating</li>
<li>Remove proxy from the array</li>
<li>Update temp</li>
<li>Push temp in the original array</li>
</ol>
**Example issue case:**
```javascript
const a = proxy([
{
nested: {
nested: {
test: 'apple',
},
},
},
])
const sa = snapshot(a) // b.
// a.
subscribe(a, () => {
const updated = snapshot(a)
console.log('this is updated proxy. test is Banana', a)
console.log('however, for the snapshot of a, test is still apple', updated)
})
function handle() {
const temp = a[0] // c.
a.splice(0, 1) // d.
temp.nested.nested.test = 'Banana' // e.
a.push(temp) // f.
console.log(Object.is(temp, a[0])) // this will be true
}
```
**To work around this, swap d and e:**
```javascript
// ...
function handle() {
const temp = a[0]
temp.nested.nested.test = 'Banana' // Update first remove from array
a.splice(0, 1)
a.push(temp)
}
// ...
```
If the workaround is not applied and you are using react with [devtools()](https://valtio.pmnd.rs/docs/api/utils/devtools), the redux devtools will notify a value update, but the snapshot will remain the same within the devtools' subscription.
As a result, the devtools will not display any state change.
Additionally, this issue involved not only updating devtools, but also triggering `re-render`.
## Issue with imports when using a library other than `react` (i.e. solidjs)
Valtio does not have to work within react, however it was built with react in mind. This being the case, the main `valtio` module exports the react modules alongside the vanilla modules for convenience. This means if you are attempting to import from the main `valtio` module or the `valtio/utils` submodule into a non-react project, you may end up with build errors like this:
```
node_modules/.pnpm/valtio@2.1.4/node_modules/valtio/esm/react.mjs (2:18): "useRef" is not exported by "__vite-optional-peer-dep:react:valtio", imported by "node_modules/.pnpm/valtio@2.1.4/node_modules/valtio/esm/react.mjs".
```
This occurs because the main valtio module exports both framework-agnostic and React-specific functionality, causing build tools like Rollup to look for React dependencies even when they're not needed.
There is a simple fix for this, however. Instead of importing from the main `valtio` module like this:
```ts
import { proxy, snapshot, subscribe } from 'valtio'
```
you can import directly from the framework-agnostic `vanilla` submodule:
```ts
import { proxy, snapshot, subsribe } from 'valtio/vanilla'
// this also applies for the utils
import { proxyMap, deepClone } from 'valtio/vanilla/utils'
```
================================================
FILE: docs/introduction/getting-started.mdx
================================================
---
title: 'Getting Started'
description: 'Get started with valtio.'
---

# Valtio
### Proxy state made simple.
The Valtio API is minimal, flexible, unopinionated and a touch magical. Valtio's proxy turns the object you pass it into a self-aware proxy, allowing fine-grained subscription and reactivity when making state updates. In React, Valtio shines at render optimization. It is compatible with Suspense and React 18. Valtio is also a viable option in vanilla javascript applications.
#### Installation
```bash
npm install valtio
```
#### The to-do app example
#### 1. proxy
Let's learn Valtio by coding a simple to-do app in React and Typescript. We'll start by creating some state using [`proxy`](../api/basic/proxy).
```ts
import { proxy, useSnapshot } from 'valtio'
type Status = 'pending' | 'completed'
type Filter = Status | 'all'
type Todo = {
description: string
status: Status
id: number
}
export const store = proxy<{ filter: Filter; todos: Todo[] }>({
filter: 'all',
todos: [],
})
```
#### 2. useSnapshot
To access the data in this store, we'll use [`useSnapshot`](../api/basic/useSnapshot). The Todos component will rerender when the "todos" or "filter" properties are updated. Any other data we add to the proxy will be ignored.
```tsx
const Todos = () => {
const snap = useSnapshot(store)
return (
<ul>
{snap.todos
.filter(({ status }) => status === snap.filter || snap.filter === 'all')
.map(({ description, status, id }) => {
return (
<li key={id}>
<span data-status={status} className="description">
{description}
</span>
<button className="remove">x</button>
</li>
)
})}
</ul>
)
}
```
#### 3. actions
Finally, we need to create, update, and delete our todos. To do so, we simply mutate properties on the store we created, not the snap. Commonly, these mutations are wrapped up in functions called actions.
```ts
const addTodo = (description: string) => {
store.todos.push({
description,
status: 'pending',
id: Date.now(),
})
}
const removeTodo = (id: number) => {
const index = store.todos.findIndex((todo) => todo.id === id)
if (index >= 0) {
store.todos.splice(index, 1)
}
}
const toggleDone = (id: number, currentStatus: Status) => {
const nextStatus = currentStatus === 'pending' ? 'completed' : 'pending'
const todo = store.todos.find((todo) => todo.id === id)
if (todo) {
todo.status = nextStatus
}
}
const setFilter = (filter: Filter) => {
store.filter = filter
}
```
Finally, we wire up these actions to our inputs and buttons - check the demo below for the full code.
```tsx
<button className="remove" onClick={() => removeTodo(id)}>
x
</button>
```
## Codesandbox demo
https://codesandbox.io/s/valtio-to-do-list-forked-6w9h3z
#### Mutating state outside of components
In our first to-do app, Valtio enabled mutations without worrying about performance or "breaking" React. `useSnapshot` turned these mutations into immutable snapshots and optimized renders. But we could have easily used React's own state handling. Let's add a bit of complexity to our to-do app to see what else Valtio offers.
We will add a "timeLeft" property to a todo. Now each todo will tick down to zero and become "overdue" if not completed in time.
```ts
type Todo = {
description: string
status: Status
id: number
timeLeft: number
}
```
Among other changes, we will add a Countdown component to display the ticking time for each todo. We are using an advanced technique of passing a nested proxy object to [`useSnapshot`](../api/basic/useSnapshot). Alternatively, make this a dumb component by passing the todo's "timeLeft" as a prop.
```tsx
import { useSnapshot } from 'valtio'
import { formatTimeDelta, calcTimeDelta } from './utils'
import { store } from './App'
export const Countdown = ({ index }: { index: number }) => {
const snap = useSnapshot(store.todos[index])
const delta = calcTimeDelta(snap.timeLeft)
const { days, hours, minutes, seconds } = formatTimeDelta(delta)
return (
<span className="countdown-time">
{delta.total < 0 ? '-' : ''}
{days}
{days ? ':' : ''}
{hours}:{minutes}:{seconds}
</span>
)
}
```
#### Mutate in module scope
Instead of managing multiple timers inside of a React component, let's move these updates outside of React altogether by defining a recursive `countdown` function in module scope which will mutate the todos.
```tsx
const countdown = (index: number) => {
const todo = store.todos[index]
// user removed todo case
if (!todo) return
// todo done of overdue case
if (todo.status !== 'pending') {
return
}
// time over
if (todo.timeLeft < 1000) {
todo.timeLeft = 0
todo.status = 'overdue'
return
}
setTimeout(() => {
todo.timeLeft -= 1000
countdown(index)
}, 1000)
}
```
We can start the recursive countdown from an enhanced `addTodo` action.
```tsx
const addTodo = (e: React.SyntheticEvent, reset: VoidFunction) => {
e.preventDefault()
const target = e.target as typeof e.target & {
deadline: { value: Date }
description: { value: string }
}
const deadline = target.deadline.value
const description = target.description.value
const now = Date.now()
store.todos.push({
description,
status: 'pending',
id: now,
timeLeft: new Date(deadline).getTime() - now,
})
// clear the form
reset()
countdown(store.todos.length - 1)
}
```
Please see the rest of the changes in the demo below.
#### Subscribe in module scope
Being able to mutate state outside of components is a huge benefit. We can also [`subscribe`](../api/advanced/subscribe) to state changes in module scope. We will leave it to you to try this out. You might, for instance, persist your todos to local storage as in [this example](https://github.com/pmndrs/valtio/wiki/How-to-persist-states#persist-with-localstorage).
## Codesandbox demo
https://codesandbox.io/s/valtio-countdown-to-do-list-xkgmri
================================================
FILE: docs/introduction.mdx
================================================
# Introduction
================================================
FILE: docs/readme.md
================================================
## How to contribute
### Basic things to know before adding docs
- Docs live in `docs/` folder.
- Website lives in `website/` folder.
- Docs are written in `mdx` format.
- Docs filename shouldn't have spaces.
- Website would generate title and other metadata from graymatter in the file.
- You should be able to render condesandbox inside `mdx` files by simply adding the url for the same
- Once you have a doc, you can add it to the sidebar section by adding it to the nav in `getDocsNav` function inside `website/lib/mdx.ts`
================================================
FILE: docs/resources/community.mdx
================================================
---
title: 'Community'
description: 'Discussion, wikis, and other community resources'
section: 'Resources'
---
# Community
## Recipes wiki
Valtio is unopinionated about best practices.
The community is working on recipes on wiki pages.
- [How to organize actions](https://github.com/pmndrs/valtio/wiki/How-to-organize-actions)
- [How to persist states](https://github.com/pmndrs/valtio/wiki/How-to-persist-states)
- [How to use with context](https://github.com/pmndrs/valtio/wiki/How-to-use-with-context)
- [How to split and compose states](https://github.com/pmndrs/valtio/wiki/How-to-split-and-compose-states)
## Discussion Forums
- [Pomandres collective](https://discord.gg/poimandres) - discord around React-three and related projects like Valtio
- [React Fan](https://discord.gg/MrQdmzd) - discord maintained by Daishi Kato
================================================
FILE: docs/resources/learn.mdx
================================================
---
title: 'Learn'
description: 'Courses, tutorials, and more'
section: 'Resources'
---
# Learn
- [learn valtio](https://daishi.gumroad.com/l/learn-valtio) - a code walkthrough by the library author
- [7GUIs with Valtio](https://stackblitz.com/edit/vitejs-vite-mxacfhmz?file=src%2Fguis%2FCircleDrawer%2Findex.tsx) - learn Valtio by [building 7 GUIs](https://eugenkiss.github.io/7guis/)
================================================
FILE: docs/resources/libraries.mdx
================================================
---
title: 'Libraries'
description: 'Libraries related to Valtio'
section: 'Resources'
---
# Libraries
Valtio provides bare necessities for proxy state management which is great for most projects; however, some users wish to extend the library's feature set. This can be done using 3rd-party libraries created by the community.
<br />
<blockquote className="warning">
⚠️ Warning
Disclaimer: These libraries may have bugs, limited maintenance, or other limitations and are not officially recommended by pmndrs or the valtio maintainers. This list is to provide a good starting point for someone looking to extend valtio's feature set.
</blockquote>
<br />
- [electron-valtio](https://github.com/water-a/electron-valtio) - Share state between the Electron main process and various renderer windows via valtio
- [eslint-plugin-valtio](https://github.com/pmndrs/eslint-plugin-valtio) - Eslint plugin for valtio
- [storybook-valtio-auto-bind](https://github.com/CosPie/storybook-valtio-auto-bind) - Automatically sync your Storybook args bidirectionally with the Valtio store
- [sveltio](https://github.com/wobsoriano/sveltio) - State management solution for Svelte using proxies. Powered by valtio.
- [swc-plugin-valtio](https://github.com/sosukesuzuki/swc-plugin-valtio) - Valtio useProxy transformer for SWC.
- [tauri-plugin-valtio](https://github.com/ferreira-tb/tauri-store/tree/main/packages/plugin-valtio) - Persistent valtio state for Tauri, accessible from both JavaScript and Rust.
- [use-valtio](https://github.com/dai-shi/use-valtio) - Another custom hook to use Valtio proxy state
- [valtio-element](https://github.com/lxsmnsyc/valtio-element) - Create reactive, declarative custom elements with valtio
- [valtio-factory](https://github.com/mfellner/valtio-factory) - Create valtio state using the factory pattern
- [valtio-fsm](https://github.com/valtiojs/valtio-fsm) - A a simple and chainable TypeScript-first finite state machine library powered by Valtio's reactivity system.
- [valtio-persist](https://github.com/valtiojs/valtio-persist) - Flexible and performant saving of state to disk.
- [valtio-auto-persist](https://github.com/valtiojs/valtio-auto-persist) - Experimental fork of valtio-persist that allows storing your state objects without the need for attaching a key to identify it. It uses [structure-id](https://github.com/overthemike/structure-id) behind the scenes.
- [valtio-plugin](https://github.com/valtiojs/valtio-plugin) - A brand new lifecycle plugin system that makes it much easier to customize how you use valtio.
- [valtio-reactive](https://github.com/valtiojs/valtio-reactive) - Valtio-reactive makes valtio a reactive library.
- [valtio-signal](https://github.com/dai-shi/valtio-signal) - Another React binding for Valtio proxy state
- [valtio-yjs](https://github.com/dai-shi/valtio-yjs) - Valtio-yjs makes yjs state easy
- [valtio-zod](https://github.com/valtiojs/valtio-zod) - Validate your valtio state updates with [Zod](https://zod.dev)
================================================
FILE: eslint.config.mjs
================================================
import eslint from '@eslint/js'
import vitest from '@vitest/eslint-plugin'
import { defineConfig, globalIgnores } from 'eslint/config'
import importPlugin from 'eslint-plugin-import'
import jestDom from 'eslint-plugin-jest-dom'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import testingLibrary from 'eslint-plugin-testing-library'
import tseslint from 'typescript-eslint'
export default defineConfig(
globalIgnores(['dist/', 'examples/', 'website/', 'coverage/']),
eslint.configs.recommended,
importPlugin.flatConfigs.recommended,
tseslint.configs.recommended,
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
reactHooks.configs.flat.recommended,
{
languageOptions: {
parserOptions: {
project: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: true,
},
},
rules: {
eqeqeq: 'error',
curly: ['warn', 'multi-line', 'consistent'],
'sort-imports': [
'error',
{
ignoreDeclarationSort: true,
},
],
'import/no-unresolved': ['error', { commonjs: true, amd: true }],
'import/named': 'off',
'import/namespace': 'off',
'import/no-named-as-default-member': 'off',
'import/no-duplicates': 'error',
'import/extensions': ['error', 'always', { ignorePackages: true }],
'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'],
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
files: ['tests/**/*.{ts,tsx}'],
...testingLibrary.configs['flat/react'],
},
{
files: ['tests/**/*.{ts,tsx}'],
...jestDom.configs['flat/recommended'],
},
{
files: ['tests/**/*.{ts,tsx}'],
...vitest.configs.recommended,
settings: { vitest: { typecheck: true } },
},
{
files: ['tests/**/*.{ts,tsx}'],
rules: {
'import/extensions': ['error', 'never'],
'vitest/consistent-test-it': [
'error',
{ fn: 'it', withinDescribe: 'it' },
],
},
},
{
files: ['src/vanilla/utils/proxyMap.ts', 'src/vanilla/utils/proxySet.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off',
},
},
{
files: ['*.config.*'],
languageOptions: {
parserOptions: {
project: null,
},
},
},
)
================================================
FILE: examples/README.md
================================================
# Examples
## Simple examples
- [Starter](https://github.com/pmndrs/valtio/tree/main/examples/starter)
- [Counter](https://github.com/pmndrs/valtio/tree/main/examples/counter)
- [Todo](https://github.com/pmndrs/valtio/tree/main/examples/todo)
- [Todo with proxyMap](https://github.com/pmndrs/valtio/tree/main/examples/todo-with-proxyMap)
================================================
FILE: examples/counter/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/github-dark.min.css"
/>
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: examples/counter/package.json
================================================
{
"name": "counter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@types/lowlight": "^0.0.7",
"highlight.js": "^11.11.0",
"lowlight": "^3.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-lowlight": "^3.0.1",
"valtio": "^2.1.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}
================================================
FILE: examples/counter/src/App.tsx
================================================
import { proxy, useSnapshot } from 'valtio'
import Lowlight from 'react-lowlight'
import typescript from 'highlight.js/lib/languages/typescript'
// You wrap your state
const state = proxy<{
number: number
nested?: {
ticks: number
}
}>({ number: 0 })
// You can freely mutate it from anywhere you want ...
state.nested = { ticks: 0 }
setInterval(() => state.nested && state.nested.ticks++, 200)
const Figure = () => {
const snap = useSnapshot(state)
// This component *only* renders when state.number changes ...
return <div className="figure">{snap.number}</div>
}
const Ticks = () => {
const snap = useSnapshot(state)
// This component *only* renders when state.nested.ticks changes ...
return <div className="ticks">{snap?.nested?.ticks} —</div>
}
const Controls = () => {
// This component simply mutates the state model, just like that ...
return (
<div className="logo">
<ButtonUp onClick={() => state.number++} />
<ButtonDown onClick={() => state.number--} />
</div>
)
}
const ButtonUp = ({ onClick }: { onClick: () => void }) => (
<svg viewBox="0 0 430 452" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
onClick={onClick}
d="M214.83 0.95459C82.4432 0.95459 0.0568237 91.2955 0.0568237 226.523C0.0568237 272.651 9.76549 313.624 27.8727 347.545L340.5 36.3569C306.7 13.5435 264.249 0.95459 214.83 0.95459Z"
fill="#A5FFCE"
/>
</svg>
)
const ButtonDown = ({ onClick }: { onClick: () => void }) => (
<svg viewBox="0 0 430 452" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
onClick={onClick}
d="M214.83 451.523C347.216 451.523 429.602 360.898 429.602 226.523C429.602 187.816 422.852 152.786 410.112 122.5L106 426.214C136.689 442.604 173.299 451.523 214.83 451.523Z"
fill="#FFBEC2"
/>
</svg>
)
const code = `import { proxy, useSnapshot } from 'valtio'
// You wrap your state
const state = proxy({ number: 0 })
// You can freely mutate it from anywhere you want ...
state.nested = { ticks: 0 }
setInterval(() => state.nested.ticks++, 200)
const Figure = () => {
const snap = useSnapshot(state)
// This component *only* renders when state.number changes ...
return <div className="figure">{snap.number}</div>
}
const Ticks = () => {
const snap = useSnapshot(state)
// This component *only* renders when state.nested.ticks changes ...
return <div className="ticks">{snap.nested.ticks} —</div>
}
const Controls = () => {
// This component simply mutates the state model, just like that ...
return (
<div className="logo">
<ButtonUp onClick={() => state.number++} />
<ButtonDown onClick={() => state.number--} />
</div>
)
}
const ButtonUp = ({ onClick }) => (
<svg viewBox="0 0 430 452" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
onClick={onClick}
d="M214.83 0.95459C82.4432 0.95459 0.0568237 91.2955 0.0568237 226.523C0.0568237 272.651 9.76549 313.624 27.8727 347.545L340.5 36.3569C306.7 13.5435 264.249 0.95459 214.83 0.95459Z"
fill="#A5FFCE"
/>
</svg>
)
const ButtonDown = ({ onClick }) => (
<svg viewBox="0 0 430 452" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
onClick={onClick}
d="M214.83 451.523C347.216 451.523 429.602 360.898 429.602 226.523C429.602 187.816 422.852 152.786 410.112 122.5L106 426.214C136.689 442.604 173.299 451.523 214.83 451.523Z"
fill="#FFBEC2"
/>
</svg>
)
`
Lowlight.registerLanguage('tsx', typescript)
export default function App() {
return (
<>
<div className="app">
<Figure />
<Ticks />
<Controls />
</div>
<div className="code">
<Lowlight language="ts" value={code} />
</div>
</>
)
}
================================================
FILE: examples/counter/src/index.css
================================================
@import url('https://rsms.me/inter/inter.css');
html {
font-family: 'Inter', sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.app {
background: white;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
position: relative;
width: 100%;
height: 100vh;
}
svg {
pointer-events: none;
}
path {
pointer-events: auto;
}
.ticks {
position: absolute;
top: 50px;
left: 50px;
font-size: 12px;
font-weight: 200;
font-variant-numeric: tabular-nums;
}
.figure {
font-variant-numeric: tabular-nums;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
font-weight: 800;
font-size: 30em;
letter-spacing: -20px;
pointer-events: none;
color: black;
-webkit-text-fill-color: white; /* Will override color (regardless of order) */
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: black;
}
.logo {
position: absolute;
right: 150px;
bottom: 150px;
}
.logo > svg {
position: absolute;
width: 100px;
height: 100px;
cursor: pointer;
}
.code {
padding: 10%;
}
pre {
font-size: 0.8em;
margin-left: -2.5rem !important;
margin-right: -2.5rem !important;
width: calc(100% + 5rem);
padding: 3em !important;
border: 1px solid #eee !important;
border-radius: 4px;
}
.src a * {
opacity: 0.5;
display: inline-block;
margin: 10px 5px;
}
================================================
FILE: examples/counter/src/main.tsx
================================================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './prism.css'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
================================================
FILE: examples/counter/src/prism.css
================================================
/**
* VS theme by Andrew Lock (https://andrewlock.net)
* Inspired by Visual Studio syntax coloring
*/
code[class*='language-'],
pre[class*='language-'] {
color: #393a34;
font-family:
'Consolas', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
font-size: 0.9em;
line-height: 1.2em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre > code[class*='language-'] {
font-size: 1em;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
background: #c1def1;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
background: #c1def1;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border: 1px solid #dddddd;
background-color: white;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.2em;
padding-top: 1px;
padding-bottom: 1px;
background: #f8f8f8;
border: 1px solid #dddddd;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #b2b8c0;
font-style: italic;
}
.token.namespace {
opacity: 0.7;
}
.token.string {
color: #67c2c7;
}
.token.punctuation,
.token.operator {
color: #393a34 !important; /* no highlight */
}
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.inserted {
color: #36acaa;
}
.token.atrule,
.token.keyword,
.token.attr-value,
.language-autohotkey .token.selector,
.language-json .token.boolean,
.language-json .token.number,
code[class*='language-css'] {
color: #9494ab;
}
.token.function {
color: #ff7bab;
}
.token.deleted,
.language-autohotkey .token.tag {
color: #9a050f;
}
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.class-name,
.language-json .token.property {
color: #67c2c7;
}
.token.tag,
.token.selector {
color: #67c2c7;
}
.token.attr-name,
.token.property,
.token.regex,
.token.entity {
color: #91adbd;
}
.token.directive.tag .tag {
background: #ffff00;
color: #393a34;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers .line-numbers-rows {
border-right-color: #a5a5a5;
}
.line-numbers-rows > span:before {
color: #2b91af;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight {
background: rgba(193, 222, 241, 0.2);
background: -webkit-linear-gradient(
left,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
background: linear-gradient(
to right,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
}
================================================
FILE: examples/counter/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: examples/counter/tsconfig.app.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/counter/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: examples/counter/tsconfig.node.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/counter/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: examples/editor-proxyWithHistory/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: examples/editor-proxyWithHistory/package.json
================================================
{
"name": "editor-proxyWithHistory",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"prismjs": "^1.23.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"valtio": "^2.1.1",
"valtio-history": "^1.0.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}
================================================
FILE: examples/editor-proxyWithHistory/src/App.tsx
================================================
import React from 'react'
import { useSnapshot } from 'valtio'
import { proxyWithHistory } from 'valtio-history'
const textProxy = proxyWithHistory({
text: 'Add some text to this initial value and then undo/redo',
})
const update = (event: React.ChangeEvent<HTMLTextAreaElement>) =>
(textProxy.value.text = event.target.value)
export default function App() {
const { value, undo, redo, history, canUndo, canRedo, getCurrentChangeDate } =
useSnapshot(textProxy)
return (
<div className="App">
<h2>Editor with history</h2>
<div className="info">
<span>
change {history.index + 1} / {history.nodes.length}
</span>
<span>|</span>
<span>{getCurrentChangeDate().toISOString()}</span>
</div>
<div className="editor">
<textarea value={value.text} rows={4} onChange={update} />
</div>
<button onClick={undo} disabled={!canUndo()}>
Undo
</button>
<button onClick={redo} disabled={!canRedo()}>
Redo
</button>
</div>
)
}
================================================
FILE: examples/editor-proxyWithHistory/src/index.css
================================================
.App {
font-family: 'Helvetica Neue', sans-serif;
text-align: center;
}
.info {
display: flex;
justify-content: center;
padding: 16px;
& > span {
margin-right: 32px;
}
}
.editor {
display: flex;
margin: 0 auto;
width: 80%;
flex-direction: column;
margin-bottom: 8px;
}
================================================
FILE: examples/editor-proxyWithHistory/src/main.tsx
================================================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
================================================
FILE: examples/editor-proxyWithHistory/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: examples/editor-proxyWithHistory/tsconfig.app.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/editor-proxyWithHistory/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: examples/editor-proxyWithHistory/tsconfig.node.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/editor-proxyWithHistory/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: examples/photo-booth-vanillajs/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Photo Booth</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"
/>
</head>
<body>
<header><a href="https://github.com/pmndrs/valtio">Valtio 🧙</a></header>
<main>
<h1>Photo Booth</h1>
<section id="video-container">
<video playsinline autoplay></video>
</section>
<controls>
<button id="take-pic-btn">Take picture</button>
</controls>
<canvas hidden></canvas>
<controls id="candidate-img-controls"> </controls>
<div id="images"></div>
</main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
================================================
FILE: examples/photo-booth-vanillajs/package.json
================================================
{
"name": "valtio-photo-booth-demo",
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "yarn run dev"
},
"dependencies": {
"lighterhtml": "4.2.0",
"localforage": "1.10.0",
"valtio": "^2.1.1",
"vite": "2.8.1"
},
"keywords": [],
"description": ""
}
================================================
FILE: examples/photo-booth-vanillajs/src/index.css
================================================
main {
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
video {
width: 100%;
max-height: calc(50vh - 16px);
background: var(--background-alt);
cursor: pointer;
}
controls {
width: 100%;
display: flex;
justify-content: center;
padding: 32px 0;
}
.image-remove-btn {
position: absolute;
top: 0;
right: 0;
padding: 8px;
margin-top: 4px;
cursor: pointer;
}
.image-remove-btn.selected {
display: none;
}
canvas {
margin: 32px;
}
button {
margin-top: 18px;
}
#images {
display: flex;
flex-flow: wrap;
gap: 8px;
}
a:not(.selected) {
position: relative;
flex: 0 1 auto;
width: calc(33% - 4px);
padding-top: 2;
padding-right: 2;
padding-bottom: 0;
padding-left: 2;
}
a.selected {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
background: var(--background);
z-index: 10;
}
img {
border: 1px solid transparent;
transition: all 250ms cubic-bezier(0, 0.72, 0, 1.02);
}
img.selected {
display: block;
width: 90%;
border-radius: 8;
margin: 64px auto;
}
img:not(.selected) {
width: 100%;
border-radius: 4;
}
img:hover {
border: 1px solid var(--highlight);
transform-origin: center center;
z-index: 2;
cursor: pointer;
}
.animate-in img:hover {
border: 1px solid transparent;
transform: scale(1);
}
.animate-in {
animation: up-in;
animation-duration: 250ms;
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0, 0.72, 0, 1.02);
will-change: transform;
transform: translate3d(0, -200px, 0);
opacity: 0.5;
}
.animate-in:after {
position: absolute;
top: 32px;
right: calc(5%);
content: 'Close';
color: var(--select-arrow);
font-size: 1rem;
}
.hidden {
display: none;
}
@keyframes up-in {
from {
transform: translate3d(0, -200px, 0);
opacity: 0.5;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
================================================
FILE: examples/photo-booth-vanillajs/src/main.js
================================================
import './index.css'
import { html, render } from 'lighterhtml'
import localforage from 'localforage'
import { proxy, snapshot, subscribe } from 'valtio/vanilla'
localforage.config({ name: 'valtio_photo_booth' })
const getId = () => new Date().getTime()
window.history.pushState({}, '', '/photo-booth')
// reference https://github.com/webrtc/samples/blob/gh-pages/src/content/getusermedia/canvas/js/main.js
const canvas = document.querySelector('canvas')
canvas.width = 480
canvas.height = 360
const ctx = canvas.getContext('2d')
const video = document.querySelector('video')
const imageContainer = document.getElementById('images')
const snapPicButton = document.getElementById('take-pic-btn')
const acceptPicButton = document.getElementById('accept-pic-btn')
const rejectPicButton = document.getElementById('reject-pic-btn')
const candidateImgControlsContainer = document.getElementById(
'candidate-img-controls',
)
const store = proxy({
camera: {
images: [],
candidateImage: null,
},
ui: {
selectedImageId:
decodeURI(window.location.pathname).split('/photo-booth/')[1] || null,
},
})
subscribe(store, () => {
// keep storage synced
localforage.setItem('images', snapshot(store).camera.images || [])
// hide canvas when not used
if (store.camera.candidateImage) {
canvas.removeAttribute('hidden')
} else {
canvas.setAttribute('hidden', 'hidden')
}
// render html
renderCanvasControls(!!store.camera.candidateImage)
renderImages(store.camera.images, store.ui.selectedImageId)
})
/******** PERSISTED IMAGES HANDLING *********/
async function loadImages() {
const images = await localforage.getItem('images')
if (images) store.camera.images = images
}
function removeImage(id) {
store.camera.images = store.camera.images.filter((img) => img.id !== id)
}
function selectImage(id) {
const selectedId = store.ui.selectedImageId
if (selectedId === id) {
store.ui.selectedImageId = null
window.history.pushState({}, '', '/photo-booth/')
} else {
store.ui.selectedImageId = id
window.history.pushState({}, '', '/photo-booth/' + id)
}
}
function saveImage(url) {
const id = getId()
store.camera.images.push({ id, url })
window.history.pushState({}, '', id)
}
function renderImages(images, selectedId) {
render(
imageContainer,
html.node`
${images.map(({ url, id }) => {
const selected = id === selectedId
return html.node`
<a href="${url}" class=${
selected ? 'selected animate-in' : ''
} onclick=${(e) => {
e.preventDefault()
selectImage(id)
}}>
<img src=${url} class=${selected ? 'selected animate-in' : ''}>
<button class=${
selected ? 'image-remove-btn selected' : 'image-remove-btn'
} onclick=${() => removeImage(id)}>X</button>
</a>
`
})}
`,
)
}
/******** CANDIDATE IMAGE HANDLING *********/
function keepCandidateImage(e) {
e.preventDefault()
saveImage(store.camera.candidateImage)
store.camera.candidateImage = null
}
function removeCandidateImage(e) {
e.preventDefault()
store.camera.candidateImage = null
}
function renderCanvasControls(hasCandidateImage) {
const className = !hasCandidateImage ? 'hidden' : ''
render(
candidateImgControlsContainer,
html.node`
<button id="accept-pic-btn" class=${className} onclick=${keepCandidateImage}>
Keep picture</button>
<button id="reject-pic-btn" class=${className} onclick=${removeCandidateImage}>Reject picture</button>
`,
)
}
/******** MEDIA STREAM INIT *********/
async function startMediaStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
video.srcObject = stream
} catch (err) {
// should show user an error message
console.log('failed to create stream', err)
}
}
/******** SETUP *********/
window.addEventListener('load', async () => {
await loadImages()
await startMediaStream()
snapPicButton.addEventListener('click', () => {
const width = video.videoWidth
const height = video.videoHeight
ctx.fillRect(0, 0, width, height)
ctx.drawImage(video, 0, 0, width, height)
store.camera.candidateImage = canvas.toDataURL('image/webp')
})
acceptPicButton.addEventListener('click', () => {
saveImage(store.camera.candidateImage)
store.camera.candidateImage = null
})
rejectPicButton.addEventListener('click', () => {
store.camera.candidateImage = null
})
})
================================================
FILE: examples/starter/README.md
================================================
# Starter [](https://stackblitz.com/github/pmndrs/valtio/tree/main/examples/starter)
## Set up locally
```bash
git clone https://github.com/pmndrs/valtio
# install project dependencies & build the library
cd valtio && pnpm install
# move to the examples folder & install dependencies
cd examples/starter && pnpm install
# start the dev server
pnpm dev
```
## Set up on `StackBlitz`
Link: https://stackblitz.com/github/pmndrs/valtio/tree/main/examples/starter
================================================
FILE: examples/starter/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="valtio examples" />
<title>Valtio Examples | Starter</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
================================================
FILE: examples/starter/package.json
================================================
{
"name": "starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"valtio": "^2.1.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.0.0",
"vite": "^5.3.4"
}
}
================================================
FILE: examples/starter/src/index.css
================================================
html,
body,
#root {
height: 100%;
}
#root {
display: flex;
place-items: center;
justify-content: center;
color: #fff;
background-color: rgb(23 23 23);
}
================================================
FILE: examples/starter/src/index.tsx
================================================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { proxy, useSnapshot } from 'valtio'
import banner from './assets/banner.svg'
import './index.css'
const state = proxy({ count: 0 })
const Counter = () => {
const snap = useSnapshot(state)
return (
<>
<span className="text-3xl">{snap.count}</span>
<button
className="bg-sky-400 font-bold py-2 px-4 rounded"
onClick={() => ++state.count}
>
+1
</button>
</>
)
}
function App() {
return (
<div className="grid place-items-center gap-6">
<a href="https://valtio.dev/" target="_blank" rel="noreferrer">
<img
src={banner}
alt="Valtio banner"
className="w-96"
style={{
filter: 'drop-shadow(0 0 2em #b2ebf2)',
}}
/>
</a>
<h1 className="text-5xl font-bold">Valtio Starter</h1>
<Counter />
</div>
)
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
================================================
FILE: examples/starter/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: examples/starter/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2019",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["vite.config.ts", "./src/**/*"],
"exclude": ["node_modules"]
}
================================================
FILE: examples/starter/vite.config.ts
================================================
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: examples/subscribe/index.html
================================================
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Valtio vanilla</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"
/>
</head>
<body>
<script type="module" async defer>
import { render, html } from '//unpkg.com/lighterhtml?module'
import pk from '//unpkg.com/valtio@1.3.0?module'
</script>
</body>
</html>
================================================
FILE: examples/todo/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: examples/todo/package.json
================================================
{
"name": "counter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"prismjs": "^1.23.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"valtio": "^2.1.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}
================================================
FILE: examples/todo/src/App.tsx
================================================
import * as React from 'react'
import { actions, filters, Todo, useFilter, useTodos } from './store'
function App() {
return (
<div className="App">
<h1>Todo</h1>
<AddTodoInput />
<TodoList />
<FilterRow />
</div>
)
}
function AddTodoInput() {
const [value, setValue] = React.useState('')
function handleSubmit() {
actions.addTodo({ name: value })
setValue('')
}
return (
<div>
<input
type="text"
value={value}
onChange={({ target }) => setValue(target.value)}
/>
<button onClick={handleSubmit}>Add Todo</button>
</div>
)
}
function TodoList() {
const todos = useTodos()
return (
<div>
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} />
))}
</div>
)
}
function TodoRow({ todo }: { todo: Todo }) {
function handleCheckBoxChange(event: React.ChangeEvent<HTMLInputElement>) {
actions.toggleTodo(todo.id, event.target.checked)
}
function handleDeleteClick() {
actions.removeTodo(todo.id)
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
textDecoration: todo.completed ? 'line-through' : 'initial',
}}
>
<input type="checkbox" onChange={handleCheckBoxChange} />
<h2>{todo.name}</h2>
<h2 style={{ color: 'red', paddingLeft: 10 }} onClick={handleDeleteClick}>
X
</h2>
</div>
)
}
function FilterRow() {
const activeFilter = useFilter()
return (
<div style={{ padding: 20 }}>
{filters.map((filter) => (
<button
style={{
fontWeight: activeFilter === filter ? 'bold' : 'normal',
}}
onClick={() => {
actions.toggleFilter(filter)
}}
>
{filter}
</button>
))}
</div>
)
}
export default App
================================================
FILE: examples/todo/src/index.css
================================================
@import url('https://rsms.me/inter/inter.css');
html {
font-family: 'Inter', sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.app {
background: white;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
position: relative;
width: 100%;
height: 100vh;
}
svg {
pointer-events: none;
}
path {
pointer-events: auto;
}
.ticks {
position: absolute;
top: 50px;
left: 50px;
font-size: 12px;
font-weight: 200;
font-variant-numeric: tabular-nums;
}
.figure {
font-variant-numeric: tabular-nums;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
font-weight: 800;
font-size: 30em;
letter-spacing: -20px;
pointer-events: none;
color: black;
-webkit-text-fill-color: white; /* Will override color (regardless of order) */
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: black;
}
.logo {
position: absolute;
right: 150px;
bottom: 150px;
}
.logo > svg {
position: absolute;
width: 100px;
height: 100px;
cursor: pointer;
}
.code {
padding: 10%;
}
pre {
font-size: 0.8em;
margin-left: -2.5rem !important;
margin-right: -2.5rem !important;
width: calc(100% + 5rem);
padding: 3em !important;
border: 1px solid #eee !important;
border-radius: 4px;
}
.src a * {
opacity: 0.5;
display: inline-block;
margin: 10px 5px;
}
================================================
FILE: examples/todo/src/main.tsx
================================================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './prism.css'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
================================================
FILE: examples/todo/src/prism.css
================================================
/**
* VS theme by Andrew Lock (https://andrewlock.net)
* Inspired by Visual Studio syntax coloring
*/
code[class*='language-'],
pre[class*='language-'] {
color: #393a34;
font-family:
'Consolas', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
font-size: 0.9em;
line-height: 1.2em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre > code[class*='language-'] {
font-size: 1em;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
background: #c1def1;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
background: #c1def1;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border: 1px solid #dddddd;
background-color: white;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.2em;
padding-top: 1px;
padding-bottom: 1px;
background: #f8f8f8;
border: 1px solid #dddddd;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #b2b8c0;
font-style: italic;
}
.token.namespace {
opacity: 0.7;
}
.token.string {
color: #67c2c7;
}
.token.punctuation,
.token.operator {
color: #393a34 !important; /* no highlight */
}
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.inserted {
color: #36acaa;
}
.token.atrule,
.token.keyword,
.token.attr-value,
.language-autohotkey .token.selector,
.language-json .token.boolean,
.language-json .token.number,
code[class*='language-css'] {
color: #9494ab;
}
.token.function {
color: #ff7bab;
}
.token.deleted,
.language-autohotkey .token.tag {
color: #9a050f;
}
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.class-name,
.language-json .token.property {
color: #67c2c7;
}
.token.tag,
.token.selector {
color: #67c2c7;
}
.token.attr-name,
.token.property,
.token.regex,
.token.entity {
color: #91adbd;
}
.token.directive.tag .tag {
background: #ffff00;
color: #393a34;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers .line-numbers-rows {
border-right-color: #a5a5a5;
}
.line-numbers-rows > span:before {
color: #2b91af;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight {
background: rgba(193, 222, 241, 0.2);
background: -webkit-linear-gradient(
left,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
background: linear-gradient(
to right,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
}
================================================
FILE: examples/todo/src/store.ts
================================================
import { proxy, useSnapshot } from 'valtio'
export interface Todo {
id: number
name: string
completed?: boolean
}
export const filters: Filter[] = ['all', 'completed']
type Filter = 'all' | 'completed'
interface Store {
todos: Todo[]
filter: Filter
}
export const store = proxy<Store>({
todos: [],
filter: 'all',
})
let id = 0
export const actions = {
addTodo(todo: Omit<Todo, 'id'>) {
store.todos.push({
...todo,
id: id++,
})
},
toggleTodo(id: number, value: boolean) {
const todo = store.todos.find((todo) => todo.id === id)
if (todo && value) todo.completed = value
else if (todo) todo.completed = !todo.completed
},
removeTodo(id: number) {
store.todos = store.todos.filter((todo) => todo.id !== id)
},
toggleFilter(filter: Filter) {
store.filter = filter
},
}
export function useTodos() {
const snapShot = useSnapshot(store)
switch (snapShot.filter) {
case 'all':
return snapShot.todos
case 'completed':
return snapShot.todos.filter((todo) => todo.completed)
default:
throw Error('Error: un supported filter')
}
}
export function useFilter() {
return useSnapshot(store).filter
}
================================================
FILE: examples/todo/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: examples/todo/tsconfig.app.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/todo/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: examples/todo/tsconfig.node.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/todo/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: examples/todo-with-proxyMap/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: examples/todo-with-proxyMap/package.json
================================================
{
"name": "counter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.1.1",
"prismjs": "^1.23.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"valtio": "^2.1.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}
================================================
FILE: examples/todo-with-proxyMap/src/AddTodoInput.tsx
================================================
import React from 'react'
import { actions } from './store'
export function AddTodoInput() {
const [value, setValue] = React.useState('')
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (value.trim().length > 0) {
actions.addTodo({ name: value })
setValue('')
}
}
return (
<header className="header">
<h1>Todo</h1>
<form onSubmit={handleSubmit}>
<input
className="new-todo"
type="text"
value={value}
placeholder="What needs to be done?"
onChange={({ target }) => setValue(target.value)}
/>
</form>
</header>
)
}
================================================
FILE: examples/todo-with-proxyMap/src/App.tsx
================================================
import { AddTodoInput } from './AddTodoInput'
import { TodoList } from './TodoList'
import { Filter } from './Filter'
import './styles.css'
function App() {
return (
<div className="todoapp">
<AddTodoInput />
<TodoList />
<Filter />
</div>
)
}
export default App
================================================
FILE: examples/todo-with-proxyMap/src/Filter.tsx
================================================
import cx from 'clsx'
import { useFilter, actions, useTodosCount } from './store'
export function Filter() {
const count = useTodosCount()
const activeFilter = useFilter()
if (!count.active && !count.completed) return null
return (
<footer className="footer">
<span className="todo-count">
<strong>{count.active}</strong> {`item${count.active > 1 ? 's' : ''}`}{' '}
left
</span>
<ul className="filters">
<li>
<button
onClick={() => actions.toggleFilter('all')}
className={cx({ selected: activeFilter === 'all' })}
>
All
</button>
</li>{' '}
<li>
<button
onClick={() => actions.toggleFilter('todo')}
className={cx({
selected: activeFilter === 'todo',
})}
>
Active
</button>
</li>{' '}
<li>
<button
onClick={() => actions.toggleFilter('done')}
className={cx({
selected: activeFilter === 'done',
})}
>
Completed
</button>
</li>
</ul>
</footer>
)
}
================================================
FILE: examples/todo-with-proxyMap/src/TodoItem.tsx
================================================
import { useEffect, useRef, useState } from 'react'
import cx from 'clsx'
import { actions, Todo } from './store'
export function TodoItem({ todo }: { todo: Todo }) {
const [isEditing, setIsEditing] = useState(false)
const [editText, setEditText] = useState(todo.name)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (isEditing && inputRef.current) {
const node = inputRef.current
node.focus()
node.setSelectionRange(node.value.length, node.value.length)
} else if (inputRef.current) {
const node = inputRef.current
node.blur()
}
}, [isEditing])
const handleCheckBoxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
actions.toggleTodo(todo.id, event.target.checked)
}
const handleDelete = () => {
actions.removeTodo(todo.id)
}
const handleDoubleClick = () => {
setIsEditing(true)
}
const handleEdit = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditText(e.target.value)
}
const handleKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
setIsEditing(false)
setEditText(todo.name)
} else if (e.key === 'Enter' && editText.trim().length > 0) {
setIsEditing(false)
actions.updateTodo(todo.id, editText)
}
}
const handleBlur = () => {
setIsEditing(false)
if (editText.trim().length > 0) {
actions.updateTodo(todo.id, editText)
}
}
return (
<li className={cx({ completed: todo.completed, editing: isEditing })}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={handleCheckBoxChange}
/>
<label onDoubleClick={handleDoubleClick}>{todo.name}</label>
<button className="destroy" onClick={handleDelete} />
</div>
<input
ref={inputRef}
className="edit"
value={editText}
onChange={handleEdit}
onKeyDown={handleKeydown}
onBlur={handleBlur}
/>
</li>
)
}
================================================
FILE: examples/todo-with-proxyMap/src/TodoList.tsx
================================================
import { actions, useTodos } from './store'
import { TodoItem } from './TodoItem'
export function TodoList() {
const todos = useTodos()
const handleToggleAll = (e: React.ChangeEvent<HTMLInputElement>) => {
actions.toggleAll(e.target.checked)
}
if (!todos.length) return null
return (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={handleToggleAll}
/>
<label htmlFor="toggle-all" />
<ul className="todo-list">
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</section>
)
}
================================================
FILE: examples/todo-with-proxyMap/src/main.tsx
================================================
import { render } from 'react-dom'
import App from './App'
const rootElement = document.getElementById('root')
render(<App />, rootElement)
================================================
FILE: examples/todo-with-proxyMap/src/react-app-env.d.ts
================================================
/// <reference types="react-scripts" />
================================================
FILE: examples/todo-with-proxyMap/src/store.ts
================================================
import { proxy, useSnapshot } from 'valtio'
import { proxyMap } from 'valtio/utils'
export interface Todo {
id: number
name: string
completed?: boolean
}
export const filters: Filter[] = ['all', 'todo', 'done']
type Filter = 'all' | 'todo' | 'done'
interface Store {
todos: Map<number, Todo>
filter: Filter
}
export const store = proxy<Store>({
todos: proxyMap(),
filter: 'all',
})
let id = 0
export const actions = {
addTodo(todo: Omit<Todo, 'id'>) {
const nextId = id++
store.todos.set(nextId, { id: nextId, ...todo, completed: false })
},
toggleTodo(id: number, value: boolean) {
const todo = store.todos.get(id)
if (todo && value) todo.completed = value
else if (todo) todo.completed = !todo.completed
},
toggleAll(completed: boolean) {
store.todos.forEach((todo) => {
todo.completed = completed
})
},
removeTodo(id: number) {
store.todos.delete(id)
},
toggleFilter(filter: Filter) {
store.filter = filter
},
updateTodo(id: number, value: string) {
const todo = store.todos.get(id)
if (todo) todo.name = value
},
}
export function useTodos() {
const snapshot = useSnapshot(store)
switch (snapshot.filter) {
case 'all':
return Array.from(snapshot.todos.values())
case 'done':
return Array.from(snapshot.todos.values()).filter(
(todo) => todo.completed,
)
case 'todo':
return Array.from(snapshot.todos.values()).filter(
(todo) => !todo.completed,
)
default:
throw Error('Error: un supported filter')
}
}
export function useTodosCount() {
const snapshot = useSnapshot(store)
const count = {
active: 0,
completed: 0,
}
snapshot.todos.forEach(({ completed }) => {
if (completed) {
count.completed++
} else {
count.active++
}
})
return count
}
export function useFilter() {
return useSnapshot(store).filter
}
================================================
FILE: examples/todo-with-proxyMap/src/styles.css
================================================
/* https://www.npmjs.com/package/todomvc-app-css */
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font:
14px 'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: rgba(175, 47, 47, 0.45);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
height: 65px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 65px;
font-size: 0;
position: absolute;
top: -65px;
left: -0;
}
.toggle-all + label:before {
content: '❯';
display: inline-block;
font-size: 22px;
color: #949494;
padding: 10px 27px 10px 27px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all:checked + label:before {
color: #484848;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #484848;
}
.todo-list li.completed label {
color: #949494;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #949494;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover,
.todo-list li .destroy:focus {
color: #c18585;
}
.todo-list li .destroy:after {
content: '×';
display: block;
height: 100%;
line-height: 1.1;
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow:
0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li button {
color: inherit;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li button:hover {
border-color: #db7676;
}
.filters li button.selected {
border-color: #ce4646;
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 19px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
:focus,
.toggle:focus + label,
.toggle-all:focus + label {
box-shadow: 0 0 2px 2px #cf7d7d;
outline: 0;
}
================================================
FILE: examples/todo-with-proxyMap/tsconfig.app.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/todo-with-proxyMap/tsconfig.json
================================================
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
================================================
FILE: examples/todo-with-proxyMap/tsconfig.node.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/todo-with-proxyMap/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: package.json
================================================
{
"name": "valtio",
"description": "🧙 Valtio makes proxy-state simple for React and Vanilla",
"private": true,
"type": "commonjs",
"version": "2.3.1",
"main": "./index.js",
"types": "./index.d.ts",
"typesVersions": {
">=4.5": {
"esm/*": [
"esm/*"
],
"*": [
"*"
]
},
"*": {
"esm/*": [
"ts_version_4.5_and_above_is_required.d.ts"
],
"*": [
"ts_version_4.5_and_above_is_required.d.ts"
]
}
},
"exports": {
"./package.json": "./package.json",
".": {
"react-native": {
"types": "./index.d.ts",
"default": "./index.js"
},
"import": {
"types": "./esm/index.d.mts",
"default": "./esm/index.mjs"
},
"default": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"./*": {
"react-native": {
"types": "./*.d.ts",
"default": "./*.js"
},
"import": {
"types": "./esm/*.d.mts",
"default": "./esm/*.mjs"
},
"default": {
"types": "./*.d.ts",
"default": "./*.js"
}
}
},
"files": [
"**"
],
"sideEffects": false,
"scripts": {
"prebuild": "shx rm -rf dist",
"build": "pnpm run prebuild && pnpm run \"/^build:.*/\" && pnpm run postbuild",
"build-watch": "pnpm run \"/^build:.*/\" --watch",
"build:base": "rollup -c",
"build:utils": "rollup -c --config-utils",
"build:vanilla": "rollup -c --config-vanilla",
"build:vanilla_utils": "rollup -c --config-vanilla_utils",
"build:react": "rollup -c --config-react",
"build:react_utils": "rollup -c --config-react_utils",
"postbuild": "pnpm run patch-d-ts && pnpm run copy && pnpm run patch-old-ts && pnpm run patch-esm-ts",
"fix": "pnpm run fix:lint && pnpm run fix:format",
"fix:format": "prettier . --write",
"fix:lint": "eslint . --fix",
"test": "pnpm run \"/^test:.*/\"",
"test:format": "prettier . --list-different",
"test:types": "tsc --noEmit",
"test:lint": "eslint .",
"test:spec": "vitest run",
"patch-d-ts": "node --input-type=module -e \"import { entries } from './rollup.config.mjs'; import shelljs from 'shelljs'; const { find, sed } = shelljs; find('dist/**/*.d.ts').forEach(f => { entries.forEach(({ find, replacement }) => { sed('-i', new RegExp(' from \\'' + find.source.slice(0, -1) + '\\';$'), ' from \\'' + replacement + '\\';', f); }); sed('-i', / from '(\\.[^']+)\\.ts';$/, ' from \\'\\$1\\';', f); });\"",
"copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/{src,tests} && shx cp package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"",
"patch-old-ts": "shx touch dist/ts_version_4.5_and_above_is_required.d.ts",
"patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').renameSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\""
},
"engines": {
"node": ">=12.20.0"
},
"prettier": {
"semi": false,
"singleQuote": true
},
"repository": {
"type": "git",
"url": "git+https://github.com/pmndrs/valtio.git"
},
"keywords": [
"react",
"state",
"manager",
"management",
"mobx",
"proxy",
"store"
],
"author": "Daishi Kato",
"contributors": [],
"license": "MIT",
"bugs": {
"url": "https://github.com/pmndrs/valtio/issues"
},
"homepage": "https://github.com/pmndrs/valtio",
"packageManager": "pnpm@10.18.3",
"dependencies": {
"proxy-compare": "^3.0.1"
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@redux-devtools/extension": "^3.3.0",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/jsdom": "^28.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/eslint-plugin": "^1.6.9",
"@vitest/ui": "^4.0.18",
"esbuild": "^0.27.3",
"eslint": "^9.39.3",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-testing-library": "^7.16.0",
"jest-leak-detector": "^30.2.0",
"jsdom": "^28.1.0",
"json": "^11.0.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.8.1",
"proxy-memoize": "^3.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"redux": "^5.0.1",
"rollup": "^4.59.0",
"rollup-plugin-esbuild": "^6.2.1",
"shelljs": "^0.10.0",
"shx": "^0.4.0",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vitest": "^4.0.18"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- .
minimumReleaseAge: 1440
================================================
FILE: rollup.config.mjs
================================================
/*global process*/
import path from 'path'
import alias from '@rollup/plugin-alias'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import typescript from '@rollup/plugin-typescript'
import esbuild from 'rollup-plugin-esbuild'
const extensions = ['.js', '.ts', '.tsx']
const { root } = path.parse(process.cwd())
export const entries = [
{ find: /.*\/vanilla\/utils\.ts$/, replacement: 'valtio/vanilla/utils' },
{ find: /.*\/react\/utils\.ts$/, replacement: 'valtio/react/utils' },
{ find: /.*\/vanilla\.ts$/, replacement: 'valtio/vanilla' },
{ find: /.*\/react\.ts$/, replacement: 'valtio/react' },
]
function external(id) {
return !id.startsWith('.') && !id.startsWith(root)
}
function getEsbuild() {
return esbuild({
target: 'es2018',
supported: { 'import-meta': true },
tsconfig: path.resolve('./tsconfig.json'),
})
}
function createDeclarationConfig(input, output) {
return {
input,
output: {
dir: output,
},
external,
plugins: [
typescript({
declaration: true,
emitDeclarationOnly: true,
outDir: output,
}),
],
}
}
function createESMConfig(input, output) {
return {
input,
output: { file: output, format: 'esm' },
external,
plugins: [
alias({ entries: entries.filter((entry) => !entry.find.test(input)) }),
resolve({ extensions }),
replace({
...(output.endsWith('.js')
? {
'import.meta.env?.MODE': 'process.env.NODE_ENV',
}
: {
'import.meta.env?.MODE':
'(import.meta.env ? import.meta.env.MODE : undefined)',
}),
delimiters: ['\\b', '\\b(?!(\\.|/))'],
preventAssignment: true,
}),
getEsbuild(),
],
}
}
function createCommonJSConfig(input, output) {
return {
input,
output: { file: output, format: 'cjs' },
external,
plugins: [
alias({ entries: entries.filter((entry) => !entry.find.test(input)) }),
resolve({ extensions }),
replace({
'import.meta.env?.MODE': 'process.env.NODE_ENV',
delimiters: ['\\b', '\\b(?!(\\.|/))'],
preventAssignment: true,
}),
getEsbuild(),
],
}
}
export default function (args) {
let c = Object.keys(args).find((key) => key.startsWith('config-'))
if (c) {
c = c.slice('config-'.length).replace(/_/g, '/')
} else {
c = 'index'
}
return [
...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []),
createCommonJSConfig(`src/${c}.ts`, `dist/${c}.js`),
createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`),
]
}
================================================
FILE: src/index.ts
================================================
export * from './vanilla.ts'
export * from './react.ts'
================================================
FILE: src/react/utils/useProxy.ts
================================================
import { useLayoutEffect } from 'react'
import { useSnapshot } from '../../react.ts'
const DUMMY_SYMBOL = Symbol()
/**
* useProxy
*
* Takes a proxy and returns a new proxy which you can use in both react render
* and in callbacks. The root reference is replaced on every render, but the
* keys (and subkeys) below it are stable until they're intentionally mutated.
* For the best ergonomics, you can export a custom hook from your store, so you
* don't have to figure out a separate name for the hook reference. E.g.:
*
* @example
* export const store = proxy(initialState)
* export const useStore = () => useProxy(store)
* // in the component file:
* function Cmp() {
* const store = useStore()
* return <button onClick={() => {store.count++}}>{store.count}</button>
* }
*
*/
export function useProxy<T extends object>(
proxy: T,
options?: NonNullable<Parameters<typeof useSnapshot>[1]>,
): T {
const snapshot = useSnapshot(proxy, options) as T
// touch dummy prop so that it doesn't trigger re-renders when no props are touched.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
;(snapshot as any)[DUMMY_SYMBOL]
let isRendering = true
useLayoutEffect(() => {
// This is an intentional hack
// It might not work with React Compiler
// eslint-disable-next-line react-hooks/immutability, react-hooks/exhaustive-deps
isRendering = false
})
return new Proxy(proxy, {
get(target, prop) {
return isRendering ? snapshot[prop as keyof T] : target[prop as keyof T]
},
})
}
================================================
FILE: src/react/utils.ts
================================================
export { useProxy } from './utils/useProxy.ts'
================================================
FILE: src/react.ts
================================================
import {
useCallback,
useDebugValue,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useSyncExternalStore,
} from 'react'
import {
affectedToPathList,
createProxy as createProxyToCompare,
isChanged,
} from 'proxy-compare'
import { snapshot, subscribe } from './vanilla.ts'
import type { Snapshot } from './vanilla.ts'
/**
* React hook to display affected paths in React DevTools for debugging
*
* This internal hook collects the paths that were accessed during render
* and displays them in React DevTools to help with debugging render optimizations.
*/
const useAffectedDebugValue = (
state: object,
affected: WeakMap<object, unknown>,
) => {
const pathList = useRef<(string | number | symbol)[][]>(undefined)
useEffect(() => {
pathList.current = affectedToPathList(state, affected, true)
})
// TODO should we use useState instead?
// eslint-disable-next-line react-hooks/refs
useDebugValue(pathList.current)
}
const condUseAffectedDebugValue = useAffectedDebugValue
// This is required only for performance.
// Ref: https://github.com/pmndrs/valtio/issues/519
const targetCache = new WeakMap()
type Options = {
sync?: boolean
}
/**
* useSnapshot
*
* Create a local snapshot that catches changes. This hook actually returns a wrapped snapshot in a proxy for
* render optimization instead of a plain object compared to `snapshot()` method.
* Rule of thumb: read from snapshots, mutate the source.
* The component will only re-render when the parts of the state you access have changed, it is render-optimized.
*
* @example A
* function Counter() {
* const snap = useSnapshot(state)
* return (
* <div>
* {snap.count}
* <button onClick={() => ++state.count}>+1</button>
* </div>
* )
* }
*
* [Notes]
* Every object inside your proxy also becomes a proxy (if you don't use "ref"), so you can also use them to create
* the local snapshot as seen on example B.
*
* @example B
* function ProfileName() {
* const snap = useSnapshot(state.profile)
* return (
* <div>
* {snap.name}
* </div>
* )
* }
*
* Beware that you still can replace the child proxy with something else so it will break your snapshot. You can see
* above what happens with the original proxy when you replace the child proxy.
*
* > console.log(state)
* { profile: { name: "valtio" } }
* > childState = state.profile
* > console.log(childState)
* { name: "valtio" }
* > state.profile.name = "react"
* > console.log(childState)
* { name: "react" }
* > state.profile = { name: "new name" }
* > console.log(childState)
* { name: "react" }
* > console.log(state)
* { profile: { name: "new name" } }
*
* `useSnapshot()` depends on the original reference of the child proxy so if you replace it with a new one, the
* component that is subscribed to the old proxy won't receive new updates because it is still subscribed to
* the old one.
*
* In this case we recommend the example C or D. On both examples you don't need to worry with re-render,
* because it is render-optimized.
*
* @example C
* const snap = useSnapshot(state)
* return (
* <div>
* {snap.profile.name}
* </div>
* )
*
* @example D
* const { profile } = useSnapshot(state)
* return (
* <div>
* {profile.name}
* </div>
* )
*/
export function useSnapshot<T extends object>(
proxyObject: T,
options?: Options,
): Snapshot<T> {
const notifyInSync = options?.sync
// per-proxy & per-hook affected, it's not ideal but memo compatible
const affected = useMemo(
() => proxyObject && new WeakMap<object, unknown>(),
[proxyObject],
)
const lastSnapshot = useRef<Snapshot<T>>(undefined)
let inRender = true
const currSnapshot = useSyncExternalStore(
useCallback(
(callback) => {
const unsub = subscribe(proxyObject, callback, notifyInSync)
callback() // Note: do we really need this?
return unsub
},
[proxyObject, notifyInSync],
),
() => {
const nextSnapshot = snapshot(proxyObject)
try {
if (
!inRender &&
lastSnapshot.current &&
!isChanged(
lastSnapshot.current,
nextSnapshot,
affected,
new WeakMap(),
)
) {
// not changed
return lastSnapshot.current
}
} catch {
// ignore if a promise or something is thrown
}
return nextSnapshot
},
() => snapshot(proxyObject),
)
// TODO how could we bypass this?
// eslint-disable-next-line react-hooks/immutability
inRender = false
useLayoutEffect(() => {
lastSnapshot.current = currSnapshot
})
if (import.meta.env?.MODE !== 'production') {
condUseAffectedDebugValue(currSnapshot as object, affected)
}
const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
return createProxyToCompare(currSnapshot, affected, proxyCache, targetCache)
}
================================================
FILE: src/types.d.ts
================================================
declare interface ImportMeta {
env?: {
MODE: string
}
}
================================================
FILE: src/utils.ts
================================================
export * from './vanilla/utils.ts'
export * from './react/utils.ts'
================================================
FILE: src/vanilla/utils/deepClone.ts
================================================
import { unstable_getInternalStates } from '../../vanilla.ts'
const isObject = (x: unknown): x is object =>
typeof x === 'object' && x !== null
let defaultRefSet: WeakSet<object> | undefined
const getDefaultRefSet = (): WeakSet<object> => {
if (!defaultRefSet) {
defaultRefSet = unstable_getInternalStates().refSet
}
return defaultRefSet
}
/**
* Creates a deep clone of an object, maintaining proxy behavior for Maps and Sets
*/
export function deepClone<T>(
obj: T,
getRefSet: () => WeakSet<object> = getDefaultRefSet,
): T {
if (!isObject(obj) || getRefSet().has(obj)) {
return obj
}
const baseObject: T = Array.isArray(obj)
? []
: Object.create(Object.getPrototypeOf(obj))
Reflect.ownKeys(obj).forEach((key) => {
baseObject[key as keyof T] = deepClone(obj[key as keyof T], getRefSet)
})
return baseObject
}
================================================
FILE: src/vanilla/utils/deepProxy.ts
===========
gitextract_vdkwfl3k/
├── .codesandbox/
│ └── ci.json
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── bug-report.yml
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── compressed-size.yml
│ ├── preview-release.yml
│ ├── publish.yml
│ ├── test-multiple-builds.yml
│ ├── test-multiple-versions.yml
│ ├── test-old-typescript.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs/
│ ├── api/
│ │ ├── advanced/
│ │ │ ├── ref.mdx
│ │ │ ├── snapshot.mdx
│ │ │ ├── subscribe-ops.mdx
│ │ │ └── subscribe.mdx
│ │ ├── basic/
│ │ │ ├── proxy.mdx
│ │ │ └── useSnapshot.mdx
│ │ ├── hacks/
│ │ │ ├── getVersion.mdx
│ │ │ └── internals.mdx
│ │ └── utils/
│ │ ├── derive.mdx
│ │ ├── devtools.mdx
│ │ ├── proxyMap.mdx
│ │ ├── proxySet.mdx
│ │ ├── proxyWithHistory.mdx
│ │ ├── subscribeKey.mdx
│ │ ├── unstable_deepProxy.mdx
│ │ └── watch.mdx
│ ├── guides/
│ │ ├── async.mdx
│ │ ├── component-state.mdx
│ │ ├── computed-properties.mdx
│ │ └── migrating-to-v2.mdx
│ ├── how-tos/
│ │ ├── how-to-avoid-rerenders-manually.mdx
│ │ ├── how-to-easily-access-the-state-from-anywhere-in-the-application.mdx
│ │ ├── how-to-organize-actions.mdx
│ │ ├── how-to-persist-states.mdx
│ │ ├── how-to-reset-state.mdx
│ │ ├── how-to-split-and-compose-states.mdx
│ │ ├── how-to-update-values-inside-arrays.mdx
│ │ ├── how-to-use-with-context.mdx
│ │ ├── how-valtio-works.mdx
│ │ └── some-gotchas.mdx
│ ├── introduction/
│ │ └── getting-started.mdx
│ ├── introduction.mdx
│ ├── readme.md
│ └── resources/
│ ├── community.mdx
│ ├── learn.mdx
│ └── libraries.mdx
├── eslint.config.mjs
├── examples/
│ ├── README.md
│ ├── counter/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ ├── prism.css
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── editor-proxyWithHistory/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── photo-booth-vanillajs/
│ │ ├── index.html
│ │ ├── package.json
│ │ └── src/
│ │ ├── index.css
│ │ └── main.js
│ ├── starter/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── subscribe/
│ │ └── index.html
│ ├── todo/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── main.tsx
│ │ │ ├── prism.css
│ │ │ ├── store.ts
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ └── todo-with-proxyMap/
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── AddTodoInput.tsx
│ │ ├── App.tsx
│ │ ├── Filter.tsx
│ │ ├── TodoItem.tsx
│ │ ├── TodoList.tsx
│ │ ├── main.tsx
│ │ ├── react-app-env.d.ts
│ │ ├── store.ts
│ │ └── styles.css
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── package.json
├── pnpm-workspace.yaml
├── rollup.config.mjs
├── src/
│ ├── index.ts
│ ├── react/
│ │ ├── utils/
│ │ │ └── useProxy.ts
│ │ └── utils.ts
│ ├── react.ts
│ ├── types.d.ts
│ ├── utils.ts
│ ├── vanilla/
│ │ ├── utils/
│ │ │ ├── deepClone.ts
│ │ │ ├── deepProxy.ts
│ │ │ ├── devtools.ts
│ │ │ ├── proxyMap.ts
│ │ │ ├── proxySet.ts
│ │ │ ├── subscribeKey.ts
│ │ │ └── watch.ts
│ │ └── utils.ts
│ └── vanilla.ts
├── tests/
│ ├── async.test.tsx
│ ├── basic.test.tsx
│ ├── class.test.tsx
│ ├── deepClone.test.tsx
│ ├── deepProxy.test.tsx
│ ├── devtools.test.tsx
│ ├── getter.test.tsx
│ ├── mapset.test.tsx
│ ├── memoryleaks.test.ts
│ ├── optimization.test.tsx
│ ├── performance.test.tsx
│ ├── proxyMap.bench.ts
│ ├── proxyMap.test.tsx
│ ├── proxySet.test.tsx
│ ├── ref.test.tsx
│ ├── setup.ts
│ ├── snapshot.test.ts
│ ├── subscribe.test.tsx
│ ├── useProxy.test.tsx
│ ├── utils.tsx
│ └── watch.test.tsx
├── tsconfig.json
├── vitest.config.mts
└── website/
├── .eslintrc.json
├── .gitignore
├── README.md
├── _utils/
│ ├── file_helpers.ts
│ └── index.ts
├── components/
│ ├── LandingPage/
│ │ ├── AnimatedShapes.tsx
│ │ ├── CodeExample.tsx
│ │ ├── GettingStarted.tsx
│ │ ├── state.ts
│ │ └── useFloatAnimation.tsx
│ ├── MDXRenderer/
│ │ ├── MDXRenderer.tsx
│ │ └── index.ts
│ ├── SEO/
│ │ ├── SEO.tsx
│ │ └── index.ts
│ ├── ToggleTheme/
│ │ ├── ToggleTheme.tsx
│ │ └── index.ts
│ └── layouts/
│ ├── BasicLayout/
│ │ ├── BasicLayout.tsx
│ │ └── index.ts
│ ├── DocLayout/
│ │ ├── DocLayout.tsx
│ │ └── index.ts
│ ├── Header/
│ │ ├── Header.tsx
│ │ └── index.ts
│ └── index.ts
├── hooks/
│ ├── index.ts
│ ├── useCodesandboxTheme.ts
│ ├── useIsomorphicLayoutEffect.ts
│ └── useTheme.ts
├── lib/
│ ├── mdx.ts
│ └── remarkCodeSandboxURLUpdater.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── docs/
│ │ └── [...slug].tsx
│ └── index.tsx
├── postcss.config.js
├── state/
│ ├── index.ts
│ └── useThemeState.ts
├── styles/
│ ├── landing-page.css
│ ├── prism-theme.css
│ └── tailwind.css
├── tailwind.config.js
├── tsconfig.json
└── types.d.ts
SYMBOL INDEX (199 symbols across 48 files)
FILE: examples/counter/src/App.tsx
function App (line 113) | function App() {
FILE: examples/editor-proxyWithHistory/src/App.tsx
function App (line 12) | function App() {
FILE: examples/photo-booth-vanillajs/src/main.js
function loadImages (line 53) | async function loadImages() {
function removeImage (line 58) | function removeImage(id) {
function selectImage (line 62) | function selectImage(id) {
function saveImage (line 73) | function saveImage(url) {
function renderImages (line 79) | function renderImages(images, selectedId) {
function keepCandidateImage (line 105) | function keepCandidateImage(e) {
function removeCandidateImage (line 111) | function removeCandidateImage(e) {
function renderCanvasControls (line 116) | function renderCanvasControls(hasCandidateImage) {
function startMediaStream (line 130) | async function startMediaStream() {
FILE: examples/starter/src/index.tsx
function App (line 27) | function App() {
FILE: examples/todo-with-proxyMap/src/AddTodoInput.tsx
function AddTodoInput (line 5) | function AddTodoInput() {
FILE: examples/todo-with-proxyMap/src/App.tsx
function App (line 7) | function App() {
FILE: examples/todo-with-proxyMap/src/Filter.tsx
function Filter (line 4) | function Filter() {
FILE: examples/todo-with-proxyMap/src/TodoItem.tsx
function TodoItem (line 6) | function TodoItem({ todo }: { todo: Todo }) {
FILE: examples/todo-with-proxyMap/src/TodoList.tsx
function TodoList (line 5) | function TodoList() {
FILE: examples/todo-with-proxyMap/src/store.ts
type Todo (line 4) | interface Todo {
type Filter (line 11) | type Filter = 'all' | 'todo' | 'done'
type Store (line 13) | interface Store {
method addTodo (line 25) | addTodo(todo: Omit<Todo, 'id'>) {
method toggleTodo (line 29) | toggleTodo(id: number, value: boolean) {
method toggleAll (line 34) | toggleAll(completed: boolean) {
method removeTodo (line 39) | removeTodo(id: number) {
method toggleFilter (line 42) | toggleFilter(filter: Filter) {
method updateTodo (line 45) | updateTodo(id: number, value: string) {
function useTodos (line 51) | function useTodos() {
function useTodosCount (line 70) | function useTodosCount() {
function useFilter (line 88) | function useFilter() {
FILE: examples/todo/src/App.tsx
function App (line 4) | function App() {
function AddTodoInput (line 15) | function AddTodoInput() {
function TodoList (line 35) | function TodoList() {
function TodoRow (line 46) | function TodoRow({ todo }: { todo: Todo }) {
function FilterRow (line 72) | function FilterRow() {
FILE: examples/todo/src/store.ts
type Todo (line 3) | interface Todo {
type Filter (line 10) | type Filter = 'all' | 'completed'
type Store (line 12) | interface Store {
method addTodo (line 24) | addTodo(todo: Omit<Todo, 'id'>) {
method toggleTodo (line 30) | toggleTodo(id: number, value: boolean) {
method removeTodo (line 35) | removeTodo(id: number) {
method toggleFilter (line 38) | toggleFilter(filter: Filter) {
function useTodos (line 43) | function useTodos() {
function useFilter (line 56) | function useFilter() {
FILE: rollup.config.mjs
function external (line 18) | function external(id) {
function getEsbuild (line 22) | function getEsbuild() {
function createDeclarationConfig (line 30) | function createDeclarationConfig(input, output) {
function createESMConfig (line 47) | function createESMConfig(input, output) {
function createCommonJSConfig (line 72) | function createCommonJSConfig(input, output) {
FILE: src/react.ts
type Options (line 42) | type Options = {
function useSnapshot (line 119) | function useSnapshot<T extends object>(
FILE: src/react/utils/useProxy.ts
constant DUMMY_SYMBOL (line 4) | const DUMMY_SYMBOL = Symbol()
function useProxy (line 25) | function useProxy<T extends object>(
FILE: src/types.d.ts
type ImportMeta (line 1) | interface ImportMeta {
FILE: src/vanilla.ts
type AnyFunction (line 7) | type AnyFunction = (...args: any[]) => any
type ProxyObject (line 10) | type ProxyObject = object
type Path (line 13) | type Path = (string | symbol)[]
type Op (line 20) | type Op =
type Listener (line 25) | type Listener = (op: Op | undefined, nextVersion: number) => void
type INTERNAL_Op (line 27) | type INTERNAL_Op = Op
type Primitive (line 30) | type Primitive = string | number | boolean | null | undefined | symbol |...
type SnapshotIgnore (line 33) | type SnapshotIgnore =
type Snapshot (line 47) | type Snapshot<T> = T extends { $$valtioSnapshot: infer S }
type RemoveListener (line 55) | type RemoveListener = () => void
type AddListener (line 56) | type AddListener = (listener: Listener) => RemoveListener
type ProxyState (line 58) | type ProxyState = readonly [
method deleteProperty (line 127) | deleteProperty(target: T, prop: string | symbol) {
method set (line 136) | set(target: T, prop: string | symbol, value: any, receiver: object) {
function proxy (line 185) | function proxy<T extends object>(baseObject: T = {} as T): T {
function getVersion (line 304) | function getVersion(proxyObject: unknown): number | undefined {
function subscribe (line 312) | function subscribe<T extends object>(
function snapshot (line 353) | function snapshot<T extends object>(proxyObject: T): Snapshot<T> {
function ref (line 368) | function ref<T extends object>(obj: T) {
function unstable_getInternalStates (line 377) | function unstable_getInternalStates(): {
function unstable_replaceInternalFunction (line 418) | function unstable_replaceInternalFunction(
function unstable_enableOp (line 448) | function unstable_enableOp(
FILE: src/vanilla/utils/deepClone.ts
function deepClone (line 17) | function deepClone<T>(
FILE: src/vanilla/utils/deepProxy.ts
function unstable_deepProxy (line 28) | function unstable_deepProxy<T>(
FILE: src/vanilla/utils/devtools.ts
type Message (line 5) | type Message = {
constant DEVTOOLS (line 11) | const DEVTOOLS = Symbol()
type Config (line 13) | type Config = Parameters<
type Options (line 19) | type Options = {
function devtools (line 37) | function devtools<T extends object>(
FILE: src/vanilla/utils/proxyMap.ts
type InternalProxyObject (line 6) | type InternalProxyObject<K, V> = Map<K, V> & {
function proxyMap (line 55) | function proxyMap<K, V>(entries?: Iterable<[K, V]> | undefined | null) {
FILE: src/vanilla/utils/proxySet.ts
type RSetLike (line 7) | type RSetLike<T> = { has(value: T): boolean }
type InternalProxySet (line 9) | type InternalProxySet<T> = Set<T> & {
function proxySet (line 57) | function proxySet<T>(initialValues?: Iterable<T> | null) {
FILE: src/vanilla/utils/subscribeKey.ts
function subscribeKey (line 14) | function subscribeKey<T extends object, K extends keyof T>(
FILE: src/vanilla/utils/watch.ts
type Cleanup (line 3) | type Cleanup = () => void
type WatchGet (line 4) | type WatchGet = <T extends object>(proxyObject: T) => T
type WatchCallback (line 5) | type WatchCallback = (
type WatchOptions (line 8) | type WatchOptions = {
function watch (line 37) | function watch(
FILE: tests/class.test.tsx
class CountClass (line 17) | class CountClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
class CountClass (line 50) | class CountClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
class BaseClass (line 107) | class BaseClass {
method constructor (line 109) | constructor() {
method constructor (line 199) | constructor() {
method doubled (line 202) | public doubled() {
class CountClass (line 113) | class CountClass extends BaseClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
class CountClass (line 147) | class CountClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
class BaseClass (line 197) | class BaseClass {
method constructor (line 109) | constructor() {
method constructor (line 199) | constructor() {
method doubled (line 202) | public doubled() {
class CountClass (line 206) | class CountClass extends BaseClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
class CountClass (line 262) | class CountClass {
method constructor (line 19) | constructor() {
method constructor (line 53) | constructor() {
method constructor (line 115) | constructor() {
method constructor (line 149) | constructor() {
method doubled (line 152) | public doubled() {
method constructor (line 208) | constructor() {
method constructor (line 265) | constructor() {
method count1 (line 269) | get count1() {
method sum (line 272) | get sum() {
FILE: tests/deepProxy.test.tsx
method get (line 251) | get() {
method set (line 254) | set(v: number) {
class Counter (line 272) | class Counter {
method constructor (line 274) | constructor(n: number) {
method inc (line 277) | inc() {
FILE: tests/getter.test.tsx
method doubled (line 19) | get doubled() {
method doubled (line 59) | get doubled() {
FILE: tests/performance.test.tsx
constant DEPTHS (line 4) | const DEPTHS = [4, 8, 16, 32, 64, 128, 256]
constant REPEATS (line 5) | const REPEATS = 5000
FILE: tests/proxyMap.bench.ts
function generateTestData (line 6) | function generateTestData(size: number): [number, number][] {
constant TEST_SIZES (line 14) | const TEST_SIZES = [1000, 10_000, 100_000]
FILE: tests/proxyMap.test.tsx
type MapItem (line 746) | interface MapItem {
type MapItem (line 816) | interface MapItem {
FILE: tests/proxySet.test.tsx
type MapItem (line 718) | interface MapItem {
type MapItem (line 777) | interface MapItem {
FILE: tests/snapshot.test.ts
type A (line 91) | type A = Snapshot<{
type B (line 100) | type B = {
type A (line 114) | type A = Snapshot<number[]>
type B (line 115) | type B = readonly number[]
type A (line 121) | type A = Snapshot<{
type B (line 130) | type B = {
type A (line 144) | type A = Snapshot<{ key: string }[]>
type B (line 145) | type B = readonly { readonly key: string }[]
type A (line 151) | type A = Snapshot<{
type B (line 162) | type B = {
class User (line 178) | class User {
method hasRole (line 183) | hasRole(role: string): boolean {
type A (line 188) | type A = Snapshot<typeof user>
type B (line 189) | type B = {
type A (line 205) | type A = Snapshot<{
type B (line 209) | type B = {
FILE: tests/utils.tsx
function sleep (line 3) | function sleep(ms: number): Promise<void> {
function useCommitCount (line 9) | function useCommitCount(initialCount = 0): number {
FILE: website/_utils/file_helpers.ts
type fn (line 4) | type fn = (...args: any[]) => any
FILE: website/_utils/index.ts
function classNames (line 5) | function classNames(...classes: string[]) {
FILE: website/components/LandingPage/useFloatAnimation.tsx
type AnimationName (line 6) | type AnimationName = 'float' | 'float-mid' | 'float-rotate-mid' | 'float...
type Animation (line 7) | type Animation = {
function useFloatAnimation (line 33) | function useFloatAnimation(
FILE: website/components/MDXRenderer/MDXRenderer.tsx
type Props (line 5) | interface Props {
function MDXRenderer (line 10) | function MDXRenderer({ mdxSource, frontMatter }: Props) {
FILE: website/components/SEO/SEO.tsx
type SeoProps (line 3) | interface SeoProps {
function SEO (line 9) | function SEO({ title }: SeoProps) {
FILE: website/components/ToggleTheme/ToggleTheme.tsx
function ToggleTheme (line 4) | function ToggleTheme() {
FILE: website/components/layouts/BasicLayout/BasicLayout.tsx
type Props (line 3) | interface Props {}
function BasicLayout (line 4) | function BasicLayout({
FILE: website/components/layouts/DocLayout/DocLayout.tsx
type NavItemProps (line 10) | interface NavItemProps extends Partial<Navigation> {
function nearestScrollableContainer (line 51) | function nearestScrollableContainer(el?: Element) {
type NavProps (line 84) | interface NavProps {
function Nav (line 90) | function Nav({
type WrapperProps (line 263) | interface WrapperProps {
function Wrapper (line 267) | function Wrapper({
type Props (line 278) | interface Props {
type ContextProps (line 285) | interface ContextProps {
function DocLayout (line 297) | function DocLayout({
FILE: website/components/layouts/Header/Header.tsx
function NavPopover (line 9) | function NavPopover({
function NavItems (line 93) | function NavItems() {
type HeaderProps (line 105) | interface HeaderProps {
function Header (line 114) | function Header({
FILE: website/lib/mdx.ts
function handleEmbedderError (line 24) | function handleEmbedderError({ url }: { url: string }) {
type GottenHTML (line 28) | type GottenHTML = string | null
function handleEmbedderHtml (line 29) | function handleEmbedderHtml(html: GottenHTML, info: TransformerInfo) {
function makeEmbed (line 45) | function makeEmbed(html: string, type: string) {
function getAllDocs (line 67) | function getAllDocs() {
function formatSlug (line 75) | function formatSlug(slug: string) {
function getSlugs (line 79) | function getSlugs(p: string) {
function dateSortDesc (line 83) | function dateSortDesc(a: any, b: any) {
function getSourceFromSlug (line 89) | function getSourceFromSlug(slug: string) {
function getDocBySlug (line 95) | async function getDocBySlug(slug: string) {
function getAllFilesFrontMatter (line 180) | async function getAllFilesFrontMatter(folder: string) {
function prepareDoc (line 218) | function prepareDoc(doc: string) {
type PageNavigation (line 231) | type PageNavigation = Record<string, Navigation[]>
type NavigationTree (line 233) | type NavigationTree = Record<string, Navigation[] | PageNavigation>
function getDocsMap (line 235) | function getDocsMap(): Record<string, Navigation> {
function getDocsNav (line 244) | function getDocsNav(): NavigationTree {
FILE: website/lib/remarkCodeSandboxURLUpdater.ts
type LinkNode (line 14) | interface LinkNode extends Node {
function visitor (line 26) | function visitor(linkNode: LinkNode) {
function transform (line 40) | function transform(tree: Node) {
FILE: website/pages/_app.tsx
type NextPageWithLayout (line 8) | type NextPageWithLayout = NextPage & {
type AppPropsWithLayout (line 15) | type AppPropsWithLayout = AppProps & {
function MyApp (line 19) | function MyApp({ Component, pageProps }: AppPropsWithLayout) {
FILE: website/pages/_document.tsx
function Document (line 38) | function Document() {
FILE: website/pages/docs/[...slug].tsx
type Props (line 31) | interface Props {
function Doc (line 36) | function Doc({ doc, nav }: Props) {
FILE: website/types.d.ts
type Dict (line 1) | type Dict = Record<string, any>
type Navigation (line 2) | interface Navigation {
type Doc (line 10) | interface Doc {
Condensed preview — 203 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (471K chars).
[
{
"path": ".codesandbox/ci.json",
"chars": 206,
"preview": "{\n \"packages\": [\"dist\"],\n \"sandboxes\": [\n \"new\",\n \"react-typescript-react-ts\",\n \"simple-react-browserify-x9yn"
},
{
"path": ".github/DISCUSSION_TEMPLATE/bug-report.yml",
"chars": 588,
"preview": "labels: ['bug']\nbody:\n - type: markdown\n attributes:\n value: If you don't have a reproduction link, please choo"
},
{
"path": ".github/FUNDING.yml",
"chars": 859,
"preview": "# These are supported funding model platforms\n\ngithub: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled username"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 171,
"preview": "---\nname: Assigned issue\nabout: This is to create a new issue that already has an assignee. Please open a new discussion"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 466,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Bug Reports\n url: https://github.com/pmndrs/valtio/discussions/n"
},
{
"path": ".github/pull_request_template.md",
"chars": 137,
"preview": "## Related Bug Reports or Discussions\n\nFixes #\n\n## Summary\n\n## Check List\n\n- [ ] `pnpm run fix` for formatting and linti"
},
{
"path": ".github/workflows/compressed-size.yml",
"chars": 570,
"preview": "name: Compressed Size\n\non: [pull_request]\n\njobs:\n compressed_size:\n runs-on: ubuntu-latest\n steps:\n - uses: "
},
{
"path": ".github/workflows/preview-release.yml",
"chars": 568,
"preview": "name: Preview Release\n\non: [push, pull_request]\n\njobs:\n preview_release:\n runs-on: ubuntu-latest\n steps:\n - "
},
{
"path": ".github/workflows/publish.yml",
"chars": 638,
"preview": "name: Publish\n\non:\n release:\n types: [published]\n\npermissions:\n id-token: write\n contents: read\n\njobs:\n publish:\n"
},
{
"path": ".github/workflows/test-multiple-builds.yml",
"chars": 1844,
"preview": "name: Test Multiple Builds\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n t"
},
{
"path": ".github/workflows/test-multiple-versions.yml",
"chars": 1510,
"preview": "name: Test Multiple Versions\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n "
},
{
"path": ".github/workflows/test-old-typescript.yml",
"chars": 2687,
"preview": "name: Test Old TypeScript\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n te"
},
{
"path": ".github/workflows/test.yml",
"chars": 698,
"preview": "name: Test\n\non:\n push:\n branches: [main]\n pull_request:\n types: [opened, synchronize]\n\njobs:\n test:\n runs-on"
},
{
"path": ".gitignore",
"chars": 400,
"preview": "# dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\nbuild\n\n# dotenv environment variables fi"
},
{
"path": ".prettierignore",
"chars": 20,
"preview": "dist\npnpm-lock.yaml\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 3935,
"preview": "# Contributing\n\n## General Guideline\n\n### Reporting Issues\n\nIf you have found what you think is a bug, please [start a d"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2020 Poimandres\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 10900,
"preview": "<img src=\"logo.svg\" alt=\"valtio\">\n<br />\n<br />\n\n<code>npm install valtio</code> makes proxy-state simple\n\n[\n\n# Valtio\n\n### "
},
{
"path": "docs/introduction.mdx",
"chars": 15,
"preview": "# Introduction\n"
},
{
"path": "docs/readme.md",
"chars": 529,
"preview": "## How to contribute\n\n### Basic things to know before adding docs\n\n- Docs live in `docs/` folder.\n- Website lives in `we"
},
{
"path": "docs/resources/community.mdx",
"chars": 837,
"preview": "---\ntitle: 'Community'\ndescription: 'Discussion, wikis, and other community resources'\nsection: 'Resources'\n---\n\n# Commu"
},
{
"path": "docs/resources/learn.mdx",
"chars": 389,
"preview": "---\ntitle: 'Learn'\ndescription: 'Courses, tutorials, and more'\nsection: 'Resources'\n---\n\n# Learn\n\n- [learn valtio](https"
},
{
"path": "docs/resources/libraries.mdx",
"chars": 2999,
"preview": "---\ntitle: 'Libraries'\ndescription: 'Libraries related to Valtio'\nsection: 'Resources'\n---\n\n# Libraries\n\nValtio provides"
},
{
"path": "eslint.config.mjs",
"chars": 3039,
"preview": "import eslint from '@eslint/js'\nimport vitest from '@vitest/eslint-plugin'\nimport { defineConfig, globalIgnores } from '"
},
{
"path": "examples/README.md",
"chars": 340,
"preview": "# Examples\n\n## Simple examples\n\n- [Starter](https://github.com/pmndrs/valtio/tree/main/examples/starter)\n- [Counter](htt"
},
{
"path": "examples/counter/index.html",
"chars": 506,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "examples/counter/package.json",
"chars": 619,
"preview": "{\n \"name\": \"counter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n "
},
{
"path": "examples/counter/src/App.tsx",
"chars": 3739,
"preview": "import { proxy, useSnapshot } from 'valtio'\nimport Lowlight from 'react-lowlight'\nimport typescript from 'highlight.js/l"
},
{
"path": "examples/counter/src/index.css",
"chars": 1456,
"preview": "@import url('https://rsms.me/inter/inter.css');\nhtml {\n font-family: 'Inter', sans-serif;\n}\n\n* {\n box-sizing: border-b"
},
{
"path": "examples/counter/src/main.tsx",
"chars": 251,
"preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App.tsx'\nimport './p"
},
{
"path": "examples/counter/src/prism.css",
"chars": 3102,
"preview": "/**\n * VS theme by Andrew Lock (https://andrewlock.net)\n * Inspired by Visual Studio syntax coloring\n */\n\ncode[class*='l"
},
{
"path": "examples/counter/src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "examples/counter/tsconfig.app.json",
"chars": 552,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "examples/counter/tsconfig.json",
"chars": 119,
"preview": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n"
},
{
"path": "examples/counter/tsconfig.node.json",
"chars": 479,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2023\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true"
},
{
"path": "examples/counter/vite.config.ts",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "examples/editor-proxyWithHistory/index.html",
"chars": 366,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "examples/editor-proxyWithHistory/package.json",
"chars": 570,
"preview": "{\n \"name\": \"editor-proxyWithHistory\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \""
},
{
"path": "examples/editor-proxyWithHistory/src/App.tsx",
"chars": 1055,
"preview": "import React from 'react'\nimport { useSnapshot } from 'valtio'\nimport { proxyWithHistory } from 'valtio-history'\n\nconst "
},
{
"path": "examples/editor-proxyWithHistory/src/index.css",
"chars": 300,
"preview": ".App {\n font-family: 'Helvetica Neue', sans-serif;\n text-align: center;\n}\n\n.info {\n display: flex;\n justify-content:"
},
{
"path": "examples/editor-proxyWithHistory/src/main.tsx",
"chars": 230,
"preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App.tsx'\nimport './i"
},
{
"path": "examples/editor-proxyWithHistory/src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "examples/editor-proxyWithHistory/tsconfig.app.json",
"chars": 552,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "examples/editor-proxyWithHistory/tsconfig.json",
"chars": 119,
"preview": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n"
},
{
"path": "examples/editor-proxyWithHistory/tsconfig.node.json",
"chars": 479,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2023\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true"
},
{
"path": "examples/editor-proxyWithHistory/vite.config.ts",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "examples/photo-booth-vanillajs/index.html",
"chars": 815,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "examples/photo-booth-vanillajs/package.json",
"chars": 321,
"preview": "{\n \"name\": \"valtio-photo-booth-demo\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\""
},
{
"path": "examples/photo-booth-vanillajs/src/index.css",
"chars": 1948,
"preview": "main {\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n\nvideo {\n wi"
},
{
"path": "examples/photo-booth-vanillajs/src/main.js",
"chars": 4523,
"preview": "import './index.css'\nimport { html, render } from 'lighterhtml'\nimport localforage from 'localforage'\nimport { proxy, sn"
},
{
"path": "examples/starter/README.md",
"chars": 577,
"preview": "# Starter [ {\n "
},
{
"path": "examples/todo/src/index.css",
"chars": 1456,
"preview": "@import url('https://rsms.me/inter/inter.css');\nhtml {\n font-family: 'Inter', sans-serif;\n}\n\n* {\n box-sizing: border-b"
},
{
"path": "examples/todo/src/main.tsx",
"chars": 251,
"preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App.tsx'\nimport './p"
},
{
"path": "examples/todo/src/prism.css",
"chars": 3102,
"preview": "/**\n * VS theme by Andrew Lock (https://andrewlock.net)\n * Inspired by Visual Studio syntax coloring\n */\n\ncode[class*='l"
},
{
"path": "examples/todo/src/store.ts",
"chars": 1200,
"preview": "import { proxy, useSnapshot } from 'valtio'\n\nexport interface Todo {\n id: number\n name: string\n completed?: boolean\n}"
},
{
"path": "examples/todo/src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "examples/todo/tsconfig.app.json",
"chars": 552,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "examples/todo/tsconfig.json",
"chars": 119,
"preview": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n"
},
{
"path": "examples/todo/tsconfig.node.json",
"chars": 479,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2023\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true"
},
{
"path": "examples/todo/vite.config.ts",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "examples/todo-with-proxyMap/index.html",
"chars": 366,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "examples/todo-with-proxyMap/package.json",
"chars": 544,
"preview": "{\n \"name\": \"counter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n "
},
{
"path": "examples/todo-with-proxyMap/src/AddTodoInput.tsx",
"chars": 677,
"preview": "import React from 'react'\n\nimport { actions } from './store'\n\nexport function AddTodoInput() {\n const [value, setValue]"
},
{
"path": "examples/todo-with-proxyMap/src/App.tsx",
"chars": 296,
"preview": "import { AddTodoInput } from './AddTodoInput'\nimport { TodoList } from './TodoList'\nimport { Filter } from './Filter'\n\ni"
},
{
"path": "examples/todo-with-proxyMap/src/Filter.tsx",
"chars": 1204,
"preview": "import cx from 'clsx'\nimport { useFilter, actions, useTodosCount } from './store'\n\nexport function Filter() {\n const co"
},
{
"path": "examples/todo-with-proxyMap/src/TodoItem.tsx",
"chars": 2069,
"preview": "import { useEffect, useRef, useState } from 'react'\nimport cx from 'clsx'\n\nimport { actions, Todo } from './store'\n\nexpo"
},
{
"path": "examples/todo-with-proxyMap/src/TodoList.tsx",
"chars": 665,
"preview": "import { actions, useTodos } from './store'\n\nimport { TodoItem } from './TodoItem'\n\nexport function TodoList() {\n const"
},
{
"path": "examples/todo-with-proxyMap/src/main.tsx",
"chars": 142,
"preview": "import { render } from 'react-dom'\n\nimport App from './App'\n\nconst rootElement = document.getElementById('root')\nrender("
},
{
"path": "examples/todo-with-proxyMap/src/react-app-env.d.ts",
"chars": 40,
"preview": "/// <reference types=\"react-scripts\" />\n"
},
{
"path": "examples/todo-with-proxyMap/src/store.ts",
"chars": 1926,
"preview": "import { proxy, useSnapshot } from 'valtio'\nimport { proxyMap } from 'valtio/utils'\n\nexport interface Todo {\n id: numbe"
},
{
"path": "examples/todo-with-proxyMap/src/styles.css",
"chars": 7522,
"preview": "/* https://www.npmjs.com/package/todomvc-app-css */\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nbutton {\n margin: 0;\n p"
},
{
"path": "examples/todo-with-proxyMap/tsconfig.app.json",
"chars": 552,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "examples/todo-with-proxyMap/tsconfig.json",
"chars": 119,
"preview": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n"
},
{
"path": "examples/todo-with-proxyMap/tsconfig.node.json",
"chars": 479,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2023\"],\n \"module\": \"ESNext\",\n \"skipLibCheck\": true"
},
{
"path": "examples/todo-with-proxyMap/vite.config.ts",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "package.json",
"chars": 5472,
"preview": "{\n \"name\": \"valtio\",\n \"description\": \"🧙 Valtio makes proxy-state simple for React and Vanilla\",\n \"private\": true,\n \""
},
{
"path": "pnpm-workspace.yaml",
"chars": 40,
"preview": "packages:\n - .\nminimumReleaseAge: 1440\n"
},
{
"path": "rollup.config.mjs",
"chars": 2679,
"preview": "/*global process*/\nimport path from 'path'\nimport alias from '@rollup/plugin-alias'\nimport resolve from '@rollup/plugin-"
},
{
"path": "src/index.ts",
"chars": 56,
"preview": "export * from './vanilla.ts'\nexport * from './react.ts'\n"
},
{
"path": "src/react/utils/useProxy.ts",
"chars": 1563,
"preview": "import { useLayoutEffect } from 'react'\nimport { useSnapshot } from '../../react.ts'\n\nconst DUMMY_SYMBOL = Symbol()\n\n/**"
},
{
"path": "src/react/utils.ts",
"chars": 47,
"preview": "export { useProxy } from './utils/useProxy.ts'\n"
},
{
"path": "src/react.ts",
"chars": 4971,
"preview": "import {\n useCallback,\n useDebugValue,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} "
},
{
"path": "src/types.d.ts",
"chars": 64,
"preview": "declare interface ImportMeta {\n env?: {\n MODE: string\n }\n}\n"
},
{
"path": "src/utils.ts",
"chars": 68,
"preview": "export * from './vanilla/utils.ts'\nexport * from './react/utils.ts'\n"
},
{
"path": "src/vanilla/utils/deepClone.ts",
"chars": 860,
"preview": "import { unstable_getInternalStates } from '../../vanilla.ts'\n\nconst isObject = (x: unknown): x is object =>\n typeof x "
},
{
"path": "src/vanilla/utils/deepProxy.ts",
"chars": 2492,
"preview": "import { proxy, unstable_getInternalStates } from '../../vanilla.ts'\nimport { isProxyMap, proxyMap } from './proxyMap.ts"
},
{
"path": "src/vanilla/utils/devtools.ts",
"chars": 4076,
"preview": "import { snapshot, subscribe, unstable_enableOp } from '../../vanilla.ts'\nimport type {} from '@redux-devtools/extension"
},
{
"path": "src/vanilla/utils/proxyMap.ts",
"chars": 5360,
"preview": "import { proxy, unstable_getInternalStates } from '../../vanilla.ts'\n\nconst { proxyStateMap, snapCache } = unstable_getI"
},
{
"path": "src/vanilla/utils/proxySet.ts",
"chars": 9091,
"preview": "import { proxy, unstable_getInternalStates } from '../../vanilla.ts'\n\nconst { proxyStateMap, snapCache } = unstable_getI"
},
{
"path": "src/vanilla/utils/subscribeKey.ts",
"chars": 935,
"preview": "import { subscribe } from '../../vanilla.ts'\n\n/**\n * subscribeKey\n *\n * The subscribeKey utility enables subscription to"
},
{
"path": "src/vanilla/utils/watch.ts",
"chars": 3940,
"preview": "import { subscribe } from '../../vanilla.ts'\n\ntype Cleanup = () => void\ntype WatchGet = <T extends object>(proxyObject: "
},
{
"path": "src/vanilla/utils.ts",
"chars": 368,
"preview": "export { subscribeKey } from './utils/subscribeKey.ts'\nexport { watch } from './utils/watch.ts'\nexport { devtools } from"
},
{
"path": "src/vanilla.ts",
"chars": 13290,
"preview": "import { getUntracked, markToTrack } from 'proxy-compare'\n\nconst isObject = (x: unknown): x is object =>\n typeof x === "
},
{
"path": "tests/async.test.tsx",
"chars": 4983,
"preview": "/// <reference types=\"react/canary\" />\n\nimport ReactExports, { StrictMode, Suspense } from 'react'\nimport { act, fireEve"
},
{
"path": "tests/basic.test.tsx",
"chars": 15954,
"preview": "import { StrictMode, useEffect, useLayoutEffect, useState } from 'react'\nimport { act, fireEvent, render, screen } from "
},
{
"path": "tests/class.test.tsx",
"chars": 7847,
"preview": "import { StrictMode } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afte"
},
{
"path": "tests/deepClone.test.tsx",
"chars": 1533,
"preview": "import { describe, expect, it } from 'vitest'\nimport { proxy } from 'valtio'\nimport { deepClone } from 'valtio/utils'\n\nd"
},
{
"path": "tests/deepProxy.test.tsx",
"chars": 9868,
"preview": "import { describe, expect, it, vi } from 'vitest'\nimport { proxy } from 'valtio'\nimport {\n unstable_deepProxy as deepPr"
},
{
"path": "tests/devtools.test.tsx",
"chars": 10894,
"preview": "import { StrictMode, Suspense } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimp"
},
{
"path": "tests/getter.test.tsx",
"chars": 2508,
"preview": "import { StrictMode } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afte"
},
{
"path": "tests/mapset.test.tsx",
"chars": 1414,
"preview": "import { StrictMode } from 'react'\nimport { fireEvent, render, screen } from '@testing-library/react'\nimport { describe,"
},
{
"path": "tests/memoryleaks.test.ts",
"chars": 4222,
"preview": "import LeakDetector from 'jest-leak-detector'\nimport { describe, expect, it } from 'vitest'\nimport { proxy, subscribe } "
},
{
"path": "tests/optimization.test.tsx",
"chars": 4672,
"preview": "import { useState } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afterE"
},
{
"path": "tests/performance.test.tsx",
"chars": 2048,
"preview": "import { describe, expect, it } from 'vitest'\nimport { proxy, snapshot, subscribe } from 'valtio'\n\nconst DEPTHS = [4, 8,"
},
{
"path": "tests/proxyMap.bench.ts",
"chars": 3735,
"preview": "import { bench, describe } from 'vitest'\nimport { snapshot } from 'valtio'\nimport { proxyMap } from 'valtio/utils'\n\n// H"
},
{
"path": "tests/proxyMap.test.tsx",
"chars": 26867,
"preview": "import { StrictMode } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afte"
},
{
"path": "tests/proxySet.test.tsx",
"chars": 27926,
"preview": "import { StrictMode } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afte"
},
{
"path": "tests/ref.test.tsx",
"chars": 3036,
"preview": "import { StrictMode } from 'react'\nimport type { ReactElement } from 'react'\nimport { act, fireEvent, render, screen } f"
},
{
"path": "tests/setup.ts",
"chars": 42,
"preview": "import '@testing-library/jest-dom/vitest'\n"
},
{
"path": "tests/snapshot.test.ts",
"chars": 6327,
"preview": "import { createProxy, getUntracked } from 'proxy-compare'\nimport { describe, expect, expectTypeOf, it } from 'vitest'\nim"
},
{
"path": "tests/subscribe.test.tsx",
"chars": 6303,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { proxy, ref, snapshot, subscribe, unsta"
},
{
"path": "tests/useProxy.test.tsx",
"chars": 3494,
"preview": "import { StrictMode } from 'react'\nimport { act, fireEvent, render, screen } from '@testing-library/react'\nimport { afte"
},
{
"path": "tests/utils.tsx",
"chars": 409,
"preview": "import { useEffect, useRef } from 'react'\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((reso"
},
{
"path": "tests/watch.test.tsx",
"chars": 3523,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { proxy } from 'valtio'\nimport { watch }"
},
{
"path": "tsconfig.json",
"chars": 721,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"esModuleInterop\": true"
},
{
"path": "vitest.config.mts",
"chars": 814,
"preview": "import { resolve } from 'path'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n resolve: {\n"
},
{
"path": "website/.eslintrc.json",
"chars": 56,
"preview": "{\n \"root\": true,\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": "website/.gitignore",
"chars": 414,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "website/README.md",
"chars": 2146,
"preview": "## Basic things to know before adding docs\n\n- Docs live in `docs/` folder.\n- Website lives in `website/` folder.\n- Docs "
},
{
"path": "website/_utils/file_helpers.ts",
"chars": 1478,
"preview": "import fs from 'fs'\nimport path from 'path'\n\ntype fn = (...args: any[]) => any\n\nconst pipe =\n (...fns: fn[]) =>\n (x: a"
},
{
"path": "website/_utils/index.ts",
"chars": 218,
"preview": "// we ignore this now cause we can't import fs in browser\n// and file_helpers uses fs\n// export * from \"./file_helpers\";"
},
{
"path": "website/components/LandingPage/AnimatedShapes.tsx",
"chars": 9368,
"preview": "import { animated } from 'react-spring'\nimport { useFloatAnimation } from './useFloatAnimation'\n\nconst Box = () => {\n c"
},
{
"path": "website/components/LandingPage/CodeExample.tsx",
"chars": 2246,
"preview": "import { useSnapshot } from 'valtio'\nimport Highlight, { defaultProps } from 'prism-react-renderer'\nimport { GettingStar"
},
{
"path": "website/components/LandingPage/GettingStarted.tsx",
"chars": 792,
"preview": "import Link from 'next/link'\n\nexport const GettingStarted = ({ className }: { className: string }) => (\n <div\n class"
},
{
"path": "website/components/LandingPage/state.ts",
"chars": 304,
"preview": "import { proxy } from 'valtio'\n\nexport const state = proxy({\n dur: 4,\n count: 0,\n})\n\nexport const incDuration = () => "
},
{
"path": "website/components/LandingPage/useFloatAnimation.tsx",
"chars": 1486,
"preview": "import { useSpring } from 'react-spring'\nimport { easeQuadInOut } from 'd3-ease'\nimport { useSnapshot } from 'valtio'\nim"
},
{
"path": "website/components/MDXRenderer/MDXRenderer.tsx",
"chars": 455,
"preview": "import { getMDXComponent } from 'mdx-bundler/client'\nimport { useMemo } from 'react'\nimport { useCodesandboxTheme } from"
},
{
"path": "website/components/MDXRenderer/index.ts",
"chars": 40,
"preview": "export { default } from './MDXRenderer'\n"
},
{
"path": "website/components/SEO/SEO.tsx",
"chars": 328,
"preview": "import Head from 'next/head'\n\ninterface SeoProps {\n title?: string\n}\n\nconst defaultTitle = 'Valtio, makes proxy-state s"
},
{
"path": "website/components/SEO/index.ts",
"chars": 32,
"preview": "export { default } from './SEO'\n"
},
{
"path": "website/components/ToggleTheme/ToggleTheme.tsx",
"chars": 971,
"preview": "import { SunIcon, MoonIcon } from '@heroicons/react/outline'\nimport { useTheme } from '~/hooks'\n\nexport default function"
},
{
"path": "website/components/ToggleTheme/index.ts",
"chars": 40,
"preview": "export { default } from './ToggleTheme'\n"
},
{
"path": "website/components/layouts/BasicLayout/BasicLayout.tsx",
"chars": 275,
"preview": "import React from 'react'\n\ninterface Props {}\nexport default function BasicLayout({\n children,\n}: React.PropsWithChildr"
},
{
"path": "website/components/layouts/BasicLayout/index.ts",
"chars": 40,
"preview": "export { default } from './BasicLayout'\n"
},
{
"path": "website/components/layouts/DocLayout/DocLayout.tsx",
"chars": 14318,
"preview": "import React, { forwardRef, useRef } from 'react'\nimport { Dialog } from '@headlessui/react'\nimport Link from 'next/link"
},
{
"path": "website/components/layouts/DocLayout/index.ts",
"chars": 38,
"preview": "export { default } from './DocLayout'\n"
},
{
"path": "website/components/layouts/Header/Header.tsx",
"chars": 9647,
"preview": "import Link from 'next/link'\nimport Router from 'next/router'\nimport { Dialog } from '@headlessui/react'\nimport React, {"
},
{
"path": "website/components/layouts/Header/index.ts",
"chars": 35,
"preview": "export { default } from './Header'\n"
},
{
"path": "website/components/layouts/index.ts",
"chars": 151,
"preview": "export { default as BasicLayout } from './BasicLayout'\nexport { default as DocLayout } from './DocLayout'\nexport { defau"
},
{
"path": "website/hooks/index.ts",
"chars": 109,
"preview": "export * from './useIsomorphicLayoutEffect'\nexport * from './useTheme'\nexport * from './useCodesandboxTheme'\n"
},
{
"path": "website/hooks/useCodesandboxTheme.ts",
"chars": 985,
"preview": "import { useCallback, useEffect } from 'react'\nimport { subscribe, snapshot } from 'valtio'\nimport { themeState } from '"
},
{
"path": "website/hooks/useIsomorphicLayoutEffect.ts",
"chars": 155,
"preview": "import { useEffect, useLayoutEffect } from 'react'\n\nexport const useIsomorphicLayoutEffect =\n typeof window !== 'undefi"
},
{
"path": "website/hooks/useTheme.ts",
"chars": 2249,
"preview": "import { useCallback, useEffect } from 'react'\nimport { themeState } from '~/state'\n\nexport const useTheme = () => {\n c"
},
{
"path": "website/lib/mdx.ts",
"chars": 8116,
"preview": "import { bundleMDX } from 'mdx-bundler'\nimport fs from 'fs'\nimport matter from 'gray-matter'\nimport path from 'path'\nimp"
},
{
"path": "website/lib/remarkCodeSandboxURLUpdater.ts",
"chars": 935,
"preview": "import type { Node } from 'unist'\nimport { visit } from 'unist-util-visit'\n\nconst codesandboxBaseUrl = 'https://codesand"
},
{
"path": "website/next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "website/next.config.js",
"chars": 336,
"preview": "// Prevent crash on Node.js 25+ where SlowBuffer was removed.\n// This error originates from a compiled version of jsonwe"
},
{
"path": "website/package.json",
"chars": 1274,
"preview": "{\n \"name\": \"website\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build\",\n \"start\": "
},
{
"path": "website/pages/_app.tsx",
"chars": 514,
"preview": "import type { NextPage } from 'next'\nimport type { AppProps } from 'next/app'\n\nimport '~/styles/tailwind.css'\nimport '~/"
},
{
"path": "website/pages/_document.tsx",
"chars": 1457,
"preview": "import { Head, Html, Main, NextScript } from 'next/document'\n\nconst modeScript = `\n let darkModeMediaQuery = window.mat"
},
{
"path": "website/pages/docs/[...slug].tsx",
"chars": 1058,
"preview": "import { GetStaticPaths, GetStaticProps } from 'next'\nimport { DocLayout } from '~/components/layouts'\nimport MDXRendere"
},
{
"path": "website/pages/index.tsx",
"chars": 488,
"preview": "import { AnimatedShapes } from '~/components/LandingPage/AnimatedShapes'\nimport { GettingStarted } from '~/components/La"
},
{
"path": "website/postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "website/state/index.ts",
"chars": 32,
"preview": "export * from './useThemeState'\n"
},
{
"path": "website/state/useThemeState.ts",
"chars": 89,
"preview": "import { proxy } from 'valtio'\nexport const themeState = proxy({\n isDarkMode: false,\n})\n"
},
{
"path": "website/styles/landing-page.css",
"chars": 2228,
"preview": "svg {\n z-index: -1;\n}\n\nhtml {\n --theme-blue: #b2ebf2;\n --base-animation-duration: 4s;\n}\n\n.top-left {\n position: fixe"
},
{
"path": "website/styles/prism-theme.css",
"chars": 2640,
"preview": "/*\n * Nord Theme Originally by Arctic Ice Studio\n * https://nordtheme.com\n *\n * Ported for PrismJS by Zane Hitchcox (@zw"
},
{
"path": "website/styles/tailwind.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
}
]
// ... and 3 more files (download for full content)
About this extraction
This page contains the full source code of the pmndrs/valtio GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 203 files (429.7 KB), approximately 122.8k tokens, and a symbol index with 199 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.