CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-recoil

Recoil is an experimental state management framework for React applications that provides atoms and selectors for fine-grained reactivity.

Pending
Overview
Eval results
Files

family-patterns.mddocs/

Family Patterns

Functions for creating parameterized atoms and selectors that are memoized by parameter. Family patterns allow you to create collections of related state that share the same structure but vary by some input parameter.

Capabilities

Atom Families

Creates a function that returns memoized atoms based on parameters.

/**
 * Returns a function which returns a memoized atom for each unique parameter value
 */
function atomFamily<T, P extends SerializableParam>(
  options: AtomFamilyOptions<T, P>
): (param: P) => RecoilState<T>;

type AtomFamilyOptions<T, P extends SerializableParam> = {
  /** Unique string identifying this atom family */
  key: string;
  /** Default value or function that returns default based on parameter */
  default?: T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | 
    ((param: P) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>);
  /** Effects for each atom or function returning effects based on parameter */
  effects?: ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
  /** Allow direct mutation of atom values */
  dangerouslyAllowMutability?: boolean;
};

type SerializableParam = 
  | undefined | null | boolean | number | symbol | string
  | ReadonlyArray<SerializableParam>
  | ReadonlySet<SerializableParam>
  | ReadonlyMap<SerializableParam, SerializableParam>
  | Readonly<{[key: string]: SerializableParam}>;

Usage Examples:

import { atomFamily, useRecoilState } from 'recoil';

// Simple atom family with primitive parameter
const itemState = atomFamily({
  key: 'itemState',
  default: null,
});

// Usage in components
function ItemEditor({ itemId }) {
  const [item, setItem] = useRecoilState(itemState(itemId));
  
  return (
    <input
      value={item || ''}
      onChange={(e) => setItem(e.target.value)}
    />
  );
}

// Atom family with object parameter
const userPreferencesState = atomFamily({
  key: 'userPreferencesState',
  default: (userId) => ({
    theme: 'light',
    notifications: true,
    userId,
  }),
});

// Atom family with async default
const userProfileState = atomFamily({
  key: 'userProfileState',
  default: async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

// Atom family with parameter-based effects
const persistedItemState = atomFamily({
  key: 'persistedItemState',
  default: '',
  effects: (itemId) => [
    ({setSelf, onSet}) => {
      const key = `item-${itemId}`;
      const saved = localStorage.getItem(key);
      if (saved != null) {
        setSelf(JSON.parse(saved));
      }
      
      onSet((newValue) => {
        localStorage.setItem(key, JSON.stringify(newValue));
      });
    },
  ],
});

Selector Families

Creates functions that return memoized selectors based on parameters.

/**
 * Returns a function which returns a memoized selector for each unique parameter value
 */
function selectorFamily<T, P extends SerializableParam>(
  options: ReadWriteSelectorFamilyOptions<T, P>
): (param: P) => RecoilState<T>;

function selectorFamily<T, P extends SerializableParam>(
  options: ReadOnlySelectorFamilyOptions<T, P>
): (param: P) => RecoilValueReadOnly<T>;

interface ReadOnlySelectorFamilyOptions<T, P extends SerializableParam> {
  /** Unique string identifying this selector family */
  key: string;
  /** Function that computes the selector's value based on parameter */
  get: (param: P) => (opts: {
    get: GetRecoilValue;
    getCallback: GetCallback;
  }) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
  /** Cache policy for selectors in this family */
  cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
  /** Allow direct mutation of selector values */
  dangerouslyAllowMutability?: boolean;
}

interface ReadWriteSelectorFamilyOptions<T, P extends SerializableParam> {
  /** Unique string identifying this selector family */
  key: string;
  /** Function that computes the selector's value based on parameter */
  get: (param: P) => (opts: {
    get: GetRecoilValue;
    getCallback: GetCallback;
  }) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>;
  /** Function that handles setting the selector's value based on parameter */
  set: (param: P) => (opts: {
    set: SetRecoilState;
    get: GetRecoilValue;
    reset: ResetRecoilState;
  }, newValue: T | DefaultValue) => void;
  /** Cache policy for selectors in this family */
  cachePolicy_UNSTABLE?: CachePolicyWithoutEquality;
  /** Allow direct mutation of selector values */
  dangerouslyAllowMutability?: boolean;
}

Usage Examples:

import { selectorFamily, atomFamily, useRecoilValue } from 'recoil';

// Read-only selector family
const itemWithMetadataState = selectorFamily({
  key: 'itemWithMetadataState',
  get: (itemId) => ({get}) => {
    const item = get(itemState(itemId));
    const metadata = get(itemMetadataState(itemId));
    
    return {
      ...item,
      ...metadata,
      fullId: `item-${itemId}`,
    };
  },
});

// Async selector family
const userPostsState = selectorFamily({
  key: 'userPostsState',
  get: (userId) => async ({get}) => {
    const user = get(userState(userId));
    const response = await fetch(`/api/users/${userId}/posts`);
    return response.json();
  },
});

// Read-write selector family
const itemDisplayNameState = selectorFamily({
  key: 'itemDisplayNameState',
  get: (itemId) => ({get}) => {
    const item = get(itemState(itemId));
    return item?.name || `Item ${itemId}`;
  },
  set: (itemId) => ({set, get}, newValue) => {
    const currentItem = get(itemState(itemId));
    set(itemState(itemId), {
      ...currentItem,
      name: newValue as string,
    });
  },
});

// Selector family with complex parameter
const filteredListState = selectorFamily({
  key: 'filteredListState',
  get: ({listId, filter}) => ({get}) => {
    const list = get(listState(listId));
    return list.filter(item => 
      item.category === filter.category &&
      item.status === filter.status
    );
  },
});

// Usage with object parameter
function FilteredList({ listId, category, status }) {
  const filteredItems = useRecoilValue(filteredListState({
    listId,
    filter: { category, status }
  }));
  
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Parameter-based Effects

Examples of using effects with families for per-parameter side effects.

Usage Examples:

import { atomFamily } from 'recoil';

// WebSocket connection per chat room
const chatRoomState = atomFamily({
  key: 'chatRoomState',
  default: { messages: [], connected: false },
  effects: (roomId) => [
    ({setSelf, onSet}) => {
      let ws: WebSocket;
      
      // Connect to room-specific WebSocket
      const connect = () => {
        ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);
        
        ws.onopen = () => {
          setSelf(current => ({ ...current, connected: true }));
        };
        
        ws.onmessage = (event) => {
          const message = JSON.parse(event.data);
          setSelf(current => ({
            ...current,
            messages: [...current.messages, message],
          }));
        };
        
        ws.onclose = () => {
          setSelf(current => ({ ...current, connected: false }));
        };
      };
      
      // Track when messages are sent
      onSet((newValue, oldValue) => {
        if (ws && ws.readyState === WebSocket.OPEN) {
          const newMessages = newValue.messages;
          const oldMessages = oldValue.messages || [];
          
          if (newMessages.length > oldMessages.length) {
            const lastMessage = newMessages[newMessages.length - 1];
            if (lastMessage.type === 'outgoing') {
              ws.send(JSON.stringify(lastMessage));
            }
          }
        }
      });
      
      connect();
      
      // Cleanup
      return () => {
        if (ws) {
          ws.close();
        }
      };
    },
  ],
});

// API cache with per-endpoint invalidation
const apiCacheState = atomFamily({
  key: 'apiCacheState',
  default: null,
  effects: (endpoint) => [
    ({setSelf}) => {
      // Auto-refresh certain endpoints
      if (endpoint.includes('/live-data/')) {
        const interval = setInterval(async () => {
          try {
            const response = await fetch(endpoint);
            const data = await response.json();
            setSelf(data);
          } catch (error) {
            console.error(`Failed to refresh ${endpoint}:`, error);
          }
        }, 5000);
        
        return () => clearInterval(interval);
      }
    },
  ],
});

Best Practices

Parameter Design:

  • Use serializable parameters only (primitives, arrays, objects, Maps, Sets)
  • Keep parameters immutable to ensure proper memoization
  • Use object parameters for complex combinations of values
  • Consider parameter normalization for consistent cache keys

Performance Considerations:

  • Family instances are memoized by parameter reference/value
  • Avoid creating new parameter objects on every render
  • Use useMemo for complex parameter objects
  • Consider cache policies for selector families with expensive computations

Usage Examples:

import React, { useMemo } from 'react';
import { selectorFamily, useRecoilValue } from 'recoil';

// Good: Stable parameter object
function UserDashboard({ userId, filters }) {
  const filterParams = useMemo(() => ({
    userId,
    ...filters,
  }), [userId, filters]);
  
  const dashboardData = useRecoilValue(userDashboardState(filterParams));
  
  return <div>{/* render dashboard */}</div>;
}

// Bad: New object on every render
function UserDashboardBad({ userId, filters }) {
  // This creates a new parameter object on every render!
  const dashboardData = useRecoilValue(userDashboardState({
    userId,
    ...filters,
  }));
  
  return <div>{/* render dashboard */}</div>;
}

Install with Tessl CLI

npx tessl i tessl/npm-recoil

docs

advanced-hooks.md

concurrency-helpers.md

core-hooks.md

family-patterns.md

index.md

loadable-system.md

memory-management.md

root-provider.md

state-definition.md

tile.json