state-effector

Effector reactive state management for React/Next.js applications. This skill should be used when implementing Effector stores, events, and effects, or when choosing Effector as the state management solution.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "state-effector" with this command: npx skills add aussiegingersnap/cursor-skills/aussiegingersnap-cursor-skills-state-effector

Effector Skill

Patterns for reactive state management using Effector in React and Next.js applications.

When to Use This Skill

  • Setting up Effector in a new project
  • Creating stores, events, and effects
  • Integrating Effector with React components
  • Organizing Effector models/domains
  • Combining Effector with Tanstack Query

Why Effector

Effector provides a reactive, event-driven approach to state management:

FeatureBenefit
Fine-grained reactivityComponents only re-render when their specific data changes
TypeScript-firstFull type inference without extra work
Small bundle~10KB gzipped
Framework agnosticCore logic works without React
TestablePure functions, easy to test in isolation
No boilerplateNo reducers, action creators, or switch statements

Effector vs Zustand

AspectEffectorZustand
ModelEvent-driven, reactive graphsFlux-like stores
Learning curveSteeperGentler
ReactivityAutomatic derived stateManual selectors
AsyncFirst-class effectsDIY with middleware
Best forComplex reactive flowsSimple shared state

Setup

Installation

npm install effector effector-react

For Next.js with SSR

npm install effector effector-react @effector/next

Core Concepts

The Three Primitives

import { createStore, createEvent, createEffect } from 'effector';

// EVENT: Triggers state changes
const increment = createEvent();
const decrement = createEvent();
const reset = createEvent();

// STORE: Holds reactive state
const $count = createStore(0)
  .on(increment, (count) => count + 1)
  .on(decrement, (count) => count - 1)
  .reset(reset);

// EFFECT: Handles async operations
const fetchUserFx = createEffect(async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
});

Naming Conventions

TypeConventionExample
Store$ prefix$user, $count, $isLoading
EventcamelCase verbclicked, submitted, reset
EffectFx suffixfetchUserFx, saveDataFx

React Integration

useUnit Hook

The useUnit hook binds stores and events to React components:

'use client';

import { useUnit } from 'effector-react';
import { $count, increment, decrement, reset } from './model';

export function Counter() {
  const [count, onIncrement, onDecrement, onReset] = useUnit([
    $count,
    increment,
    decrement,
    reset,
  ]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => onIncrement()}>+</button>
      <button onClick={() => onDecrement()}>-</button>
      <button onClick={() => onReset()}>Reset</button>
    </div>
  );
}

Object Form (Named)

export function Counter() {
  const { count, inc, dec } = useUnit({
    count: $count,
    inc: increment,
    dec: decrement,
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
    </div>
  );
}

Derived State

Computed Stores

import { createStore, createEvent, combine } from 'effector';

const $items = createStore<Item[]>([]);
const $filter = createStore<'all' | 'active' | 'completed'>('all');

// Derived store - automatically updates when dependencies change
const $filteredItems = combine($items, $filter, (items, filter) => {
  switch (filter) {
    case 'active':
      return items.filter((item) => !item.completed);
    case 'completed':
      return items.filter((item) => item.completed);
    default:
      return items;
  }
});

// Count derived from items
const $itemCount = $items.map((items) => items.length);
const $completedCount = $items.map(
  (items) => items.filter((item) => item.completed).length
);

Effects (Async Operations)

Basic Effect

import { createEffect, createStore, createEvent } from 'effector';

interface User {
  id: string;
  name: string;
  email: string;
}

// Define effect with typed input/output
const fetchUserFx = createEffect<string, User, Error>(async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch');
  return response.json();
});

// Stores react to effect states
const $user = createStore<User | null>(null)
  .on(fetchUserFx.doneData, (_, user) => user)
  .reset(fetchUserFx.fail);

const $isLoading = fetchUserFx.pending;

const $error = createStore<string | null>(null)
  .on(fetchUserFx.failData, (_, error) => error.message)
  .reset(fetchUserFx);

Effect States

Every effect has built-in states:

fetchUserFx.pending;     // Store<boolean> - true while running
fetchUserFx.done;        // Event<{params, result}> - on success
fetchUserFx.doneData;    // Event<result> - just the result
fetchUserFx.fail;        // Event<{params, error}> - on failure
fetchUserFx.failData;    // Event<error> - just the error
fetchUserFx.finally;     // Event - always fires after completion

Usage in Components

'use client';

import { useUnit } from 'effector-react';
import { $user, $isLoading, $error, fetchUserFx } from './model';

export function UserProfile({ userId }: { userId: string }) {
  const { user, isLoading, error, fetchUser } = useUnit({
    user: $user,
    isLoading: $isLoading,
    error: $error,
    fetchUser: fetchUserFx,
  });

  useEffect(() => {
    fetchUser(userId);
  }, [userId, fetchUser]);

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage message={error} />;
  if (!user) return null;

  return <div>{user.name}</div>;
}

Event Chaining

sample - Connect Events to Effects

import { sample } from 'effector';

const formSubmitted = createEvent<FormData>();
const saveFormFx = createEffect<FormData, void>();

// When formSubmitted fires, call saveFormFx with the data
sample({
  clock: formSubmitted,
  target: saveFormFx,
});

sample with Transformation

const userIdChanged = createEvent<string>();
const $currentUserId = createStore('');

sample({
  clock: userIdChanged,
  source: $currentUserId,
  filter: (currentId, newId) => currentId !== newId, // Only if changed
  fn: (_, newId) => newId, // Transform data
  target: fetchUserFx,
});

forward - Simple Event-to-Event

import { forward } from 'effector';

const buttonClicked = createEvent();
const formReset = createEvent();

// When buttonClicked fires, also fire formReset
forward({
  from: buttonClicked,
  to: formReset,
});

Project Structure

Feature-Based Organization

src/
├── features/
│   ├── auth/
│   │   ├── model.ts        # Stores, events, effects
│   │   ├── ui/
│   │   │   ├── LoginForm.tsx
│   │   │   └── UserMenu.tsx
│   │   └── index.ts
│   ├── projects/
│   │   ├── model.ts
│   │   ├── ui/
│   │   │   ├── ProjectList.tsx
│   │   │   └── ProjectCard.tsx
│   │   └── index.ts
│   └── shared/
│       └── api.ts          # Shared effects for API calls
├── app/
│   └── ...
└── lib/
    └── effector.ts         # Shared utilities

Model File Pattern

// features/projects/model.ts
import { createStore, createEvent, createEffect, sample } from 'effector';

// Types
interface Project {
  id: string;
  name: string;
  status: 'active' | 'archived';
}

// Events
export const projectCreated = createEvent<{ name: string }>();
export const projectDeleted = createEvent<string>();
export const projectsPageOpened = createEvent();

// Effects
export const fetchProjectsFx = createEffect(async () => {
  const res = await fetch('/api/projects');
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json() as Promise<Project[]>;
});

export const createProjectFx = createEffect(async (data: { name: string }) => {
  const res = await fetch('/api/projects', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error('Failed to create');
  return res.json() as Promise<Project>;
});

export const deleteProjectFx = createEffect(async (id: string) => {
  const res = await fetch(`/api/projects/${id}`, { method: 'DELETE' });
  if (!res.ok) throw new Error('Failed to delete');
});

// Stores
export const $projects = createStore<Project[]>([])
  .on(fetchProjectsFx.doneData, (_, projects) => projects)
  .on(createProjectFx.doneData, (projects, newProject) => [...projects, newProject])
  .on(deleteProjectFx.done, (projects, { params: id }) =>
    projects.filter((p) => p.id !== id)
  );

export const $isLoading = fetchProjectsFx.pending;

// Connections
sample({
  clock: projectsPageOpened,
  target: fetchProjectsFx,
});

sample({
  clock: projectCreated,
  target: createProjectFx,
});

sample({
  clock: projectDeleted,
  target: deleteProjectFx,
});

Next.js SSR Integration

With @effector/next

// app/providers.tsx
'use client';

import { EffectorNext } from '@effector/next';

export function Providers({ children }: { children: React.ReactNode }) {
  return <EffectorNext>{children}</EffectorNext>;
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Hydration with Server Data

// Server Component
async function getProjects() {
  // Fetch on server
  return db.query.project.findMany();
}

export default async function ProjectsPage() {
  const projects = await getProjects();
  
  return (
    <ProjectsProvider initialProjects={projects}>
      <ProjectList />
    </ProjectsProvider>
  );
}

// Client Provider
'use client';

import { useEffect } from 'react';
import { projectsLoaded } from './model';

export function ProjectsProvider({
  children,
  initialProjects,
}: {
  children: React.ReactNode;
  initialProjects: Project[];
}) {
  useEffect(() => {
    projectsLoaded(initialProjects);
  }, [initialProjects]);

  return <>{children}</>;
}

Combining with Tanstack Query

Use Tanstack Query for server state, Effector for client state:

// Server state with Query
const { data: projects } = useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
});

// Client state with Effector
const { selectedProjectId, setSelectedProject } = useUnit({
  selectedProjectId: $selectedProjectId,
  setSelectedProject: projectSelected,
});

// Derived from both
const selectedProject = projects?.find((p) => p.id === selectedProjectId);

Testing

Testing Stores

import { fork, allSettled } from 'effector';
import { $count, increment, reset } from './model';

describe('counter', () => {
  it('increments count', async () => {
    const scope = fork();
    
    await allSettled(increment, { scope });
    await allSettled(increment, { scope });
    
    expect(scope.getState($count)).toBe(2);
  });

  it('resets to zero', async () => {
    const scope = fork({
      values: [[$count, 10]], // Initial state
    });
    
    await allSettled(reset, { scope });
    
    expect(scope.getState($count)).toBe(0);
  });
});

Testing Effects

import { fork, allSettled } from 'effector';
import { $user, fetchUserFx } from './model';

describe('fetchUser', () => {
  it('updates user on success', async () => {
    const mockUser = { id: '1', name: 'John' };
    
    const scope = fork({
      handlers: [
        [fetchUserFx, () => mockUser], // Mock the effect
      ],
    });
    
    await allSettled(fetchUserFx, { scope, params: '1' });
    
    expect(scope.getState($user)).toEqual(mockUser);
  });
});

Best Practices

Do

  • Use $ prefix for stores
  • Keep models separate from UI
  • Use sample for complex event flows
  • Use fork for testing
  • Derive state with combine and .map()

Don't

  • Don't call events inside effects (use sample)
  • Don't mutate store values directly
  • Don't create stores inside components
  • Don't ignore TypeScript errors

Quick Reference

// Create primitives
createStore(initialValue)
createEvent<PayloadType>()
createEffect<Params, Result, Error>(handler)

// React hooks
useUnit(store)              // Subscribe to store
useUnit([store, event])     // Subscribe to multiple
useUnit({ name: store })    // Named object form

// Derived state
$store.map(fn)              // Transform store value
combine($a, $b, fn)         // Combine multiple stores

// Event connections
sample({ clock, source, filter, fn, target })
forward({ from, to })

// Testing
fork({ values, handlers })
allSettled(unit, { scope, params })
scope.getState($store)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tools-repo-review

No summary provided by upstream source.

Repository SourceNeeds Review
General

db-postgres

No summary provided by upstream source.

Repository SourceNeeds Review
General

ui-principles

No summary provided by upstream source.

Repository SourceNeeds Review
state-effector | V50.AI