可写Selector #

什么是可写 Selector? #

默认情况下,Selector 是只读的。但通过添加 set 函数,可以让 Selector 变为可写的,实现双向数据流。

text
┌─────────────────────────────────────────────────────┐
│                  可写 Selector                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│   ┌─────────────┐                                  │
│   │    Atom     │◀──── set ────┐                   │
│   └──────┬──────┘              │                   │
│          │                     │                   │
│          ▼ get              ┌──┴──────────┐        │
│   ┌─────────────┐           │  Writable   │        │
│   │   value     │──────────▶│  Selector   │        │
│   └─────────────┘           └──┬──────────┘        │
│                                │                   │
│                                ▼                   │
│                          ┌─────────────┐           │
│                          │  Component  │           │
│                          └─────────────┘           │
│                                                     │
└─────────────────────────────────────────────────────┘

创建可写 Selector #

基本语法 #

jsx
const writableSelector = selector({
  key: 'writableSelector',
  get: ({ get }) => {
    return computedValue;
  },
  set: ({ set, get, reset }, newValue) => {
    set(someAtom, transformedValue);
  },
});

温度转换示例 #

jsx
import { atom, selector, useRecoilState } from 'recoil';

const tempCelsiusState = atom({
  key: 'tempCelsius',
  default: 25,
});

const tempFahrenheitState = selector({
  key: 'tempFahrenheit',
  get: ({ get }) => {
    const celsius = get(tempCelsiusState);
    return celsius * 9 / 5 + 32;
  },
  set: ({ set }, fahrenheit) => {
    const celsius = (fahrenheit - 32) * 5 / 9;
    set(tempCelsiusState, celsius);
  },
});

function TemperatureConverter() {
  const [celsius, setCelsius] = useRecoilState(tempCelsiusState);
  const [fahrenheit, setFahrenheit] = useRecoilState(tempFahrenheitState);
  
  return (
    <div>
      <input
        type="number"
        value={celsius}
        onChange={(e) => setCelsius(Number(e.target.value))}
      />
      °C
      <input
        type="number"
        value={fahrenheit}
        onChange={(e) => setFahrenheit(Number(e.target.value))}
      />
      °F
    </div>
  );
}

set 函数参数 #

jsx
const mySelector = selector({
  key: 'mySelector',
  get: ({ get }) => get(someAtom),
  set: ({ set, get, reset }, newValue) => {
    
  },
});
参数 说明
set 设置 Atom 的值
get 读取其他状态的值
reset 重置 Atom 到默认值
newValue 新设置的值,或 DEFAULT_VALUE 表示重置

实战示例 #

1. 表单字段转换 #

jsx
import { atom, selector, useRecoilState } from 'recoil';
import { DefaultValue } from 'recoil';

const userState = atom({
  key: 'userState',
  default: {
    firstName: '',
    lastName: '',
  },
});

const fullNameState = selector({
  key: 'fullName',
  get: ({ get }) => {
    const user = get(userState);
    return `${user.firstName} ${user.lastName}`.trim();
  },
  set: ({ set }, newValue) => {
    if (newValue instanceof DefaultValue) {
      set(userState, { firstName: '', lastName: '' });
      return;
    }
    
    const [firstName = '', lastName = ''] = newValue.split(' ');
    set(userState, { firstName, lastName });
  },
});

function UserForm() {
  const [fullName, setFullName] = useRecoilState(fullNameState);
  const user = useRecoilValue(userState);
  
  return (
    <div>
      <input
        value={fullName}
        onChange={(e) => setFullName(e.target.value)}
        placeholder="Full Name"
      />
      <p>First: {user.firstName}</p>
      <p>Last: {user.lastName}</p>
    </div>
  );
}

2. 数字格式化 #

jsx
const amountState = atom({
  key: 'amount',
  default: 0,
});

const formattedAmountState = selector({
  key: 'formattedAmount',
  get: ({ get }) => {
    const amount = get(amountState);
    return `$${amount.toFixed(2)}`;
  },
  set: ({ set }, newValue) => {
    if (newValue instanceof DefaultValue) {
      set(amountState, 0);
      return;
    }
    
    const num = parseFloat(newValue.replace(/[^0-9.-]/g, ''));
    set(amountState, isNaN(num) ? 0 : num);
  },
});

function AmountInput() {
  const [formatted, setFormatted] = useRecoilState(formattedAmountState);
  
  return (
    <input
      value={formatted}
      onChange={(e) => setFormatted(e.target.value)}
    />
  );
}

3. 日期格式化 #

jsx
const dateState = atom({
  key: 'date',
  default: new Date(),
});

const formattedDateState = selector({
  key: 'formattedDate',
  get: ({ get }) => {
    const date = get(dateState);
    return date.toISOString().split('T')[0];
  },
  set: ({ set }, newValue) => {
    if (newValue instanceof DefaultValue) {
      set(dateState, new Date());
      return;
    }
    
    set(dateState, new Date(newValue));
  },
});

function DateInput() {
  const [dateStr, setDateStr] = useRecoilState(formattedDateState);
  
  return (
    <input
      type="date"
      value={dateStr}
      onChange={(e) => setDateStr(e.target.value)}
    />
  );
}

4. 多状态同步更新 #

jsx
const widthState = atom({
  key: 'width',
  default: 100,
});

const heightState = atom({
  key: 'height',
  default: 100,
});

const aspectRatioState = selector({
  key: 'aspectRatio',
  get: ({ get }) => {
    const width = get(widthState);
    const height = get(heightState);
    return width / height;
  },
  set: ({ set, get }, newRatio) => {
    if (newRatio instanceof DefaultValue) {
      set(widthState, 100);
      set(heightState, 100);
      return;
    }
    
    const height = get(heightState);
    set(widthState, height * newRatio);
  },
});

function DimensionControls() {
  const [width, setWidth] = useRecoilState(widthState);
  const [height, setHeight] = useRecoilState(heightState);
  const [ratio, setRatio] = useRecoilState(aspectRatioState);
  
  return (
    <div>
      <label>
        Width:
        <input
          type="number"
          value={width}
          onChange={(e) => setWidth(Number(e.target.value))}
        />
      </label>
      <label>
        Height:
        <input
          type="number"
          value={height}
          onChange={(e) => setHeight(Number(e.target.value))}
        />
      </label>
      <label>
        Ratio:
        <input
          type="number"
          step="0.1"
          value={ratio.toFixed(2)}
          onChange={(e) => setRatio(Number(e.target.value))}
        />
      </label>
    </div>
  );
}

5. 列表过滤与搜索 #

jsx
const searchQueryState = atom({
  key: 'searchQuery',
  default: '',
});

const selectedCategoryState = atom({
  key: 'selectedCategory',
  default: 'all',
});

const filterState = selector({
  key: 'filter',
  get: ({ get }) => ({
    query: get(searchQueryState),
    category: get(selectedCategoryState),
  }),
  set: ({ set }, newValue) => {
    if (newValue instanceof DefaultValue) {
      set(searchQueryState, '');
      set(selectedCategoryState, 'all');
      return;
    }
    
    set(searchQueryState, newValue.query || '');
    set(selectedCategoryState, newValue.category || 'all');
  },
});

function FilterPanel() {
  const [filter, setFilter] = useRecoilState(filterState);
  
  const handleQueryChange = (e) => {
    setFilter({ ...filter, query: e.target.value });
  };
  
  const handleCategoryChange = (e) => {
    setFilter({ ...filter, category: e.target.value });
  };
  
  const clearFilter = () => {
    setFilter(DEFAULT_VALUE);
  };
  
  return (
    <div>
      <input
        value={filter.query}
        onChange={handleQueryChange}
        placeholder="Search..."
      />
      <select value={filter.category} onChange={handleCategoryChange}>
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      <button onClick={clearFilter}>Clear</button>
    </div>
  );
}

处理重置 #

使用 DefaultValue 处理重置操作:

jsx
import { DefaultValue } from 'recoil';

const mySelector = selector({
  key: 'mySelector',
  get: ({ get }) => get(someAtom),
  set: ({ set, reset }, newValue) => {
    if (newValue instanceof DefaultValue) {
      reset(someAtom);
      return;
    }
    
    set(someAtom, transform(newValue));
  },
});

使用 reset 函数 #

jsx
const mySelector = selector({
  key: 'mySelector',
  get: ({ get }) => get(someAtom),
  set: ({ set, reset }, newValue) => {
    if (newValue instanceof DefaultValue) {
      reset(someAtom);
    } else {
      set(someAtom, newValue);
    }
  },
});

TypeScript 支持 #

tsx
import { selector, useRecoilState, DefaultValue } from 'recoil';

const formattedSelector = selector<string>({
  key: 'formattedSelector',
  get: ({ get }) => {
    const value = get(numberState);
    return value.toString();
  },
  set: ({ set }, newValue) => {
    if (newValue instanceof DefaultValue) {
      set(numberState, 0);
      return;
    }
    
    set(numberState, parseInt(newValue, 10) || 0);
  },
});

完整示例:设置面板 #

jsx
import { atom, selector, useRecoilState, useResetRecoilState } from 'recoil';
import { DefaultValue } from 'recoil';

const themeState = atom({
  key: 'theme',
  default: 'light',
});

const fontSizeState = atom({
  key: 'fontSize',
  default: 16,
});

const compactModeState = atom({
  key: 'compactMode',
  default: false,
});

const settingsState = selector({
  key: 'settings',
  get: ({ get }) => ({
    theme: get(themeState),
    fontSize: get(fontSizeState),
    compactMode: get(compactModeState),
  }),
  set: ({ set, reset }, newValue) => {
    if (newValue instanceof DefaultValue) {
      reset(themeState);
      reset(fontSizeState);
      reset(compactModeState);
      return;
    }
    
    if (newValue.theme !== undefined) set(themeState, newValue.theme);
    if (newValue.fontSize !== undefined) set(fontSizeState, newValue.fontSize);
    if (newValue.compactMode !== undefined) set(compactModeState, newValue.compactMode);
  },
});

function SettingsPanel() {
  const [settings, setSettings] = useRecoilState(settingsState);
  const resetSettings = useResetRecoilState(settingsState);
  
  return (
    <div>
      <h2>Settings</h2>
      
      <label>
        Theme:
        <select
          value={settings.theme}
          onChange={(e) => setSettings({ ...settings, theme: e.target.value })}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
          <option value="auto">Auto</option>
        </select>
      </label>
      
      <label>
        Font Size: {settings.fontSize}px
        <input
          type="range"
          min="12"
          max="24"
          value={settings.fontSize}
          onChange={(e) => setSettings({ ...settings, fontSize: Number(e.target.value) })}
        />
      </label>
      
      <label>
        <input
          type="checkbox"
          checked={settings.compactMode}
          onChange={(e) => setSettings({ ...settings, compactMode: e.target.checked })}
        />
        Compact Mode
      </label>
      
      <button onClick={resetSettings}>Reset to Default</button>
    </div>
  );
}

总结 #

可写 Selector 的核心用途:

用途 说明
数据转换 格式化显示和解析输入
双向绑定 实现双向数据流
多状态同步 同时更新多个 Atom
数据验证 在设置前验证数据

下一步,让我们学习 Selector家族,了解参数化的 Selector。

最后更新:2026-03-28