skills/state-effector/SKILL.md
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.
npx skillsauth add aussiegingersnap/cursor-skills state-effectorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for reactive state management using Effector in React and Next.js applications.
Effector provides a reactive, event-driven approach to state management:
| Feature | Benefit | |---------|---------| | Fine-grained reactivity | Components only re-render when their specific data changes | | TypeScript-first | Full type inference without extra work | | Small bundle | ~10KB gzipped | | Framework agnostic | Core logic works without React | | Testable | Pure functions, easy to test in isolation | | No boilerplate | No reducers, action creators, or switch statements |
| Aspect | Effector | Zustand | |--------|----------|---------| | Model | Event-driven, reactive graphs | Flux-like stores | | Learning curve | Steeper | Gentler | | Reactivity | Automatic derived state | Manual selectors | | Async | First-class effects | DIY with middleware | | Best for | Complex reactive flows | Simple shared state |
npm install effector effector-react
npm install effector effector-react @effector/next
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();
});
| Type | Convention | Example |
|------|------------|---------|
| Store | $ prefix | $user, $count, $isLoading |
| Event | camelCase verb | clicked, submitted, reset |
| Effect | Fx suffix | fetchUserFx, saveDataFx |
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>
);
}
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>
);
}
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
);
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);
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
'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>;
}
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,
});
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,
});
import { forward } from 'effector';
const buttonClicked = createEvent();
const formReset = createEvent();
// When buttonClicked fires, also fire formReset
forward({
from: buttonClicked,
to: formReset,
});
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
// 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,
});
// 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>
);
}
// 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}</>;
}
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);
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);
});
});
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);
});
});
$ prefix for storessample for complex event flowsfork for testingcombine and .map()sample)// 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)
tools
# Versioning Skill Semantic versioning automation based on conventional commits. Automatically manages version bumps, changelogs, and git tags using `standard-version`. ## When to Use - Before releasing a new version - When preparing a deployment - To generate/update CHANGELOG.md - When the user asks about version management - Setting up versioning for a new project ## Prerequisites - Conventional commits enforced (recommended: lefthook) - Node.js project with package.json ## Setup (One-Ti
tools
Theme generation with tweakcn for shadcn/ui and Magic UI animations. Use when setting up project themes, customizing color schemes, adding dark mode, or integrating animated components.
tools
shadcn/studio component library with MCP integration, theme generation, and block patterns. This skill should be used when building UI with shadcn components, selecting dashboard layouts, or generating landing pages. Canonical source for all shadcn-based work.
development
Enforce a precise, minimal design system inspired by Linear, Notion, and Stripe. Use this skill when building dashboards, admin interfaces, or any UI that needs Jony Ive-level precision - clean, modern, minimalist with taste. Every pixel matters.