valtio Documentation

Valtio makes proxy-state simple for React and Vanilla

10,213 stars TypeScript TypeScript 1 file ยท ~11,847 tokens #mutable#proxy#react#state
RAW Doc

1. Project Overview & Quickstart (pmndrs/valtio)

File: README.md

npm install valtio makes proxy-state simple





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 (
    
      {snap.count}
       ++state.count}>+1
    
  )
}

Note for TypeScript users: Return type of useSnapshot can be too strict.

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 for more information.

Note: useSnapshot returns a new proxy for render optimization.

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.

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.

Use of this is for expert users.

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
  },
})

If you are new to this, it's highly recommended to use
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 {use(snap.post).title}
}

function App() {
  return (
    <Suspense fallback={waiting...}>
      
    </Suspense>
  )
}

It still suffers from "de-opt", which prevents useTransition to work well. To mitigate it, there is a third-party library 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 and #178 for more information.

When ref is passed an existing Valtio proxy, it returns the same proxy and globally marks it as untracked, including in later proxy composition. See the ref API documentation for details.

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 `` #270.

jsx
function TextBox() {
  const snap = useSnapshot(state, { sync: true })
  return (
     (state.text = e.target.value)} />
  )
}

Dev tools

You can use Redux DevTools Extension 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 })

Manipulating state with Redux DevTools
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.

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 (
    
      {$state.count}
       ++$state.count}>+1
    
  )
}

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.

`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.

Plugins

Recipes

Valtio is unopinionated about best practices.
The community is working on recipes.


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}</>
}
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 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 }
  1. 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"]
}

๐Ÿ’ก   Using TypeScript? Here are some links for reference

https://www.totaltypescript.com/tsconfig-cheat-sheet
https://github.com/tsconfig/bases
https://www.typescriptlang.org/tsconfig/

  1. 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',
        },
      },
    ],
    // ...
  ],
}
  1. Install the the Babel Plugin Module Resolver:
  • Using NPM: npm install babel-plugin-module-resolver
  • Using Yarn: yarn add babel-plugin-module-resolver
  1. Restart the application server

    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:


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

โ„น๏ธ   Note

This way is preferred as it is better for code splitting.

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 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)
}

โ„น๏ธ   Note

Using structuredClone()

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 refs in your state.

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-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}  ++state.count}>+1
    </>
  )
}

Alternatives

If you are not happy with useRef usage, consider:

  • use-constant
  • bunshi
  • 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

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 (() => {
    
      <h1>{ `${foo} - ${text}` }</h1>
      {/* - or - */}
      <h1>{ `${snap.foo.bar} = `${snap.text}}</h1>
      
         {
            s1.text = e.target.value
          }}
        />
      
      
        { counter }
         s1.counter++}> + 
         s1.counter--}> - 
      </di>
    
  })

}

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 has a special memo exported as a workaround.

We have some options:

  1. Do not use `React.memo`.
  2. Do not pass objects to components with `React.memo` (pass primitive values instead).
  3. Pass in the proxy of that element, and then `useSnapshot` on that proxy.

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.
  }) => (
    
      {title} - {description}
    
  ),
)

const ParentComponent = () => {
  const snap = useSnapshot(state)
  return (
    
      <ChildComponent
        title={snap.obj.title}
        description={snap.obj.description}
      />
    
  )
}

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
}
  • 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 `` 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 (
     {
        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 may arise when handling the array proxy reference in the subsequent steps:

  1. Subscribe array proxy
  2. Use the proxy as snapshot
  3. Assign temp variable for updating
  4. Remove proxy from the array
  5. Update temp
  6. Push temp in the original array

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(), 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:

text
node_modules/.pnpm/[email protected]/node_modules/valtio/esm/react.mjs (2:18): "useRef" is not exported by "__vite-optional-peer-dep:react:valtio", imported by "node_modules/.pnpm/[email protected]/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'

2. Official Technical Reference & Guides (pmndrs/docs)

File: README.md


docs/getting-started/introduction.mdx

Usage

sh
$ curl -sL https://raw.githubusercontent.com/pmndrs/docs/refs/heads/main/preview.sh | \
  MDX="docs" \
  ICON="๐Ÿฅ‘" \
  DOCKER_IMAGE="ghcr.io/pmndrs/docs:latest" \
  sh
  • you can pass any option from configuration
  • in DOCKER_IMAGE, you can specify any :tag value from docs packages container registry

Releasing

Every push to main redeploys docs.pmnd.rs via ci.yml โ€” no changeset needed for that.

Add one (pnpm changeset) only when downstream consumers pinning pmndrs/docs/.github/workflows/build.yml@v3 or ghcr.io/pmndrs/docs:v3 should pull the change. It bumps package.json, tags vX.Y.Z + vX, and publishes a matching Docker image โ€” so @v3 resolves to the latest.

TL;DR โ€” site-only tweak: skip. Anything consumers see (workflow, build behavior, templates): add one.

Test

Visual tests are performed in the cloud, through chromatic.yml.

You can also replay locally:

sh
$ npx playwright test --update-snapshots
$ npx chromatic --playwright --project-token $CHROMATIC_PROJECT_TOKEN

File: docs/getting-started/introduction.mdx


title: pmndrs/docs nav: 0

A static MDX documentation generator, with a GitHub reusable workflow. It is primarily used for some pmndrs/* projects, but will work for anyone.

Those projects are known to be using this generator.

INSTALL

Pre-requisites:

  • Install nvm, then:
    sh
    $ nvm install
    $ nvm use
    $ node -v # make sure your version satisfies package.json#engines.node
    nb: if you want this node version to be your default nvm's one:
    nvm alias default node
sh
$ git clone https://github.com/pmndrs/docs.git
$ cd docs
$ pnpm install

Configuration

[!IMPORTANT]

Default value is always: "" (think empty).

var description example
MDX* Path to *.mdx folderNB: can be relative or absolute docs or ~/code/myproject/documentation
NEXT_PUBLIC_LIBNAME* Library name React Three Fiber
NEXT_PUBLIC_LIBNAME_SHORT Library short name r3f
NEXT_PUBLIC_LIBNAME_DOTSUFFIX_LABEL Text for the ".docs" suffix link inside the header docs
NEXT_PUBLIC_LIBNAME_DOTSUFFIX_HREF Href for the ".docs" suffix link inside the header https://docs.pmnd.rs
BASE_PATH Base path for the final URL /react-three-fiber
DIST_DIR Path to the output folder (within project) out or docs/out/react-three-fiber
OUTPUT Set to export for static output export
HOME_REDIRECT Where the home should redirect /getting-started/introduction
MDX_BASEURL Base URL for inlining relative images http://localhost:60141or https://github.com/pmndrs/react-three-fiber/raw/master/docs
SOURCECODE_BASEURL Base URL for sourcecode: code path https://github.com/pmndrs/react-three-fiber/tree/main
EDIT_BASEURL Base URL for displaying "Edit this page" URLs https://github.com/pmndrs/react-three-fiber/edit/master/docs
NEXT_PUBLIC_URL Final URL of the published website https://pmndrs.github.io/react-three-fiber
ICON Emoji or image to use as (fav)icon (path local to MDX) ๐Ÿ‡จ๐Ÿ‡ญ or /icon.png or /favicon.ico
LOGO Logo src/path (either FQURL or local to MDX path) /logo.png or https://worldvectorlogo.com/r3f.png
GITHUB Github URL https://github.com/pmndrs/react-three-fiber
DISCORD Discord URL https://discord.com/channels/740090768164651008/740093168770613279
THEME_PRIMARY Primary accent color #323e48
THEME_SCHEME Theme scheme content or expressive or fidelity or monochrome or neutral or tonalSpot or vibrant
THEME_CONTRAST Theme contrast -- value between -1 and 1 0 or -1 or 1 or -.6
THEME_NOTE "note" color #1f6feb
THEME_TIP "tip" color #238636
THEME_IMPORTANT "important" color #8957e5
THEME_WARNING "warning" color #d29922
THEME_CAUTION "caution" color #da3633
CONTRIBUTORS_PAT GitHub token for contributors API (see: https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#list-repository-collaborators) ghp_1234567890

* Required

MDX_BASEURL

Given a advanced/introduction.mdx file in the MDX folder:

md

becomes (for a MDX_BASEURL=http://localhost:60141 value):

md

http://localhost:60141 being the MDX folder served.

[!TIP]
When deployed on GitHub Pages, MDX_BASEURL will typically value something like https://github.com/pmndrs/uikit/raw/main/docs, thanks to build.yml rule.

THEME_*

We implement m3 design system, using react-mcu.

[!NOTE]

  • Material Color for more information
  • We currently don't have secondary/tertiary colors (maybe some day).

Usage

dev

sh
$ (
  trap 'kill -9 0' SIGINT

  export _PORT=60141

  export MDX=docs
  export NEXT_PUBLIC_LIBNAME="Poimandres"
  export NEXT_PUBLIC_LIBNAME_SHORT="pmndrs"
  export NEXT_PUBLIC_LIBNAME_DOTSUFFIX_LABEL="docs"
  export NEXT_PUBLIC_LIBNAME_DOTSUFFIX_HREF="https://docs.pmnd.rs"
  export BASE_PATH=
  export DIST_DIR=
  export OUTPUT=
  export HOME_REDIRECT=
  export MDX_BASEURL=http://localhost:$_PORT
  export SOURCECODE_BASEURL="vscode://file$(pwd)"
  export EDIT_BASEURL="vscode://file$(pwd)/docs"
  export NEXT_PUBLIC_URL=
  export ICON=
  export LOGO=gutenberg.jpg
  export GITHUB=https://github.com/pmndrs/docs
  export DISCORD=https://discord.com/channels/740090768164651008/1264328004172255393
  export THEME_PRIMARY="#323e48"
  export THEME_SCHEME="tonalSpot"
  export THEME_CONTRAST="0"
  export THEME_NOTE="#1f6feb"
  export THEME_TIP="#238636"
  export THEME_IMPORTANT="#8957e5"
  export THEME_WARNING="#d29922"
  export THEME_CAUTION="#da3633"
  export CONTRIBUTORS_PAT=

  kill $(lsof -ti:"$_PORT")
  npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &

  pnpm run dev &

  wait
)

Then go to: http://localhost:3000

[!TIP]
If HOME_REDIRECT= empty, / will not redirect, and instead displays an index of libraries.

build

sh
$ (
  trap 'kill -9 0' SIGINT

  rm -rf out

  export _PORT=60141

  export MDX=docs
  export NEXT_PUBLIC_LIBNAME="Poimandres"
  export NEXT_PUBLIC_LIBNAME_SHORT="pmndrs"
  export NEXT_PUBLIC_LIBNAME_DOTSUFFIX_LABEL="docs"
  export NEXT_PUBLIC_LIBNAME_DOTSUFFIX_HREF="https://docs.pmnd.rs"
  export BASE_PATH=
  export DIST_DIR=
  export OUTPUT=export
  export HOME_REDIRECT=/getting-started/introduction
  export MDX_BASEURL=http://localhost:$_PORT
  export SOURCECODE_BASEURL=
  export EDIT_BASEURL=
  export NEXT_PUBLIC_URL=
  export ICON=
  export LOGO=gutenberg.jpg
  export GITHUB=https://github.com/pmndrs/docs
  export DISCORD=https://discord.com/channels/740090768164651008/1264328004172255393
  export THEME_PRIMARY="#323e48"
  export THEME_SCHEME="tonalSpot"
  export THEME_CONTRAST="0"
  export THEME_NOTE="#1f6feb"
  export THEME_TIP="#238636"
  export THEME_IMPORTANT="#8957e5"
  export THEME_WARNING="#d29922"
  export THEME_CAUTION="#da3633"
  export CONTRIBUTORS_PAT=

  pnpm run build

  kill $(lsof -ti:"$_PORT")
  npx serve $MDX -p $_PORT --no-port-switching --no-clipboard &

  npx serve out &

  wait
)

http://localhost:3000

Docker

sh
$ docker build -t pmndrs-docs .
text
/* Detailed source-code truncated for AI context efficiency. */

Then go to: http://localhost:3000

Agents

llms.txt dumps and the pmndrs MCP server moved to their own page: Agents.