Appearance
Recoil选择器
概述
Selector是Recoil中用于计算派生状态的核心概念。它们允许你基于atoms或其他selectors计算新的状态,支持异步操作,并且会自动缓存结果以提高性能。本文深入探讨Recoil selectors的各种用法和高级特性。
Selector基础
基础概念
Selector是纯函数,用于根据依赖的atoms或其他selectors计算派生状态:
jsx
import { atom, selector, useRecoilValue } from 'recoil';
// 基础atoms
const temperatureAtom = atom({
key: 'temperature',
default: 0
});
const unitAtom = atom({
key: 'unit',
default: 'celsius' // 'celsius' | 'fahrenheit'
});
// 派生selector
const displayTemperatureSelector = selector({
key: 'displayTemperature',
get: ({get}) => {
const temp = get(temperatureAtom);
const unit = get(unitAtom);
if (unit === 'fahrenheit') {
return (temp * 9/5) + 32;
}
return temp;
}
});
// 使用
function TemperatureDisplay() {
const displayTemp = useRecoilValue(displayTemperatureSelector);
const unit = useRecoilValue(unitAtom);
return (
<div>
Temperature: {displayTemp.toFixed(1)}°{unit === 'celsius' ? 'C' : 'F'}
</div>
);
}只读Selector
jsx
const todosAtom = atom({
key: 'todos',
default: []
});
const filterAtom = atom({
key: 'todoFilter',
default: 'all'
});
// 过滤todos的selector
const filteredTodosSelector = selector({
key: 'filteredTodos',
get: ({get}) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
});
// 统计selector
const todoStatsSelector = selector({
key: 'todoStats',
get: ({get}) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
completionRate: todos.length > 0
? (todos.filter(t => t.completed).length / todos.length) * 100
: 0
};
}
});
// 使用
function TodoStats() {
const stats = useRecoilValue(todoStatsSelector);
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Active: {stats.active}</p>
<p>Completion Rate: {stats.completionRate.toFixed(1)}%</p>
</div>
);
}可写Selector
jsx
// 可读可写的selector
const fahrenheitSelector = selector({
key: 'fahrenheit',
get: ({get}) => {
const celsius = get(temperatureAtom);
return (celsius * 9/5) + 32;
},
set: ({set}, newValue) => {
const celsius = (newValue - 32) * 5/9;
set(temperatureAtom, celsius);
}
});
// 使用可写selector
function TemperatureControl() {
const [celsius, setCelsius] = useRecoilState(temperatureAtom);
const [fahrenheit, setFahrenheit] = useRecoilState(fahrenheitSelector);
return (
<div>
<div>
<label>Celsius:</label>
<input
type="number"
value={celsius}
onChange={(e) => setCelsius(Number(e.target.value))}
/>
</div>
<div>
<label>Fahrenheit:</label>
<input
type="number"
value={fahrenheit}
onChange={(e) => setFahrenheit(Number(e.target.value))}
/>
</div>
</div>
);
}
// 复杂可写selector
const userFullNameSelector = selector({
key: 'userFullName',
get: ({get}) => {
const user = get(userAtom);
return user ? `${user.firstName} ${user.lastName}` : '';
},
set: ({set}, newValue) => {
const [firstName, ...lastNameParts] = newValue.split(' ');
const lastName = lastNameParts.join(' ');
set(userAtom, (prevUser) => ({
...prevUser,
firstName: firstName || '',
lastName: lastName || ''
}));
}
});异步Selector
基础异步Selector
jsx
const userIdAtom = atom({
key: 'userId',
default: 1
});
// 异步获取用户数据
const userSelector = selector({
key: 'user',
get: async ({get}) => {
const userId = get(userIdAtom);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
}
});
// 使用Suspense
function UserProfile() {
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserProfileContent />
</Suspense>
);
}
function UserProfileContent() {
const user = useRecoilValue(userSelector);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// 错误边界
import { ErrorBoundary } from 'react-error-boundary';
function UserWithErrorBoundary() {
return (
<ErrorBoundary
fallback={<div>Something went wrong loading user data</div>}
>
<Suspense fallback={<div>Loading...</div>}>
<UserProfileContent />
</Suspense>
</ErrorBoundary>
);
}依赖链式异步Selector
jsx
// 用户atom
const currentUserIdAtom = atom({
key: 'currentUserId',
default: 1
});
// 获取用户信息
const currentUserSelector = selector({
key: 'currentUser',
get: async ({get}) => {
const userId = get(currentUserIdAtom);
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
});
// 基于用户获取其帖子
const userPostsSelector = selector({
key: 'userPosts',
get: async ({get}) => {
const user = await get(currentUserSelector);
const response = await fetch(`/api/users/${user.id}/posts`);
return response.json();
}
});
// 基于帖子获取评论
const postsWithCommentsSelector = selector({
key: 'postsWithComments',
get: async ({get}) => {
const posts = await get(userPostsSelector);
const postsWithComments = await Promise.all(
posts.map(async (post) => {
const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
const comments = await commentsResponse.json();
return { ...post, comments };
})
);
return postsWithComments;
}
});
// 使用
function UserDashboard() {
return (
<Suspense fallback={<div>Loading dashboard...</div>}>
<UserInfo />
<UserPosts />
</Suspense>
);
}
function UserInfo() {
const user = useRecoilValue(currentUserSelector);
return <h1>Welcome, {user.name}!</h1>;
}
function UserPosts() {
const posts = useRecoilValue(postsWithCommentsSelector);
return (
<div>
<h2>Your Posts</h2>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.comments.length} comments</p>
</div>
))}
</div>
);
}并发异步Selector
jsx
// 并行获取多个数据源
const dashboardDataSelector = selector({
key: 'dashboardData',
get: async ({get}) => {
const userId = get(currentUserIdAtom);
// 并行请求
const [userProfile, userPosts, userAnalytics] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json()),
fetch(`/api/users/${userId}/analytics`).then(r => r.json())
]);
return {
profile: userProfile,
posts: userPosts,
analytics: userAnalytics,
lastUpdated: new Date().toISOString()
};
}
});
// 条件异步Selector
const conditionalDataSelector = selector({
key: 'conditionalData',
get: async ({get}) => {
const user = await get(currentUserSelector);
// 根据用户角色获取不同数据
if (user.role === 'admin') {
const response = await fetch('/api/admin/dashboard');
return response.json();
} else {
const response = await fetch(`/api/users/${user.id}/dashboard`);
return response.json();
}
}
});Selector Family
Selector Family用于创建参数化的selectors:
jsx
import { selectorFamily } from 'recoil';
// 基础Selector Family
const userSelectorFamily = selectorFamily({
key: 'userById',
get: (userId) => async ({get}) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
});
// 使用
function UserProfile({ userId }) {
const user = useRecoilValue(userSelectorFamily(userId));
return <div>{user.name}</div>;
}
// 带缓存的Selector Family
const cachedUserFamily = selectorFamily({
key: 'cachedUser',
get: (userId) => async ({get}) => {
// 检查缓存
const cache = get(userCacheAtom);
const cached = cache[userId];
if (cached && Date.now() - cached.timestamp < 300000) { // 5分钟缓存
return cached.data;
}
// 从服务器获取
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
// 更新缓存
set(userCacheAtom, prev => ({
...prev,
[userId]: {
data: userData,
timestamp: Date.now()
}
}));
return userData;
}
});
// 可写Selector Family
const userFieldFamily = selectorFamily({
key: 'userField',
get: ({userId, field}) => ({get}) => {
const user = get(userAtomFamily(userId));
return user[field];
},
set: ({userId, field}) => ({set}, newValue) => {
set(userAtomFamily(userId), (prevUser) => ({
...prevUser,
[field]: newValue,
updatedAt: new Date().toISOString()
}));
}
});
// 使用可写Selector Family
function UserField({ userId, field, label }) {
const [value, setValue] = useRecoilState(
userFieldFamily({userId, field})
);
return (
<div>
<label>{label}:</label>
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
/>
</div>
);
}
// 复杂参数Selector Family
const searchResultsFamily = selectorFamily({
key: 'searchResults',
get: ({query, filters, page}) => async ({get}) => {
if (!query) return { results: [], totalCount: 0 };
const params = new URLSearchParams({
q: query,
page: page.toString(),
...filters
});
const response = await fetch(`/api/search?${params}`);
return response.json();
}
});实战案例
案例1:数据分析Dashboard
jsx
import { atom, selector, selectorFamily } from 'recoil';
// 基础数据atoms
const rawDataAtom = atom({
key: 'rawData',
default: []
});
const dateRangeAtom = atom({
key: 'dateRange',
default: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7天前
end: new Date()
}
});
const metricsConfigAtom = atom({
key: 'metricsConfig',
default: {
groupBy: 'day', // 'day' | 'week' | 'month'
metrics: ['views', 'clicks', 'conversions']
}
});
// 过滤数据selector
const filteredDataSelector = selector({
key: 'filteredData',
get: ({get}) => {
const rawData = get(rawDataAtom);
const dateRange = get(dateRangeAtom);
return rawData.filter(item => {
const itemDate = new Date(item.timestamp);
return itemDate >= dateRange.start && itemDate <= dateRange.end;
});
}
});
// 分组数据selector
const groupedDataSelector = selector({
key: 'groupedData',
get: ({get}) => {
const data = get(filteredDataSelector);
const config = get(metricsConfigAtom);
const groups = {};
data.forEach(item => {
const date = new Date(item.timestamp);
let groupKey;
switch (config.groupBy) {
case 'week':
// 获取周的开始日期
const startOfWeek = new Date(date);
startOfWeek.setDate(date.getDate() - date.getDay());
groupKey = startOfWeek.toISOString().split('T')[0];
break;
case 'month':
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
break;
default: // day
groupKey = date.toISOString().split('T')[0];
}
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(item);
});
return groups;
}
});
// 聚合指标selector
const metricsSelector = selector({
key: 'metrics',
get: ({get}) => {
const groupedData = get(groupedDataSelector);
const config = get(metricsConfigAtom);
const metrics = {};
Object.entries(groupedData).forEach(([date, items]) => {
metrics[date] = {};
config.metrics.forEach(metric => {
switch (metric) {
case 'views':
metrics[date].views = items.reduce((sum, item) => sum + item.views, 0);
break;
case 'clicks':
metrics[date].clicks = items.reduce((sum, item) => sum + item.clicks, 0);
break;
case 'conversions':
metrics[date].conversions = items.reduce((sum, item) => sum + item.conversions, 0);
break;
}
});
// 计算派生指标
if (metrics[date].views && metrics[date].clicks) {
metrics[date].ctr = (metrics[date].clicks / metrics[date].views) * 100;
}
if (metrics[date].clicks && metrics[date].conversions) {
metrics[date].conversionRate = (metrics[date].conversions / metrics[date].clicks) * 100;
}
});
return metrics;
}
});
// 图表数据selector
const chartDataSelector = selector({
key: 'chartData',
get: ({get}) => {
const metrics = get(metricsSelector);
const config = get(metricsConfigAtom);
const dates = Object.keys(metrics).sort();
const datasets = {};
config.metrics.forEach(metric => {
datasets[metric] = dates.map(date => ({
x: date,
y: metrics[date][metric] || 0
}));
});
return {
dates,
datasets
};
}
});
// 摘要统计selector
const summaryStatsSelector = selector({
key: 'summaryStats',
get: ({get}) => {
const metrics = get(metricsSelector);
const dates = Object.keys(metrics);
if (dates.length === 0) {
return {
totalViews: 0,
totalClicks: 0,
totalConversions: 0,
averageCTR: 0,
averageConversionRate: 0
};
}
const totals = dates.reduce((acc, date) => {
const dayMetrics = metrics[date];
return {
views: acc.views + (dayMetrics.views || 0),
clicks: acc.clicks + (dayMetrics.clicks || 0),
conversions: acc.conversions + (dayMetrics.conversions || 0)
};
}, { views: 0, clicks: 0, conversions: 0 });
return {
totalViews: totals.views,
totalClicks: totals.clicks,
totalConversions: totals.conversions,
averageCTR: totals.views > 0 ? (totals.clicks / totals.views) * 100 : 0,
averageConversionRate: totals.clicks > 0 ? (totals.conversions / totals.clicks) * 100 : 0
};
}
});
// Dashboard组件
function AnalyticsDashboard() {
const [dateRange, setDateRange] = useRecoilState(dateRangeAtom);
const [config, setConfig] = useRecoilState(metricsConfigAtom);
const summaryStats = useRecoilValue(summaryStatsSelector);
const chartData = useRecoilValue(chartDataSelector);
return (
<div className="analytics-dashboard">
<div className="controls">
<DateRangePicker
value={dateRange}
onChange={setDateRange}
/>
<select
value={config.groupBy}
onChange={(e) => setConfig(prev => ({ ...prev, groupBy: e.target.value }))}
>
<option value="day">Daily</option>
<option value="week">Weekly</option>
<option value="month">Monthly</option>
</select>
</div>
<div className="summary-stats">
<div className="stat-card">
<h3>Total Views</h3>
<p>{summaryStats.totalViews.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Total Clicks</h3>
<p>{summaryStats.totalClicks.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Average CTR</h3>
<p>{summaryStats.averageCTR.toFixed(2)}%</p>
</div>
<div className="stat-card">
<h3>Average Conversion Rate</h3>
<p>{summaryStats.averageConversionRate.toFixed(2)}%</p>
</div>
</div>
<div className="charts">
<MetricsChart data={chartData} />
</div>
</div>
);
}案例2:实时搜索系统
jsx
import { atom, selector, selectorFamily, atomFamily } from 'recoil';
// 搜索查询atom
const searchQueryAtom = atom({
key: 'searchQuery',
default: ''
});
// 搜索过滤器atom
const searchFiltersAtom = atom({
key: 'searchFilters',
default: {
category: '',
priceRange: [0, 1000],
inStock: false,
rating: 0
}
});
// 搜索结果缓存atom family
const searchCacheFamily = atomFamily({
key: 'searchCache',
default: null
});
// 防抖搜索selector
const debouncedSearchQuerySelector = selector({
key: 'debouncedSearchQuery',
get: ({get}) => {
const query = get(searchQueryAtom);
// 这里可以配合useRecoilValueLoadable实现防抖
return query;
}
});
// 搜索结果selector family
const searchResultsFamily = selectorFamily({
key: 'searchResults',
get: ({query, filters}) => async ({get}) => {
if (!query.trim()) {
return { results: [], totalCount: 0, facets: {} };
}
// 构建缓存key
const cacheKey = JSON.stringify({ query, filters });
// 检查缓存
const cached = get(searchCacheFamily(cacheKey));
if (cached && Date.now() - cached.timestamp < 60000) { // 1分钟缓存
return cached.data;
}
// 构建查询参数
const params = new URLSearchParams({
q: query,
category: filters.category,
minPrice: filters.priceRange[0],
maxPrice: filters.priceRange[1],
inStock: filters.inStock,
minRating: filters.rating
});
const response = await fetch(`/api/search?${params}`);
const results = await response.json();
// 缓存结果
set(searchCacheFamily(cacheKey), {
data: results,
timestamp: Date.now()
});
return results;
}
});
// 搜索建议selector
const searchSuggestionsSelector = selector({
key: 'searchSuggestions',
get: async ({get}) => {
const query = get(searchQueryAtom);
if (query.length < 2) {
return [];
}
const response = await fetch(`/api/search/suggestions?q=${encodeURIComponent(query)}`);
return response.json();
}
});
// 热门搜索selector
const popularSearchesSelector = selector({
key: 'popularSearches',
get: async () => {
const response = await fetch('/api/search/popular');
return response.json();
}
});
// 搜索历史atom(持久化)
const searchHistoryAtom = atom({
key: 'searchHistory',
default: [],
effects: [
({setSelf, onSet}) => {
// 从localStorage加载
const saved = localStorage.getItem('searchHistory');
if (saved) {
setSelf(JSON.parse(saved));
}
// 保存到localStorage
onSet(newValue => {
localStorage.setItem('searchHistory', JSON.stringify(newValue));
});
}
]
});
// 组件
function SearchInterface() {
const [query, setQuery] = useRecoilState(searchQueryAtom);
const [filters, setFilters] = useRecoilState(searchFiltersAtom);
const [searchHistory, setSearchHistory] = useRecoilState(searchHistoryAtom);
const handleSearch = (searchQuery) => {
setQuery(searchQuery);
// 添加到搜索历史
if (searchQuery.trim()) {
setSearchHistory(prev => {
const filtered = prev.filter(item => item !== searchQuery);
return [searchQuery, ...filtered].slice(0, 10); // 保留最近10个
});
}
};
return (
<div className="search-interface">
<SearchBox
query={query}
onSearch={handleSearch}
/>
<SearchFilters
filters={filters}
onChange={setFilters}
/>
<Suspense fallback={<div>Searching...</div>}>
<SearchResults query={query} filters={filters} />
</Suspense>
<SearchSidebar history={searchHistory} />
</div>
);
}
function SearchResults({ query, filters }) {
const results = useRecoilValue(searchResultsFamily({query, filters}));
return (
<div className="search-results">
<div className="results-header">
<h2>{results.totalCount} results for "{query}"</h2>
</div>
<div className="results-list">
{results.results.map(item => (
<SearchResultItem key={item.id} item={item} />
))}
</div>
<SearchFacets facets={results.facets} />
</div>
);
}
function SearchBox({ query, onSearch }) {
const suggestions = useRecoilValueLoadable(searchSuggestionsSelector);
const popularSearches = useRecoilValue(popularSearchesSelector);
const [showSuggestions, setShowSuggestions] = useState(false);
return (
<div className="search-box">
<input
type="text"
value={query}
onChange={(e) => onSearch(e.target.value)}
onFocus={() => setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
placeholder="Search products..."
/>
{showSuggestions && (
<div className="suggestions-dropdown">
{suggestions.state === 'hasValue' && suggestions.contents.length > 0 && (
<div className="suggestions-section">
<h4>Suggestions</h4>
{suggestions.contents.map((suggestion, index) => (
<div
key={index}
className="suggestion"
onClick={() => onSearch(suggestion)}
>
{suggestion}
</div>
))}
</div>
)}
{popularSearches.length > 0 && (
<div className="popular-section">
<h4>Popular Searches</h4>
{popularSearches.map((search, index) => (
<div
key={index}
className="popular-search"
onClick={() => onSearch(search)}
>
{search}
</div>
))}
</div>
)}
</div>
)}
</div>
);
}案例3:表单验证系统
jsx
// 表单字段atoms
const formFieldsAtom = atom({
key: 'formFields',
default: {
firstName: '',
lastName: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
}
});
// 单个字段selector family
const fieldSelectorFamily = selectorFamily({
key: 'formField',
get: (fieldName) => ({get}) => {
const fields = get(formFieldsAtom);
return fields[fieldName];
},
set: (fieldName) => ({set}, newValue) => {
set(formFieldsAtom, prev => ({
...prev,
[fieldName]: newValue
}));
}
});
// 字段验证selector family
const fieldValidationFamily = selectorFamily({
key: 'fieldValidation',
get: (fieldName) => ({get}) => {
const value = get(fieldSelectorFamily(fieldName));
const allFields = get(formFieldsAtom);
switch (fieldName) {
case 'firstName':
case 'lastName':
if (!value) return 'This field is required';
if (value.length < 2) return 'Must be at least 2 characters';
break;
case 'email':
if (!value) return 'Email is required';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) return 'Invalid email format';
break;
case 'phone':
if (!value) return 'Phone is required';
const phoneRegex = /^\(\d{3}\) \d{3}-\d{4}$/;
if (!phoneRegex.test(value)) return 'Format: (123) 456-7890';
break;
case 'password':
if (!value) return 'Password is required';
if (value.length < 8) return 'Must be at least 8 characters';
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return 'Must contain uppercase, lowercase and number';
}
break;
case 'confirmPassword':
if (!value) return 'Please confirm password';
if (value !== allFields.password) return 'Passwords do not match';
break;
}
return null;
}
});
// 表单有效性selector
const formValidationSelector = selector({
key: 'formValidation',
get: ({get}) => {
const fields = get(formFieldsAtom);
const fieldNames = Object.keys(fields);
const errors = {};
let isValid = true;
fieldNames.forEach(fieldName => {
const error = get(fieldValidationFamily(fieldName));
if (error) {
errors[fieldName] = error;
isValid = false;
}
});
return {
isValid,
errors,
completedFields: fieldNames.filter(name => fields[name]).length,
totalFields: fieldNames.length
};
}
});
// 表单进度selector
const formProgressSelector = selector({
key: 'formProgress',
get: ({get}) => {
const validation = get(formValidationSelector);
return (validation.completedFields / validation.totalFields) * 100;
}
});
// 组件
function ValidationForm() {
const validation = useRecoilValue(formValidationSelector);
const progress = useRecoilValue(formProgressSelector);
const handleSubmit = (e) => {
e.preventDefault();
if (validation.isValid) {
console.log('Form is valid, submitting...');
} else {
console.log('Form has errors:', validation.errors);
}
};
return (
<form onSubmit={handleSubmit}>
<div className="form-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
</div>
<p>{progress.toFixed(0)}% completed</p>
</div>
<FormField name="firstName" label="First Name" />
<FormField name="lastName" label="Last Name" />
<FormField name="email" label="Email" type="email" />
<FormField name="phone" label="Phone" placeholder="(123) 456-7890" />
<FormField name="password" label="Password" type="password" />
<FormField name="confirmPassword" label="Confirm Password" type="password" />
<button type="submit" disabled={!validation.isValid}>
Submit
</button>
{Object.keys(validation.errors).length > 0 && (
<div className="form-errors">
<h4>Please fix the following errors:</h4>
<ul>
{Object.entries(validation.errors).map(([field, error]) => (
<li key={field}>{field}: {error}</li>
))}
</ul>
</div>
)}
</form>
);
}
function FormField({ name, label, type = 'text', ...props }) {
const [value, setValue] = useRecoilState(fieldSelectorFamily(name));
const error = useRecoilValue(fieldValidationFamily(name));
return (
<div className="form-field">
<label>{label}</label>
<input
type={type}
value={value}
onChange={(e) => setValue(e.target.value)}
className={error ? 'error' : ''}
{...props}
/>
{error && <div className="field-error">{error}</div>}
</div>
);
}性能优化
1. 选择器缓存
jsx
// 昂贵计算selector
const expensiveComputationSelector = selector({
key: 'expensiveComputation',
get: ({get}) => {
const data = get(dataAtom);
// 只在data变化时重新计算
return expensiveFunction(data);
}
});2. 错误边界
jsx
function SearchWithErrorBoundary() {
return (
<ErrorBoundary fallback={<SearchError />}>
<Suspense fallback={<SearchLoading />}>
<SearchResults />
</Suspense>
</ErrorBoundary>
);
}3. 条件渲染
jsx
// 避免不必要的异步selector调用
function ConditionalData() {
const shouldFetch = useRecoilValue(shouldFetchAtom);
return (
<div>
{shouldFetch && (
<Suspense fallback={<div>Loading...</div>}>
<AsyncDataComponent />
</Suspense>
)}
</div>
);
}总结
Recoil Selectors是强大的派生状态管理工具:
- 派生状态:基于atoms计算新状态
- 异步支持:原生支持异步计算
- 自动缓存:结果会自动缓存直到依赖变化
- Selector Family:参数化的selector创建
- 性能优化:精确的依赖追踪
- 错误处理:配合ErrorBoundary处理异步错误
Selectors让Recoil可以处理复杂的状态逻辑和数据变换。