tiny-slice 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Tiny Slice
2
+
3
+ A lightweight state management library for React.
4
+
5
+ The library internally uses React's `useReducer` which works standalone without a store.
6
+
7
+ This is not a replacement for Redux. It's a lightweight alternative for simple state management action/reducer.
8
+
9
+ ## Features
10
+
11
+ - Lightweight
12
+ - TypeScript support
13
+ - Async action handling (like Redux Thunks)
14
+ - Zero dependencies
15
+ - Debug mode
16
+
17
+ ## ExampleUsage
18
+
19
+ ```ts
20
+ import { createTinySlice, createAsyncAction, useTinySlice } from 'tiny-slice'
21
+
22
+ type CounterState = {
23
+ value: number
24
+ loading: boolean
25
+ }
26
+
27
+ const initialState: CounterState = {
28
+ value: 0,
29
+ loading: false,
30
+ }
31
+
32
+ const counterSlice = createTinySlice<CounterState>({
33
+ initialState,
34
+ actions: {
35
+ increment: (state) => {
36
+ state.value += 1
37
+ },
38
+ decrement: (state) => {
39
+ state.value -= 1
40
+ },
41
+ incrementAsync: createAsyncAction(async (state) => {
42
+ await new Promise((resolve) => setTimeout(resolve, 1000))
43
+ state.value += 1
44
+ }, {
45
+ onPending: (state) => {
46
+ state.loading = true
47
+ },
48
+ onSuccess: (state) => {
49
+ state.loading = false
50
+ },
51
+ }),
52
+ decrementAsync: createAsyncAction(async (state) => {
53
+ await new Promise((resolve) => setTimeout(resolve, 1000))
54
+ state.value -= 1
55
+ }, {
56
+ onPending: (state) => {
57
+ state.loading = true
58
+ },
59
+ onSuccess: (state) => {
60
+ state.loading = false
61
+ },
62
+ },
63
+ })
64
+
65
+ const App = () => {
66
+ const [state, actions] = useTinySlice(counterSlice)
67
+
68
+ return (
69
+ <div>
70
+ <p>{state.value} {state.loading ? 'Loading...' : ''}</p>
71
+ <button onClick={() => actions.increment()}>Increment</button>
72
+ <button onClick={() => actions.decrement()}>Decrement</button>
73
+ <button onClick={() => actions.incrementAsync()}>Increment Async</button>
74
+ <button onClick={() => actions.decrementAsync()}>Decrement Async</button>
75
+ </div>
76
+ )
77
+ }
78
+ ```
79
+
80
+ ## API
81
+
82
+ ### createTinySlice
83
+
84
+ Creates a tiny slice.
85
+
86
+ ```ts
87
+ const slice = createTinySlice<State, Actions>({
88
+ initialState,
89
+ actions,
90
+ })
91
+ ```
92
+
93
+ ### createAsyncAction
94
+
95
+ Creates an async action. Like in Redux Thunks, the async action will trigger the pending, success and error cases.
96
+
97
+ ```ts
98
+ const asyncAction = createAsyncAction(async (payload, { getState, dispatch }) => {
99
+ await new Promise((resolve) => setTimeout(resolve, 1000))
100
+ let value = getState().value
101
+ if (payload.type === 'increment') {
102
+ value += 1
103
+ } else if (payload.type === 'decrement') {
104
+ value -= 1
105
+ }
106
+ return { value }
107
+ }, {
108
+ onPending: (state) => {
109
+ state.loading = true
110
+ },
111
+ onSuccess: (state) => {
112
+ state.loading = false
113
+ },
114
+ onError: (state, error) => {
115
+ state.loading = false
116
+ state.error = error
117
+ },
118
+ })
119
+ ```
120
+
121
+ ### useTinySlice
122
+
123
+ A hook to use the slice state and dispatch actions.
124
+
125
+ ```ts
126
+ const [state, actions] = useTinySlice(slice)
127
+
128
+ return (
129
+ <div>
130
+ <p>{state.value}</p>
131
+ <button onClick={() => actions.increment()}>Increment</button>
132
+ </div>
133
+ )
134
+ ```
135
+
136
+
@@ -0,0 +1,54 @@
1
+ export type ActionPayload<T> = T;
2
+ type ActionReducer<State, Payload> = (state: State, payload: Payload) => void;
3
+ type ThunkAction<Result = void> = {
4
+ type: string;
5
+ payload: any;
6
+ async: true;
7
+ thunk: (dispatch: ThunkDispatch, getState: () => any) => Promise<Result>;
8
+ };
9
+ type Action = {
10
+ type: string;
11
+ payload: any;
12
+ async?: false;
13
+ };
14
+ type ThunkDispatch = <T = void>(action: Action | ThunkAction<T>) => Promise<T>;
15
+ type AsyncActionHelpers<State> = {
16
+ getState: () => State;
17
+ dispatch: ThunkDispatch;
18
+ };
19
+ type AsyncActionReducer<State, Payload, Result> = {
20
+ action: (payload: Payload, helpers: AsyncActionHelpers<State>) => Promise<Result>;
21
+ onSuccess?: (state: State, payload: Result) => void;
22
+ onPending?: (state: State) => void;
23
+ onError?: (state: State, payload: {
24
+ error: Error;
25
+ }) => void;
26
+ };
27
+ type ActionReducers<State> = Record<string, ActionReducer<State, any> | AsyncActionReducer<State, any, any>>;
28
+ interface SliceOptions<State, AR extends ActionReducers<State>> {
29
+ initialState: State;
30
+ actions: AR;
31
+ debug?: boolean;
32
+ }
33
+ type ActionCreator<T> = T extends ActionReducer<any, infer P> ? (payload?: P) => Action : T extends AsyncActionReducer<any, infer P, infer R> ? (payload?: P) => ThunkAction<R> : never;
34
+ type SliceActions<AR extends ActionReducers<any>> = {
35
+ [K in keyof AR]: ActionCreator<AR[K]>;
36
+ };
37
+ export declare function createAsyncAction<Payload, Result, State>(action: (payload: Payload, helpers: AsyncActionHelpers<State>) => Promise<Result>, handlers: {
38
+ onSuccess?: (state: State, payload: Result) => void;
39
+ onPending?: (state: State) => void;
40
+ onError?: (state: State, payload: {
41
+ error: Error;
42
+ }) => void;
43
+ }): AsyncActionReducer<State, Payload, Result>;
44
+ export declare function createTinySlice<State, AR extends ActionReducers<State>>(options: SliceOptions<State, AR>): {
45
+ readonly initialState: State;
46
+ readonly reducer: (state: State, action: {
47
+ type: string;
48
+ payload: any;
49
+ }) => State;
50
+ readonly reducers: AR;
51
+ readonly actions: SliceActions<AR>;
52
+ };
53
+ export declare function useTinySlice<State, AR extends ActionReducers<State>>(slice: ReturnType<typeof createTinySlice<State, AR>>): readonly [State, SliceActions<AR>];
54
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.createAsyncAction = createAsyncAction;
51
+ exports.createTinySlice = createTinySlice;
52
+ exports.useTinySlice = useTinySlice;
53
+ var react_1 = require("react");
54
+ // Helper function to create async actions
55
+ function createAsyncAction(action, handlers) {
56
+ return __assign({ action: action }, handlers);
57
+ }
58
+ // Create a tiny slice
59
+ function createTinySlice(options) {
60
+ var _this = this;
61
+ function reducer(state, action) {
62
+ var _a = action.type.split('/'), actionType = _a[0], status = _a[1];
63
+ var actionReducer = options.actions[actionType];
64
+ if (options.debug) {
65
+ console.group("Action: ".concat(action.type));
66
+ console.log('Payload:', JSON.stringify(action.payload, null, 2));
67
+ console.log('Previous State:', JSON.stringify(state, null, 2));
68
+ }
69
+ var newState = state;
70
+ if (actionReducer && 'action' in actionReducer) {
71
+ // Handle async action states
72
+ switch (status) {
73
+ case 'pending':
74
+ if (actionReducer.onPending) {
75
+ newState = __assign({}, state);
76
+ actionReducer.onPending(newState);
77
+ }
78
+ break;
79
+ case 'fulfilled':
80
+ if (actionReducer.onSuccess) {
81
+ newState = __assign({}, state);
82
+ actionReducer.onSuccess(newState, action.payload);
83
+ }
84
+ break;
85
+ case 'rejected':
86
+ if (actionReducer.onError) {
87
+ newState = __assign({}, state);
88
+ actionReducer.onError(newState, action.payload);
89
+ }
90
+ break;
91
+ }
92
+ }
93
+ else if (actionReducer) {
94
+ // Handle sync action
95
+ newState = __assign({}, state);
96
+ actionReducer(newState, action.payload);
97
+ }
98
+ if (options.debug) {
99
+ console.log('Next State:', JSON.stringify(newState, null, 2));
100
+ console.groupEnd();
101
+ }
102
+ return newState;
103
+ }
104
+ // Create action creators that work both in and out of hooks
105
+ var actions = {};
106
+ var _loop_1 = function (actionKey) {
107
+ var actionReducer = options.actions[actionKey];
108
+ if ('action' in actionReducer) {
109
+ actions[actionKey] = (function (payload) { return ({
110
+ type: actionKey,
111
+ payload: payload,
112
+ async: true,
113
+ thunk: function (dispatch, getState) { return __awaiter(_this, void 0, void 0, function () {
114
+ var result, error_1;
115
+ return __generator(this, function (_a) {
116
+ switch (_a.label) {
117
+ case 0:
118
+ _a.trys.push([0, 2, , 3]);
119
+ if (options.debug) {
120
+ console.group("Async Action Started: ".concat(actionKey));
121
+ console.log('Payload:', JSON.stringify(payload, null, 2));
122
+ console.log('Current State:', JSON.stringify(getState(), null, 2));
123
+ }
124
+ if (actionReducer.onPending) {
125
+ dispatch({
126
+ type: "".concat(actionKey, "/pending"),
127
+ payload: null,
128
+ });
129
+ }
130
+ return [4 /*yield*/, actionReducer.action(payload, {
131
+ getState: getState,
132
+ dispatch: dispatch,
133
+ })];
134
+ case 1:
135
+ result = _a.sent();
136
+ if (actionReducer.onSuccess) {
137
+ dispatch({
138
+ type: "".concat(actionKey, "/fulfilled"),
139
+ payload: result,
140
+ });
141
+ }
142
+ if (options.debug) {
143
+ console.log('Action Result:', JSON.stringify(result, null, 2));
144
+ console.groupEnd();
145
+ }
146
+ return [2 /*return*/, result];
147
+ case 2:
148
+ error_1 = _a.sent();
149
+ if (options.debug) {
150
+ console.log('Action Error:', error_1);
151
+ console.groupEnd();
152
+ }
153
+ if (actionReducer.onError) {
154
+ dispatch({
155
+ type: "".concat(actionKey, "/rejected"),
156
+ payload: { error: error_1 },
157
+ });
158
+ }
159
+ throw error_1;
160
+ case 3: return [2 /*return*/];
161
+ }
162
+ });
163
+ }); },
164
+ }); });
165
+ }
166
+ else {
167
+ actions[actionKey] = (function (payload) { return ({
168
+ type: actionKey,
169
+ payload: payload,
170
+ async: false,
171
+ }); });
172
+ }
173
+ };
174
+ for (var _i = 0, _a = Object.keys(options.actions); _i < _a.length; _i++) {
175
+ var actionKey = _a[_i];
176
+ _loop_1(actionKey);
177
+ }
178
+ return {
179
+ initialState: options.initialState,
180
+ reducer: reducer,
181
+ reducers: options.actions,
182
+ actions: actions,
183
+ };
184
+ }
185
+ // Hook to use the tiny slice
186
+ function useTinySlice(slice) {
187
+ var _this = this;
188
+ var _a = (0, react_1.useReducer)(slice.reducer, slice.initialState), state = _a[0], dispatch = _a[1];
189
+ var stateRef = (0, react_1.useRef)(state);
190
+ stateRef.current = state;
191
+ var actionsRef = (0, react_1.useCallback)(function () {
192
+ var actions = {};
193
+ var getState = function () { return stateRef.current; };
194
+ var enhancedDispatch = function (action) { return __awaiter(_this, void 0, void 0, function () {
195
+ return __generator(this, function (_a) {
196
+ if (action.async === true && 'thunk' in action) {
197
+ return [2 /*return*/, action.thunk(enhancedDispatch, getState)];
198
+ }
199
+ dispatch(action);
200
+ return [2 /*return*/, Promise.resolve()];
201
+ });
202
+ }); };
203
+ var _loop_2 = function (actionKey) {
204
+ var actionReducer = slice.reducers[actionKey];
205
+ if ('action' in actionReducer) {
206
+ // Handle async action
207
+ actions[actionKey] = (function (payload) { return __awaiter(_this, void 0, void 0, function () {
208
+ return __generator(this, function (_a) {
209
+ return [2 /*return*/, enhancedDispatch(slice.actions[actionKey](payload))];
210
+ });
211
+ }); });
212
+ }
213
+ else {
214
+ // Handle sync action
215
+ actions[actionKey] = (function (payload) {
216
+ return enhancedDispatch(slice.actions[actionKey](payload));
217
+ });
218
+ }
219
+ };
220
+ for (var _i = 0, _a = Object.keys(slice.reducers); _i < _a.length; _i++) {
221
+ var actionKey = _a[_i];
222
+ _loop_2(actionKey);
223
+ }
224
+ return actions;
225
+ }, [slice]);
226
+ return [state, actionsRef()];
227
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "tiny-slice",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight state management library for React",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepare": "npm run build",
13
+ "test": "jest",
14
+ "test:watch": "jest --watch",
15
+ "lint": "eslint . --ext .ts,.tsx",
16
+ "lint:fix": "eslint . --ext .ts,.tsx --fix",
17
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
18
+ },
19
+ "keywords": [
20
+ "react",
21
+ "state-management",
22
+ "typescript"
23
+ ],
24
+ "peerDependencies": {
25
+ "react": ">=16.8.0"
26
+ },
27
+ "devDependencies": {
28
+ "@testing-library/jest-dom": "^6.1.5",
29
+ "@testing-library/react": "^14.0.0",
30
+ "@types/jest": "^29.5.0",
31
+ "@types/react": "^18.2.0",
32
+ "@typescript-eslint/eslint-plugin": "^6.13.1",
33
+ "@typescript-eslint/parser": "^6.13.1",
34
+ "eslint": "^8.54.0",
35
+ "eslint-config-prettier": "^9.0.0",
36
+ "eslint-plugin-prettier": "^5.0.1",
37
+ "eslint-plugin-react": "^7.33.2",
38
+ "eslint-plugin-react-hooks": "^4.6.0",
39
+ "jest": "^29.7.0",
40
+ "jest-environment-jsdom": "^29.7.0",
41
+ "prettier": "^3.1.0",
42
+ "react": "^18.2.0",
43
+ "react-dom": "^18.2.0",
44
+ "react-test-renderer": "^18.2.0",
45
+ "ts-jest": "^29.2.5",
46
+ "typescript": "^5.0.0"
47
+ },
48
+ "license": "MIT"
49
+ }