Skip to main content

valtio

tip
  • 大状态好拆分合并
  • 被 proxy 对象都需要通过 snaoshot 的方式读取数据 - 所以才可以监听嵌套对象的变化
    • snaoshot 返回的是 不可变 对象
  • 修改会进行 batch - input 时 useSnapshot 可能需要加 sync
  • 特殊值需要特殊处理
  • ref/object 赋值都会触发变化,不会判断是否相等
note
  • support async in watch #507
import {proxy, useSnapshot, ref, subscribe, snapshot} from 'valtio';

// 不依赖 react
import {proxy, subscribe, snapshot} from 'valtio/vanilla';

import {
subscribeKey,
watch,
devtools,
derive,
underive,
proxyWithComputed,
proxyWithHistory,
proxySet,
proxyMap,
getVersion,
} from 'valtio/utils';

// 因为基于 proxy - 所以可以直接拆分合并
const state = proxy({
a: {v: 1},
b: {v: 2},
});
// 拆分
const a = state.a;
// 合并
const s = proxy({
obj1: a,
obj2: {v: 3},
});

// 可在任意地方订阅变化
const unsubscribe = subscribe(state, () => console.log('changed to', state));
// 订阅部分对象
const unsubscribe = subscribe(state.a, () => console.log('changed to', state));
// 订阅 primitive value
const unsubscribe = subscribeKey(state, 'a.v', () => console.log('changed to', state));

// 基于使用订阅
const stop = watch((get) => {
console.log('state has changed to', get(state)); // auto-subscribe on use
});

// React 组件内使用 snapshot 订阅变化
const Counter = () => {
const snap = useSnapshot(state);
// 直接读状态,不订阅
const {count} = state;
return (
<div>
{/* 订阅 count 变化 */}
{snap.count}
<button
onClick={() => {
// 操作 state 进行直接修改
++state.count;
}}
>
+1
</button>
</div>
);
};

function TextBox() {
// 修改默认会 batch - sync 则避免 batch
// https://github.com/pmndrs/valtio/issues/270
const snap = useSnapshot(state, {sync: true});
return <input value={snap.text} onChange={(e) => (state.text = e.target.value)} />;
}

// useSnapshot 时候会 Suspense
const state = proxy({post: fetch(url).then((res) => res.json())});

// 特殊值 - ref 不跟踪
const state = proxy({
count: 0,
// ref 不跟踪
dom: ref(document.body),
});
// Set 接口
const state = proxySet([1, 2, 3]);
// Map 接口
const state = proxyMap([
['key', 'value'],
['key2', 'value2'],
]);

// derive - 计算状态
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,
},
);

// 计算状态
import memoize from 'proxy-memoize';
import {proxyWithComputed} from 'valtio/utils';

const state = proxyWithComputed(
{
count: 1,
firstName: 'Alec',
lastName: 'Baldwin',
},
{
doubled: memoize((snap) => snap.count * 2),
// 包含 setter、getter
fullName: {
get: memoize((snap) => snap.firstName + ' ' + snap.lastName),
set: (state, newValue) => {
[state.firstName, state.lastName] = newValue.split(' ');
},
},
},
);

// state 支持 undo、redo
const state = proxyWithHistory({count: 0});

// Redux DevTool
const unsub = devtools(state, {name: 'state name', enabled: true});

// 更松散的 类型 - 默认 readtonly T
declare module 'valtio' {
function useSnapshot<T extends object>(p: T): T;
}
  • import {proxy} from 'valtio/vanilla' - 跟踪修改 写入
  • import {createProxy} from 'proxy-compare' - 跟踪使用 读取
  • snapshot - 创建不可变对象
    • 未 resolve 值会 throw error
  • useSnapshot - 封装 snapshot 结果,配合 createProxy 跟踪使用
  • 内部使用版本号跟踪变化
  • React.memo 无法处理代理对象
    • 可以使用 react-tracked 里的 memo
    • 可以在组件内使用 useSnapshot 读取传入对象
    • 可以读取原始值进行传递

Context

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

yjs

import * as Y from 'yjs';
import {proxy} from 'valtio';
import {bindProxyAndYMap} from 'valtio-yjs';

const ydoc = new Y.Doc();
const ymap = ydoc.getMap('mymap');

const state = proxy({});

bindProxyAndYMap(state, ymap);

useProxy marco

import {useProxy} from 'valtio/macro';
// useProxy 会变成 useSnapshot
// read state 的地方会变味 snapshot
const Component = () => {
useProxy(state);
return (
<div>
{state.count}
<button onClick={() => ++state.count}>+1</button>
</div>
);
};
npm i --save-dev aslemammad-vite-plugin-macro babel-plugin-macros
vite.config.js
import {defineConfig} from 'vite';
import macro from 'valtio/macro/vite';

export default defineConfig({
plugins: [macro],
});

FAQ

'set' on proxy: trap returned falsish for property 'validated'

尝试修改 snapshot 返回值

自定义比较

import eq from 'react-fast-compare';
import {typeOf} from 'react-is';
import {unstable_buildProxyFunction} from 'valtio';

const canProxyOrig = unstable_buildProxyFunction()[7];
export const proxyWithCompare = unstable_buildProxyFunction(eq, undefined, (v) => {
if (typeOf(v)) {
return false;
}
return canProxyOrig(v);
})[0];
  • unstable_buildProxyFunction
    • Object.is
    • new Proxy(target, handler)
    • canProxy
    • PROMISE_RESULT
    • PROMISE_ERROR
    • snapshotCache = new WeakMap
    • createSnapshot
    • proxyCacge = new WeakMap
    • versionHolder = [1]
    • proxyFunction

返回结果

const [
// public functions
proxyFunction,
// shared state
refSet,
VERSION,
LISTENERS,
SNAPSHOT,
// internal things
objectIs,
newProxy,
canProxy,
PROMISE_RESULT,
PROMISE_ERROR,
snapshotCache,
createSnapshot,
proxyCache,
versionHolder,
] = unstable_buildProxyFunction();

默认 canProxy 逻辑

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

const canProxy = (x: unknown) =>
isObject(x) &&
!refSet.has(x) &&
(Array.isArray(x) || !(Symbol.iterator in x)) &&
!(x instanceof WeakMap) &&
!(x instanceof WeakSet) &&
!(x instanceof Error) &&
!(x instanceof Number) &&
!(x instanceof Date) &&
!(x instanceof String) &&
!(x instanceof RegExp) &&
!(x instanceof ArrayBuffer);

TypeError: Cannot read properties of undefined (reading 'Symbol(SNAPSHOT)')

传入了 undefined 给 useSnapshot