可写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