Selector家族 #
什么是 selectorFamily? #
selectorFamily 是一个工厂函数,用于根据参数动态创建多个相关的 Selector。当你需要根据不同参数计算派生状态时,selectorFamily 非常有用。
text
┌─────────────────────────────────────────────────────┐
│ selectorFamily │
├─────────────────────────────────────────────────────┤
│ │
│ selectorFamily(param) ──▶ Selector │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ userQuery(1) ──▶ userSelector_1 │ │
│ │ userQuery(2) ──▶ userSelector_2 │ │
│ │ userQuery(3) ──▶ userSelector_3 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 每个参数独立缓存 │
└─────────────────────────────────────────────────────┘
基本语法 #
jsx
import { selectorFamily } from 'recoil';
const myFamily = selectorFamily({
key: 'myFamily',
get: (param) => ({ get }) => {
const value = get(someAtom);
return computedValue;
},
});
创建 selectorFamily #
简单示例 #
jsx
import { atom, selectorFamily, useRecoilValue } from 'recoil';
const itemsState = atom({
key: 'items',
default: [
{ id: 1, name: 'Item 1', price: 100 },
{ id: 2, name: 'Item 2', price: 200 },
{ id: 3, name: 'Item 3', price: 300 },
],
});
const itemByIdState = selectorFamily({
key: 'itemById',
get: (id) => ({ get }) => {
const items = get(itemsState);
return items.find(item => item.id === id);
},
});
function ItemDetail({ itemId }) {
const item = useRecoilValue(itemByIdState(itemId));
if (!item) return <div>Item not found</div>;
return (
<div>
<h3>{item.name}</h3>
<p>Price: ${item.price}</p>
</div>
);
}
参数化过滤 #
jsx
const filterByCategoryState = selectorFamily({
key: 'filterByCategory',
get: (category) => ({ get }) => {
const items = get(itemsState);
if (category === 'all') return items;
return items.filter(item => item.category === category);
},
});
function CategoryList({ category }) {
const filteredItems = useRecoilValue(filterByCategoryState(category));
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
异步数据获取 #
selectorFamily 非常适合用于 API 数据获取:
jsx
const userQueryState = selectorFamily({
key: 'userQuery',
get: (userId) => async ({ get }) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`User ${userId} not found`);
}
return response.json();
},
});
function UserProfile({ userId }) {
const userLoadable = useRecoilValueLoadable(userQueryState(userId));
switch (userLoadable.state) {
case 'loading':
return <div>Loading user {userId}...</div>;
case 'hasValue':
return (
<div>
<h2>{userLoadable.contents.name}</h2>
<p>{userLoadable.contents.email}</p>
</div>
);
case 'hasError':
return <div>Error: {userLoadable.contents.message}</div>;
}
}
实战示例:博客文章 #
jsx
import { atom, selectorFamily, useRecoilState, useRecoilValue, useRecoilValueLoadable } from 'recoil';
const postsState = atom({
key: 'posts',
default: [],
});
const postByIdState = selectorFamily({
key: 'postById',
get: (postId) => ({ get }) => {
const posts = get(postsState);
return posts.find(p => p.id === postId);
},
});
const commentsByPostIdState = selectorFamily({
key: 'commentsByPostId',
get: (postId) => async () => {
const response = await fetch(`/api/posts/${postId}/comments`);
return response.json();
},
});
const relatedPostsState = selectorFamily({
key: 'relatedPosts',
get: (postId) => ({ get }) => {
const posts = get(postsState);
const currentPost = get(postByIdState(postId));
if (!currentPost) return [];
return posts
.filter(p => p.id !== postId && p.tags.some(tag => currentPost.tags.includes(tag)))
.slice(0, 3);
},
});
function PostDetail({ postId }) {
const post = useRecoilValue(postByIdState(postId));
const commentsLoadable = useRecoilValueLoadable(commentsByPostIdState(postId));
const relatedPosts = useRecoilValue(relatedPostsState(postId));
if (!post) return <div>Post not found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<section>
<h2>Comments</h2>
{commentsLoadable.state === 'loading' && <p>Loading comments...</p>}
{commentsLoadable.state === 'hasValue' && (
<ul>
{commentsLoadable.contents.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
)}
</section>
<section>
<h2>Related Posts</h2>
{relatedPosts.map(p => (
<div key={p.id}>{p.title}</div>
))}
</section>
</article>
);
}
可写 selectorFamily #
jsx
import { selectorFamily, DefaultValue } from 'recoil';
const itemByIdWritableState = selectorFamily({
key: 'itemByIdWritable',
get: (id) => ({ get }) => {
const items = get(itemsState);
return items.find(item => item.id === id);
},
set: (id) => ({ set, get }, newValue) => {
if (newValue instanceof DefaultValue) {
return;
}
const items = get(itemsState);
const updatedItems = items.map(item =>
item.id === id ? { ...item, ...newValue } : item
);
set(itemsState, updatedItems);
},
});
function ItemEditor({ itemId }) {
const [item, setItem] = useRecoilState(itemByIdWritableState(itemId));
if (!item) return null;
const updateName = (e) => {
setItem({ ...item, name: e.target.value });
};
return (
<input value={item.name} onChange={updateName} />
);
}
对象参数 #
jsx
const searchQueryState = selectorFamily({
key: 'searchQuery',
get: ({ keyword, category, sortBy }) => async () => {
const params = new URLSearchParams({
q: keyword,
category,
sortBy,
});
const response = await fetch(`/api/search?${params}`);
return response.json();
},
});
function SearchResults({ keyword, category, sortBy }) {
const resultsLoadable = useRecoilValueLoadable(
searchQueryState({ keyword, category, sortBy })
);
if (resultsLoadable.state === 'loading') {
return <div>Searching...</div>;
}
if (resultsLoadable.state === 'hasValue') {
return (
<ul>
{resultsLoadable.contents.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
}
return <div>Error occurred</div>;
}
缓存策略 #
默认缓存 #
selectorFamily 默认会缓存每个参数的结果:
jsx
const cachedQueryState = selectorFamily({
key: 'cachedQuery',
get: (id) => async () => {
console.log(`Fetching data for id: ${id}`);
const response = await fetch(`/api/data/${id}`);
return response.json();
},
});
自定义缓存策略 #
jsx
const cachedState = selectorFamily({
key: 'cached',
get: (param) => ({ get }) => {
return computeValue(param, get(dataState));
},
cachePolicyForParams_UNSTABLE: {
equality: 'value',
},
cachePolicy_UNSTABLE: {
eviction: 'lru',
maxSize: 100,
},
});
| 选项 | 说明 |
|---|---|
eviction: 'lru' |
最近最少使用淘汰 |
eviction: 'keep-all' |
保留所有 |
maxSize |
最大缓存数量 |
与 atomFamily 结合 #
jsx
import { atomFamily, selectorFamily } from 'recoil';
const todoFamily = atomFamily({
key: 'todoFamily',
default: (id) => ({
id,
text: '',
completed: false,
}),
});
const todoStatsState = selectorFamily({
key: 'todoStats',
get: (id) => ({ get }) => {
const todo = get(todoFamily(id));
return {
charCount: todo.text.length,
wordCount: todo.text.split(/\s+/).filter(Boolean).length,
};
},
});
function TodoItem({ id }) {
const [todo, setTodo] = useRecoilState(todoFamily(id));
const stats = useRecoilValue(todoStatsState(id));
return (
<div>
<input
value={todo.text}
onChange={(e) => setTodo({ ...todo, text: e.target.value })}
/>
<small>{stats.charCount} chars, {stats.wordCount} words</small>
</div>
);
}
TypeScript 支持 #
tsx
import { selectorFamily, useRecoilValue } from 'recoil';
interface User {
id: number;
name: string;
email: string;
}
const userQueryState = selectorFamily<User, number>({
key: 'userQuery',
get: (userId) => async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
function UserProfile({ userId }: { userId: number }) {
const user = useRecoilValue(userQueryState(userId));
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
多参数类型 #
tsx
interface SearchParams {
keyword: string;
category: string;
page: number;
}
interface SearchResult {
items: Item[];
total: number;
}
const searchQueryState = selectorFamily<SearchResult, SearchParams>({
key: 'searchQuery',
get: (params) => async () => {
const response = await fetch(`/api/search?${new URLSearchParams(params as any)}`);
return response.json();
},
});
完整示例:分页数据 #
jsx
import { atom, selectorFamily, useRecoilState, useRecoilValueLoadable } from 'recoil';
const pageSizeState = atom({
key: 'pageSize',
default: 10,
});
const paginatedDataState = selectorFamily({
key: 'paginatedData',
get: (page) => async ({ get }) => {
const pageSize = get(pageSizeState);
const response = await fetch(`/api/data?page=${page}&size=${pageSize}`);
return response.json();
},
});
function DataTable() {
const [currentPage, setCurrentPage] = useState(1);
const dataLoadable = useRecoilValueLoadable(paginatedDataState(currentPage));
if (dataLoadable.state === 'loading') {
return <div>Loading...</div>;
}
if (dataLoadable.state === 'hasError') {
return <div>Error: {dataLoadable.contents.message}</div>;
}
const { items, total, totalPages } = dataLoadable.contents;
return (
<div>
<table>
<tbody>
{items.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(p => p - 1)}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(p => p + 1)}
>
Next
</button>
</div>
</div>
);
}
总结 #
selectorFamily 的核心用途:
| 用途 | 说明 |
|---|---|
| 按ID查询 | 根据ID获取特定数据 |
| 参数化过滤 | 根据条件过滤数据 |
| API请求 | 根据参数发起API请求 |
| 计算属性 | 根据参数计算派生值 |
下一步,让我们学习 useRecoilState,深入了解 Recoil 的 Hooks API。
最后更新:2026-03-28