状态快照 #

什么是状态快照? #

状态快照(Snapshot)是 Recoil 在某一时刻的状态冻结副本。通过快照,我们可以:

  • 检查当前状态
  • 实现时间旅行调试
  • 状态回滚和恢复
  • 状态对比和差异检测

获取快照 #

useRecoilSnapshot #

jsx
import { useRecoilSnapshot } from 'recoil';

function DebugObserver() {
  const snapshot = useRecoilSnapshot();
  
  useEffect(() => {
    console.log('Current snapshot:', snapshot);
    
    for (const node of snapshot.getNodes_UNSTABLE()) {
      const loadable = snapshot.getLoadable(node);
      console.log(node.key, loadable.contents);
    }
  }, [snapshot]);
  
  return null;
}

useRecoilCallback #

jsx
import { useRecoilCallback } from 'recoil';

function MyComponent() {
  const getSnapshot = useRecoilCallback(({ snapshot }) => () => {
    return snapshot;
  });
  
  const handleClick = () => {
    const snapshot = getSnapshot();
    console.log('Snapshot:', snapshot);
  };
  
  return <button onClick={handleClick}>Get Snapshot</button>;
}

Snapshot API #

getLoadable #

同步获取状态值:

jsx
const loadable = snapshot.getLoadable(state);

if (loadable.state === 'hasValue') {
  console.log('Value:', loadable.contents);
}

getPromise #

异步获取状态值:

jsx
const value = await snapshot.getPromise(state);
console.log('Value:', value);

getNodes #

获取所有状态节点:

jsx
const nodes = snapshot.getNodes_UNSTABLE();

for (const node of nodes) {
  console.log(node.key);
}

getNodes_UNSTABLE #

带过滤条件获取节点:

jsx
const modifiedNodes = snapshot.getNodes_UNSTABLE({ isModified: true });

for (const node of modifiedNodes) {
  console.log('Modified:', node.key);
}

状态调试 #

调试组件 #

jsx
import { useRecoilSnapshot } from 'recoil';

function DebugPanel() {
  const snapshot = useRecoilSnapshot();
  
  const nodes = Array.from(snapshot.getNodes_UNSTABLE());
  
  return (
    <div className="debug-panel">
      <h3>State Debug</h3>
      <ul>
        {nodes.map(node => {
          const loadable = snapshot.getLoadable(node);
          return (
            <li key={node.key}>
              <strong>{node.key}:</strong>
              <pre>{JSON.stringify(loadable.contents, null, 2)}</pre>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

变化追踪 #

jsx
function StateChangeLogger() {
  const snapshot = useRecoilSnapshot();
  
  useEffect(() => {
    const modifiedNodes = snapshot.getNodes_UNSTABLE({ isModified: true });
    
    for (const node of modifiedNodes) {
      const loadable = snapshot.getLoadable(node);
      console.log(`[${node.key}] changed:`, loadable.contents);
    }
  }, [snapshot]);
  
  return null;
}

function App() {
  return (
    <RecoilRoot>
      <StateChangeLogger />
      <YourApp />
    </RecoilRoot>
  );
}

时间旅行 #

实现历史记录 #

jsx
import { atom, useRecoilSnapshot, useRecoilCallback } from 'recoil';

const historyState = atom({
  key: 'history',
  default: [],
  dangerouslyAllowMutability: true,
});

const historyIndexState = atom({
  key: 'historyIndex',
  default: -1,
});

function useHistory() {
  const snapshot = useRecoilSnapshot();
  
  const saveSnapshot = useRecoilCallback(({ set }) => () => {
    const state = {};
    
    for (const node of snapshot.getNodes_UNSTABLE()) {
      const loadable = snapshot.getLoadable(node);
      if (loadable.state === 'hasValue') {
        state[node.key] = loadable.contents;
      }
    }
    
    set(historyState, prev => [...prev, state]);
    set(historyIndexState, prev => prev + 1);
  });
  
  const undo = useRecoilCallback(({ set, snapshot }) => async () => {
    const history = await snapshot.getPromise(historyState);
    const index = await snapshot.getPromise(historyIndexState);
    
    if (index > 0) {
      const prevState = history[index - 1];
      
      for (const [key, value] of Object.entries(prevState)) {
        set({ key }, value);
      }
      
      set(historyIndexState, index - 1);
    }
  });
  
  const redo = useRecoilCallback(({ set, snapshot }) => async () => {
    const history = await snapshot.getPromise(historyState);
    const index = await snapshot.getPromise(historyIndexState);
    
    if (index < history.length - 1) {
      const nextState = history[index + 1];
      
      for (const [key, value] of Object.entries(nextState)) {
        set({ key }, value);
      }
      
      set(historyIndexState, index + 1);
    }
  });
  
  return { saveSnapshot, undo, redo };
}

gotoSnapshot #

恢复到指定快照:

jsx
function TimeTravel() {
  const [snapshots, setSnapshots] = useState([]);
  const snapshot = useRecoilSnapshot();
  
  const saveSnapshot = useRecoilCallback(({ snapshot }) => () => {
    setSnapshots(prev => [...prev, snapshot]);
  });
  
  const gotoSnapshot = useRecoilCallback(({ gotoSnapshot }) => (snapshot) => {
    gotoSnapshot(snapshot);
  });
  
  return (
    <div>
      <button onClick={saveSnapshot}>Save Snapshot</button>
      
      <div>
        {snapshots.map((snap, index) => (
          <button key={index} onClick={() => gotoSnapshot(snap)}>
            Restore #{index + 1}
          </button>
        ))}
      </div>
    </div>
  );
}

状态检查 #

验证状态完整性 #

jsx
function validateSnapshot(snapshot) {
  const nodes = snapshot.getNodes_UNSTABLE();
  const errors = [];
  
  for (const node of nodes) {
    const loadable = snapshot.getLoadable(node);
    
    if (loadable.state === 'hasError') {
      errors.push({
        key: node.key,
        error: loadable.contents,
      });
    }
  }
  
  return errors;
}

function StateValidator() {
  const snapshot = useRecoilSnapshot();
  
  useEffect(() => {
    const errors = validateSnapshot(snapshot);
    
    if (errors.length > 0) {
      console.error('State validation errors:', errors);
    }
  }, [snapshot]);
  
  return null;
}

状态差异对比 #

jsx
function diffSnapshots(snapshot1, snapshot2) {
  const diff = {};
  const nodes = snapshot1.getNodes_UNSTABLE();
  
  for (const node of nodes) {
    const value1 = snapshot1.getLoadable(node).contents;
    const value2 = snapshot2.getLoadable(node).contents;
    
    if (JSON.stringify(value1) !== JSON.stringify(value2)) {
      diff[node.key] = {
        from: value1,
        to: value2,
      };
    }
  }
  
  return diff;
}

实战示例:完整的调试工具 #

jsx
import { useState } from 'react';
import { useRecoilSnapshot, useRecoilCallback } from 'recoil';

function RecoilDebugger() {
  const [isOpen, setIsOpen] = useState(false);
  const [history, setHistory] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(-1);
  
  const snapshot = useRecoilSnapshot();
  
  const captureSnapshot = () => {
    const state = {};
    
    for (const node of snapshot.getNodes_UNSTABLE()) {
      const loadable = snapshot.getLoadable(node);
      if (loadable.state === 'hasValue') {
        state[node.key] = {
          value: loadable.contents,
          timestamp: Date.now(),
        };
      }
    }
    
    setHistory(prev => [...prev.slice(0, currentIndex + 1), state]);
    setCurrentIndex(prev => prev + 1);
  };
  
  const restoreSnapshot = useRecoilCallback(({ set }) => (state) => {
    for (const [key, data] of Object.entries(state)) {
      set({ key }, data.value);
    }
  });
  
  const handleUndo = () => {
    if (currentIndex > 0) {
      const prevState = history[currentIndex - 1];
      restoreSnapshot(prevState);
      setCurrentIndex(prev => prev - 1);
    }
  };
  
  const handleRedo = () => {
    if (currentIndex < history.length - 1) {
      const nextState = history[currentIndex + 1];
      restoreSnapshot(nextState);
      setCurrentIndex(prev => prev + 1);
    }
  };
  
  if (!isOpen) {
    return (
      <button
        style={{ position: 'fixed', bottom: 20, right: 20 }}
        onClick={() => setIsOpen(true)}
      >
        Debug
      </button>
    );
  }
  
  const currentSnapshot = history[currentIndex];
  const nodes = currentSnapshot ? Object.entries(currentSnapshot) : [];
  
  return (
    <div
      style={{
        position: 'fixed',
        bottom: 20,
        right: 20,
        width: 400,
        maxHeight: 500,
        background: '#1e1e1e',
        color: '#fff',
        borderRadius: 8,
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          padding: 10,
          background: '#333',
          display: 'flex',
          justifyContent: 'space-between',
        }}
      >
        <span>Recoil Debugger</span>
        <button onClick={() => setIsOpen(false)}>×</button>
      </div>
      
      <div style={{ padding: 10 }}>
        <div style={{ marginBottom: 10 }}>
          <button onClick={captureSnapshot}>Capture</button>
          <button onClick={handleUndo} disabled={currentIndex <= 0}>
            Undo
          </button>
          <button onClick={handleRedo} disabled={currentIndex >= history.length - 1}>
            Redo
          </button>
        </div>
        
        <div style={{ maxHeight: 300, overflow: 'auto' }}>
          {nodes.map(([key, data]) => (
            <div key={key} style={{ marginBottom: 10 }}>
              <strong style={{ color: '#4ec9b0' }}>{key}</strong>
              <pre style={{ margin: 0, fontSize: 12 }}>
                {JSON.stringify(data.value, null, 2)}
              </pre>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function App() {
  return (
    <RecoilRoot>
      <YourApp />
      <RecoilDebugger />
    </RecoilRoot>
  );
}

总结 #

状态快照的核心要点:

功能 API
获取快照 useRecoilSnapshot
读取状态 snapshot.getLoadable
异步读取 snapshot.getPromise
获取节点 snapshot.getNodes_UNSTABLE
恢复快照 gotoSnapshot

下一步,让我们学习 性能优化,了解 Recoil 的性能优化技巧。

最后更新:2026-03-28