Skip to content

DeveloperRejaul/zustic

Repository files navigation

Zustic

Ultra-Lightweight State Management for Modern React Applications

npm version npm downloads Bundle Size License

A fast, minimal state management solution for React ecosystems. Works seamlessly with React, Next.js, and React Native with predictable state updates and a tiny footprint.

📖 Documentation · 🐛 Report Bug · 💡 Request Feature


Key Features

Core Features

  • Ultra-Lightweight — Only ~500B (gzipped) with zero dependencies
  • Simple API — One function (create) to manage all your state
  • React Hooks — Native React hooks integration with automatic subscriptions
  • Multi-Platform — React, React Native, Next.js, and modern frameworks
  • Reactive Updates — Automatic re-renders with optimized batching
  • TypeScript First — Full type safety with perfect type inference
  • Production Ready — Battle-tested in real applications

Advanced Capabilities

  • Store Middleware System — Extend state management with logging, persistence, validation, and more
  • Query System — Built-in API data fetching with automatic caching, mutations, and plugins
  • Automatic Caching — Smart cache management with configurable timeout
  • Direct State Accessget() function for reading state outside components
  • Selective Subscriptions — Components only re-render when their data changes
  • Fully Extensible — Build custom middleware and plugins for any use case
  • Easy Testing — Simple to test stores and API queries with middleware and async operations
  • Plugin System — Global hooks for authentication, logging, error handling
  • Framework Agnostic — Create middleware and plugins once, use everywhere

Installation

Choose your favorite package manager:

# npm
npm install zustic

# yarn
yarn add zustic

# pnpm
pnpm add zustic

Why Zustic?

Size & Performance

Metric Zustic Redux Zustand Context API
Bundle Size ~500B ~6KB ~2KB Built-in
Performance Optimized Good Optimized Re-renders
Dependencies 0 0 0 0

Developer Experience

  • Ultra-Simple API: Master everything in 5 minutes
  • Zero Boilerplate: No actions, reducers, or providers
  • TypeScript Native: Perfect type inference out of the box
  • Great DX: Intuitive create(), set(), get() functions

Comparison with Other Libraries

Feature Zustic Redux Zustand Context API
Bundle Size ~500B ~6KB ~2KB 0B
Learning Curve ⭐ Easy ⭐⭐⭐⭐⭐ Hard ⭐⭐ Easy ⭐⭐⭐ Medium
Boilerplate Minimal Massive Minimal Some
TypeScript Excellent Good Good Good
Store Middleware Built-in Required Optional No
Query System Built-in Separate Separate No
Caching Automatic Optional Optional No
API Simplicity Very Simple Complex Simple Medium

Quick Start

1. Create Your Store

import { create } from 'zustic';

type CounterStore = {
  count: number;
  inc: () => void;
  dec: () => void;
  reset: () => void;
};

export const useCounter = create<CounterStore>((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
  dec: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

2. Use in Your Component

import { useCounter } from './store';

function Counter() {
  const { count, inc, dec, reset } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={inc}> Increment</button>
      <button onClick={dec}> Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

export default Counter;

That's it! No providers, no boilerplate, just pure state management.


Core Concepts

Create a Store

The create function is the heart of Zustic:

const useStore = create<StoreType>((set, get) => ({
  // Your state and actions
}));
  • set: Update state (supports partial updates and functions)
  • get: Read current state (works outside components)

Reading State in Components

function Component() {
  // Subscribe to entire store
  const state = useStore();
  
  // Or subscribe to specific properties (optimized)
  const count = useStore((state) => state.count);
  
  return <div>{count}</div>;
}

Store Middleware System

Extend Zustic stores with powerful middleware for logging, persistence, validation, and more.

Logger Middleware

const logger = <T extends object>(): Middleware<T> => (set, get) => (next) => async (partial) => {
  console.log('Previous State:', get());
  await next(partial);
  console.log('New State:', get());
};

export const useStore = create<StoreType>(
  (set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
  }),
  [logger()]
);

Persistence Middleware

const persist = <T extends object>(): Middleware<T> => (set, get) => (next) => async (partial) => {
  await next(partial);
  localStorage.setItem('store', JSON.stringify(get()));
};

export const useStore = create<StoreType>(
  (set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
  }),
  [persist()]
);

Validation Middleware

const validate = <T extends object>(): Middleware<T> => (set, get) => (next) => async (partial) => {
  // Validate before updating
  if (typeof partial === 'object' && partial.count < 0) {
    console.warn('Invalid state update');
    return;
  }
  await next(partial);
};

export const useStore = create<StoreType>(
  (set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
  }),
  [validate()]
);

Multiple Middleware

export const useStore = create<StoreType>(
  (set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
  }),
  [logger(), persist(), validate()]
);

Query System (API Data Fetching)

Zustic now includes a powerful query system for managing API requests with automatic caching, mutations, middleware, and plugins.

Create an API

import { createApi } from 'zustic/query';

type User = {
  id: number;
  name: string;
  email: string;
};

const baseQuery = async (args: any) => {
  try {

    const response = await fetch(`https://api.example.com${args.url}`, {
      method: args.method || 'GET',
      headers: args.headers,
      body: args.body ? JSON.stringify(args.body) : undefined,
    });
    const data = await response.json();
    return { data };
  } catch (error) {
    return { error };
  }
};

export const api = createApi({
  baseQuery,
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => ({
        url: '/users',
        method: 'GET',
      }),
      transformResponse: (data) => data.map((u: User) => ({ ...u, name: u.name.toUpperCase() })),
    }),
    
    getUser: builder.query<User, { id: number }>({
      query: ({ id }) => ({
        url: `/users/${id}`,
        method: 'GET',
      }),
    }),
    
    createUser: builder.mutation<User, Omit<User, 'id'>>({
      query: (body) => ({
        url: '/users',
        method: 'POST',
        body,
      }),
      onSuccess: (data) => console.log('User created:', data),
      onError: (error) => console.error('Failed to create:', error),
    }),
    
    updateUser: builder.mutation<User, Partial<User>>({
      query: (body) => ({
        url: `/users/${body.id}`,
        method: 'PUT',
        body,
      }),
    }),
  }),
  cacheTimeout: 5 * 60 * 1000, // 5 minutes
});

Use Queries in Components

import { api } from './api';

function UsersList() {
  // Query hook automatically fetches on mount
  const { data: users, isLoading, isError, error, reFetch } = api.useUsersQuery();

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error?.message}</div>;

  return (
    <div>
      <h2>Users</h2>
      <button onClick={() => reFetch()}>Refresh</button>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Skip Queries (Conditional Fetching)

function UserDetail({ userId }: { userId?: number }) {
  // Only fetch when userId is provided
  const { data: user, isLoading } = api.useGetUserQuery(
    { id: userId! },
    { skip: !userId }
  );

  if (!userId) return <div>Select a user</div>;
  if (isLoading) return <div>Loading...</div>;

  return <div>{user?.name} ({user?.email})</div>;
}

Use Mutations in Components

function CreateUserForm() {
  // Mutation hook returns [mutate, state]
  const [createUser, { isLoading, isError, error, data, isSuccess }] = api.useCreateUserMutation();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    
    await createUser({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Creating...' : 'Create User'}
      </button>
      {isSuccess && <p User created!</p>}
      {isError && <p> Error: {error?.message}</p>}
    </form>
  );
}

Query API Features

Transformations

getUser: builder.query<User, { id: number }>({
  query: ({ id }) => ({
    url: `/users/${id}`,
    method: 'GET',
  }),
  // Transform the response data
  transformResponse: (data) => ({
    ...data,
    formattedDate: new Date(data.createdAt).toLocaleDateString(),
  }),
  // Transform errors
  transformError: (error) => ({
    message: error.message || 'Unknown error occurred',
    code: error.code,
  }),
  // Transform request body
  transformBody: (body) => ({
    ...body,
    timestamp: Date.now(),
  }),
  // Transform request headers
  transformHeader: (headers) => ({
    ...headers,
    'Authorization': `Bearer ${getToken()}`,
  }),
}),

Hooks and Callbacks

createUser: builder.mutation<User, CreateUserInput>({
  query: (body) => ({
    url: '/users',
    method: 'POST',
    body,
  }),
  onSuccess: async (data) => {
    console.log( 'User created:', data)
  },
  onError: async (error) => {
    console.error(' Failed:', error)
  },
}),

Automatic Caching

const api = createApi({
  baseQuery,
  endpoints: (builder) => ({
    getUser: builder.query<User, { id: number }>({
      query: ({ id }) => ({
        url: `/users/${id}`,
        method: 'GET',
      }),
    }),
  }),
  cacheTimeout: 10 * 60 * 1000, // Cache for 10 minutes
});

// First call: fetches from API
const result1 = useGetUserQuery({ id: 1 });

// Second call (within cache timeout): uses cached data
const result2 = useGetUserQuery({ id: 1 });

// Force refetch
const result3 = await reFetch();

Custom Query Function

getUserCustom: builder.query<User, { id: number }>({
  queryFnc: async (arg, baseQuery) => {
    // Implement custom logic
    const token = localStorage.getItem('token');
    return baseQuery({
      url: `/users/${arg.id}`,
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
    });
  },
}),

Query Middleware & Plugins

Query-Level Middleware

const requestLogger: ApiMiddleware = (ctx, next) => {
  console.log('Request:', ctx.arg);
  const result = await next();
  console.log('Response:', result);
  return result;
};

getUser: builder.query<User, { id: number }>({
  query: ({ id }) => ({
    url: `/users/${id}`,
    method: 'GET',
  }),
  middlewares: [requestLogger],
}),

Global Plugins

const authPlugin: ApiPlugin = {
  name: 'auth-plugin',
  
  beforeQuery: (ctx) => {
    const token = localStorage.getItem('token');
    if (!token) {
      throw new Error('Not authenticated');
    }
  },
  
  afterQuery: (result, ctx) => {
    if (result.error?.status === 401) {
      // Handle unauthorized
      window.location.href = '/login';
    }
  },
  
  onError: (error, ctx) => {
    console.error('API Error:', error);
  },
};

const api = createApi({
  baseQuery,
  endpoints: (builder) => ({
    // endpoints...
  }),
  plugins: [authPlugin],
});

Multi-Platform Examples

React Web

import { create } from 'zustic';

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function App() {
  const { count, inc } = useStore();
  return (
    <div>
      <p>{count}</p>
      <button onClick={inc}>Increment</button>
    </div>
  );
}

React Native

import { create } from 'zustic';
import { View, Text, Button } from 'react-native';

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function App() {
  const { count, inc } = useStore();
  return (
    <View>
      <Text>{count}</Text>
      <Button title="Increment" onPress={inc} />
    </View>
  );
}

Next.js

'use client';

import { create } from 'zustic';

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function Page() {
  const { count, inc } = useStore();
  return (
    <div>
      <p>{count}</p>
      <button onClick={inc}>Increment</button>
    </div>
  );
}

Testing

Zustic stores are easy to test:

import { create } from 'zustic';

// Your store
const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}));

// Test it
describe('Counter Store', () => {
  it('should increment count', () => {
    useStore.set({ count: 0 });
    useStore.get().inc();
    expect(useStore.get().count).toBe(1);
  });

  it('should reset count', () => {
    useStore.set({ count: 5 });
    useStore.get().reset();
    expect(useStore.get().count).toBe(0);
  });
});

Advanced Examples

Async State

const useUserStore = create((set, get) => ({
  user: null,
  loading: false,
  error: null,
  
  fetchUser: async (id: string) => {
    set({ loading: true });
    try {
      const response = await fetch(`/api/users/${id}`);
      const user = await response.json();
      set({ user, loading: false, error: null });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

Computed State

const useCartStore = create((set, get) => ({
  items: [],
  
  addItem: (item) => set((state) => ({
    items: [...state.items, item],
  })),
  
  get total() {
    return get().items.reduce((sum, item) => sum + item.price, 0);
  },
}));

Nested Stores

const useAuthStore = create((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

const useAppStore = create((set) => ({
  auth: useAuthStore,
  theme: 'light',
}));

Resources


API Reference

create<T>(initializer, middlewares?)

Creates a new store with state and actions.

Parameters:

  • initializer - Function that receives set and get, returns initial state
  • middlewares (optional) - Array of middleware functions

Returns:

  • A hook function that provides access to store state and actions

Example:

const useStore = create((set, get) => ({
  value: 0,
  increment: () => set((state) => ({ value: state.value + 1 })),
  getValue: () => get().value,
}));

Contributing

Contributions are welcome! Please feel free to submit a Pull Request to the GitHub repository.


License

MIT License © 2024 Rejaul Karim


Author

Created by Rejaul Karim - GitHub


Made with ❤️ for the React community

⭐ Star us on GitHub if you find this helpful!

About

A fast, minimal state management solution for React ecosystems. Works seamlessly with React, Next.js, and React Native, offering predictable state updates with a tiny footprint.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors