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