Solux is a comprehensive state management library for SolidJS applications that brings the battle-tested patterns of Flux to SolidJS's reactive ecosystem. If you're building highly dynamic applications like design tools, dashboards, or complex SPAs, Solux provides the predictable state management you need with the performance benefits of SolidJS's fine-grained reactivity.
- Why Solux?
- Installation
- Quick Start
- Core Concepts
- API Reference
- Advanced Usage
- DevTools Integration
- Contributing
- License
The SolidJS ecosystem lacks a mature state management solution. Developers building complex SolidJS applications need proven patterns with the benefits of SolidJS's fine-grained reactivity.
Solux is built specifically for SolidJS's reactive system from the ground up. It provides:
- Familiar Redux patterns - If you know Redux Toolkit, you already know 95% of Solux
- Fine-grained reactivity - Only the components that need to update will update
- Full TypeScript support - Complete type inference for events, handlers, and state
- Redux DevTools integration - Time-travel debugging and state inspection
- Built-in entity management - Normalize your data without the boilerplate
- Epic middleware - Handle complex async flows with RxJS
✅ Use Solux when you:
- Build complex, stateful applications (like Figma, Notion, or complex dashboards)
- Need predictable state management with time-travel debugging
- Want to share state across many components
- Have complex async operations or side effects
- Work in a team that needs clear state management patterns
❌ You might not need Solux if you:
- Build simple applications with minimal state
- Only need local component state
- Want the smallest possible bundle size
# bun (recommended)
bun add @carere/solux
# npm
npm install @carere/soluxHere's a complete counter example to get you started in under 50 lines:
import { createEvent, createSlice, combineSlices, configureStore } from '@carere/solux';
import { SoluxProvider, useSolux } from '@carere/solux';
import { render } from 'solid-js/web';
// 1. Create events (actions in Redux terminology)
// Events are type-safe - TypeScript infers payload types automatically
const increment = createEvent<number>('counter/increment');
const decrement = createEvent<number>('counter/decrement');
const reset = createEvent('counter/reset');
// 2. Create a slice with handlers
// Handlers use SolidJS's produce() internally, allowing mutable-style updates
const counterSlice = createSlice({
initialState: { value: 0 },
handlers: (builder) =>
builder
.addHandler(increment, (state, { payload }) => {
// Mutable syntax, but immutably applied via produce()
state.value += payload;
})
.addHandler(decrement, (state, { payload }) => {
state.value -= payload;
})
.addHandler(reset, (state) => {
state.value = 0;
}),
});
// 3. Configure the store
// The store leverages SolidJS's fine-grained reactivity system
const store = configureStore({
rootSlice: combineSlices({
counter: counterSlice,
}),
});
// 4. Use in your component
function Counter() {
const { state, dispatch } = useSolux();
return (
<div>
<h1>Count: {state.counter.value}</h1>
<button onClick={() => dispatch(increment(1))}>+1</button>
<button onClick={() => dispatch(decrement(1))}>-1</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
// 5. Wrap your app with the provider
render(
() => (
<SoluxProvider store={store}>
<Counter />
</SoluxProvider>
),
document.getElementById('root')!
);To enable Redux DevTools:
import { configureStore, applyMiddlewares, devtools } from '@carere/solux';
const store = configureStore({
rootSlice: combineSlices({ /* your slices */ }),
enhancers: [
devtools({
// Full configuration from redux devtools extension available
name: 'My App',
instanceId: 'main'
})
],
});The store holds your entire application state (or portion) and provides methods to dispatch events and subscribe to changes.
interface RootState {
user: UserState;
posts: PostsState;
settings: SettingsState;
}
const store = configureStore({
rootSlice: combineSlices({
user: userSlice,
posts: postsSlice,
settings: settingsSlice,
}),
preloadedState: { /* optional initial state */ },
enhancers: [ /* middleware and devtools */ ],
});
// Access state (reactive in SolidJS components)
console.log(store.state.user);
// Dispatch events
store.dispatch(loginSuccess({ id: 1, name: 'John' }));
// Subscribe to all changes
const unsubscribe = store.subscribe(({ state, event }) => {
console.log('State changed:', state);
console.log('Due to event:', event);
});
// Subscribe to specific events
store.subscribeToEvent(loginSuccess, ({ state, event }) => {
console.log('User logged in:', event.payload);
});Slices are modular units of state with their own handlers. Think of them as reducers with built-in action creators.
interface UserState {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
const userSlice = createSlice({
initialState: {
currentUser: null,
isLoading: false,
error: null,
} as UserState,
handlers: (builder) =>
builder
.addHandler(loginRequest, (state) => {
state.isLoading = true;
state.error = null;
})
.addHandler(loginSuccess, (state, { payload }) => {
state.currentUser = payload;
state.isLoading = false;
})
.addHandler(loginFailure, (state, { payload }) => {
state.error = payload;
state.isLoading = false;
}),
});Events are type-safe action creators. They're the only way to trigger state changes.
// Simple event without payload
const logout = createEvent('user/logout');
// Event with typed payload
const updateProfile = createEvent<{ name: string; email: string }>('user/updateProfile');
// Event with payload preparation (transforms input before dispatching)
const addTodo = createEvent('todos/add', (text: string) => ({
payload: {
id: nanoid(),
text,
completed: false,
createdAt: Date.now(),
},
}));
// Usage
store.dispatch(logout());
store.dispatch(updateProfile({ name: 'Jane', email: 'jane@example.com' }));
store.dispatch(addTodo('Learn Solux')); // Automatically generates ID and timestampHandlers process events and update state. Thanks to SolidJS's produce, you can write mutable updates for simplicity:
interface TodosState {
items: Todo[];
}
const todosSlice = createSlice({
initialState: { items: [] } as TodosState,
handlers: (builder) =>
builder
// Mutable update (powered by produce)
.addHandler(addTodo, (state, { payload }) => {
state.items.push(payload);
})
// You can also return a new state if preferred
.addHandler(clearCompleted, (state) => ({
items: state.items.filter(todo => !todo.completed)
}))
});The entity adapter provides a standardized way to manage normalized data:
interface Post {
id: string;
title: string;
authorId: string;
createdAt: number;
}
const postsAdapter = createEntityAdapter<Post>({
selectId: (post) => post.id,
sortComparer: (a, b) => b.createdAt - a.createdAt, // Optional: sort by newest first
});
const postsSlice = createSlice({
initialState: postsAdapter.getInitialState({
// Additional state alongside entities
isLoading: false,
}),
handlers: (builder) =>
builder
.addHandler(fetchPostsSuccess, (state, { payload }) => {
postsAdapter.setAll(state, payload);
state.isLoading = false;
})
.addHandler(addPost, (state, { payload }) => {
postsAdapter.addOne(state, payload);
})
.addHandler(updatePost, (state, { payload }) => {
const { id, ...changes } = payload;
const post = state.entities[id];
if (post) Object.assign(post, changes);
})
.addHandler(deletePost, (state, { payload }) => {
postsAdapter.removeOne(state, payload);
}),
});
// Create typed selectors
const selectors = postsAdapter.getSelectors<RootState>(
(state) => state.posts
);
// Usage in components
const allPosts: Post[] = selectors.selectAll(store.state);
const postCount: number = selectors.selectTotal(store.state);
const postById: Post | undefined = selectors.selectById(store.state, 'post-1');Epics handle complex async operations using RxJS observables:
import { combineEpics, epicMiddleware } from '@carere/solux';
import { filter, map, switchMap, catchError } from 'rxjs/operators';
import { from, of } from 'rxjs';
const loginEpic = (event$, state$) =>
event$.pipe(
filter(loginRequest.match),
switchMap(({ payload }) =>
from(api.login(payload)).pipe(
map(user => loginSuccess(user)),
catchError(error => of(loginFailure(error.message)))
)
)
);
const rootEpic = combineEpics(loginEpic, logoutEpic, /* more epics */);
const store = configureStore({
rootSlice: /* ... */,
enhancers: [
applyMiddlewares([
epicMiddleware({ rootEpic })
])
],
});Creates and configures a Solux store.
const store = configureStore({
rootSlice: Slice, // Required: Root slice or combined slices
preloadedState?: State, // Optional: Initial state
enhancers?: Enhancer[], // Optional: Middleware and Enhancers
});Combines multiple slices into a single root slice.
const rootSlice = combineSlices({
user: userSlice,
posts: postsSlice,
nested: combineSlices({
nested1: nested1Slice,
nested2: nested2Slice,
}),
...
});Creates a slice with handlers and optional selectors.
const slice = createSlice({
initialState: State,
handlers: (builder) => builder.addHandler(...),
});Creates a type-safe event creator.
const event = createEvent<PayloadType>('event/type');
const eventWithPrepare = createEvent('event/type', (arg) => ({
payload: transformArg(arg),
meta: { timestamp: Date.now() }
}));Creates an entity adapter for normalized state management.
const adapter = createEntityAdapter<Entity>({
selectId: (entity) => entity.id,
sortComparer?: (a, b) => a.order - b.order,
});
// Adapter methods
adapter.getInitialState(extraState?)
adapter.addOne(state, entity)
adapter.addMany(state, entities)
adapter.setOne(state, entity) // overwrite existing entity
adapter.setMany(state, entities) // overwrite existing entities
adapter.setAll(state, entities)
adapter.removeOne(state, id)
adapter.removeMany(state, ids)
adapter.removeAll(state)
// Selectors
const selectors = adapter.getSelectors(selectState);
selectors.selectIds(state)
selectors.selectEntities(state) // return a record of entities
selectors.selectAll(state) // return an array of entities ordered
selectors.selectTotal(state)
selectors.selectById(state, id)Combines multiple epics into a root epic.
const rootEpic = combineEpics(epic1, epic2, epic3);Creates middleware for handling epics.
const middleware = epicMiddleware({
rootEpic: Epic,
container?: Dependencies,
});Provides store to the component tree.
<SoluxProvider store={store}>
<App />
</SoluxProvider>Hook to access store in components.
const { state, dispatch } = useSolux();Create custom middleware to extend Solux capabilities:
import { Middleware } from '@carere/solux';
// Logging middleware
const logger: Middleware<RootState> = (api) => (next) => (event) => {
console.group(event.type);
console.log('Previous State:', api.state);
console.log('Event:', event);
next(event);
console.log('Next State:', api.state);
console.groupEnd();
};
// Persistence middleware
const persistence: Middleware<RootState> = (api) => (next) => (event) => {
next(event);
localStorage.setItem('app-state', JSON.stringify(api.state));
};
// Analytics middleware
const analytics: Middleware<RootState> = (api) => (next) => (event) => {
next(event);
if (event.type.includes('user/')) {
trackEvent('user_action', { type: event.type });
}
};
const store = configureStore({
rootSlice: rootSlice,
enhancers: [
applyMiddlewares([logger, persistence, analytics]),
devtools({ name: 'My App' }),
],
});Epics excel at handling complex async scenarios that go beyond simple data fetching:
import { debounceTime, switchMap, filter, map, catchError } from 'rxjs/operators';
import { from, of } from 'rxjs';
// Debounced search with automatic cancellation
const searchEpic = (event$, state$) =>
event$.pipe(
filter(searchInputChanged.match),
debounceTime(300), // Wait for user to stop typing
switchMap(({ payload: query }) => // Cancel previous search
from(api.search(query)).pipe(
map(results => searchSuccess(results)),
catchError(error => of(searchFailure(error.message)))
)
)
);
const rootEpic = combineEpics(searchEpic);Key benefits of epics:
- Automatic cancellation via
switchMap(previous searches are cancelled) - Complex orchestration (chain actions, race conditions, etc.)
- Declarative async (no imperative try/catch blocks)
- Powerful operators (debounce, retry, throttle, etc.)
function Dashboard() {
const { state } = useSolux();
// Expensive computation only runs when dependencies change
const statistics = createMemo(() => {
const posts = postsSelectors.selectAll(state);
return {
total: posts.length,
published: posts.filter(p => p.status === 'published').length,
draft: posts.filter(p => p.status === 'draft').length,
avgLength: posts.reduce((acc, p) => acc + p.content.length, 0) / posts.length,
};
});
return (
<div>
<h2>Statistics</h2>
<p>Total posts: {statistics().total}</p>
<p>Published: {statistics().published}</p>
<p>Drafts: {statistics().draft}</p>
<p>Average length: {statistics().avgLength.toFixed(0)} characters</p>
</div>
);
}import { describe, it, expect, beforeEach } from 'bun:test';
describe('User Slice', () => {
let store;
beforeEach(() => {
store = configureStore({
rootSlice: combineSlices({ user: userSlice }),
});
});
it('should handle login flow', () => {
// Initial state
expect(store.state.user.isLoading).toBe(false);
expect(store.state.user.currentUser).toBeNull();
// Dispatch login request
store.dispatch(loginRequest({ email: 'test@example.com', password: 'password' }));
expect(store.state.user.isLoading).toBe(true);
// Dispatch login success
const user = { id: 1, email: 'test@example.com', name: 'Test User' };
store.dispatch(loginSuccess(user));
expect(store.state.user.isLoading).toBe(false);
expect(store.state.user.currentUser).toEqual(user);
// Dispatch logout
store.dispatch(logout());
expect(store.state.user.currentUser).toBeNull();
});
it('should handle login failure', () => {
store.dispatch(loginRequest({ email: 'test@example.com', password: 'wrong' }));
store.dispatch(loginFailure('Invalid credentials'));
expect(store.state.user.isLoading).toBe(false);
expect(store.state.user.error).toBe('Invalid credentials');
expect(store.state.user.currentUser).toBeNull();
});
});Solux integrates seamlessly with Redux DevTools:
- Install the Redux DevTools Extension
- Configure your store:
import { devtools } from '@carere/solux';
const store = configureStore({
rootSlice: rootSlice,
enhancers: [
devtools({
name: 'My Solux App',
instanceId: 'main',
trace: true,
traceLimit: 25,
})
],
});Features supported:
- ✅ Action history
- ✅ State inspection
- ✅ Time-travel debugging
- ✅ Action dispatching
- ✅ State import/export
- ✅ Jump to state
We welcome contributions! Please see our Contributing Guide for details.
MIT © Kevin Abatan
Built with ❤️ for the SolidJS community