Skip to content

Commit

Permalink
feat: v5 (#152)
Browse files Browse the repository at this point in the history
* feat: v5

* breaking change from 18 and 19

* upgrade packages

* update read me

* update deps

* 4.9.0-next.0

* 5.0.0-next.0

* 5.0.0-next.1

* change payload to object shape

* remove option on should render

* keep that skip render option

* update readme
  • Loading branch information
bluebill1049 authored Jan 10, 2025
1 parent b1cd266 commit af5ac1f
Show file tree
Hide file tree
Showing 9 changed files with 3,154 additions and 3,291 deletions.
1 change: 0 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# These are supported funding model platforms

patreon: bluebill1049
github: [bluebill1049]
76 changes: 34 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,17 @@ State management made super simple

- Tiny with 0 dependency and simple (715B _gzip_)
- Persist state by default (`sessionStorage` or `localStorage`)
- Build with React Hooks
- Fine-tune the performance with partial render and selector

<h2>📦 Installation</h2>

$ npm install little-state-machine

<h2>🕹 API</h2>

#### 🔗 `StateMachineProvider`

This is a Provider Component to wrapper around your entire app in order to create context.

```tsx
<StateMachineProvider>
<App />
</StateMachineProvider>
```

#### 🔗 `createStore`

Function to initialize the global store, invoked at your app root (where `<StateMachineProvider />` lives).
Function to initialize the global store.

```tsx
function log(store) {
Expand All @@ -52,7 +42,6 @@ createStore(
name?: string; // rename the store
middleWares?: [ log ]; // function to invoke each action
storageType?: Storage; // session/local storage (default to session)

persist?: 'action' // onAction is default if not provided
// when 'none' is used then state is not persisted
// when 'action' is used then state is saved to the storage after store action is completed
Expand All @@ -66,22 +55,20 @@ createStore(
This hook function will return action/actions and state of the app.

```tsx
// Optional selector to ioslate re-render based selected state
const selector = state => state.data;

const { actions, state, getState } = useStateMachine<T>({
updateYourDetail,
});
}, selector);
```

<h2>📖 Example</h2>

Check out the <a href="https://codesandbox.io/s/wild-dawn-ud8bq">Demo</a>.
Check out the <a href="https://codesandbox.io/p/sandbox/compassionate-forest-ql3f56?workspaceId=ws_4xFLLpCJQLXZtvdkd1DS72">Demo</a>.

```tsx
import React from 'react';
import {
StateMachineProvider,
createStore,
useStateMachine,
} from 'little-state-machine';
import { createStore, useStateMachine } from 'little-state-machine';

createStore({
yourDetail: { name: '' },
Expand All @@ -97,20 +84,30 @@ function updateName(state, payload) {
};
}

function selector(state) {
return state.yourDetails.name.length > 10;
}

function YourComponent() {
const { actions, state } = useStateMachine({ updateName });
const { actions, state } = useStateMachine({ actions: { updateName } });

return (
<div onClick={() => actions.updateName({ name: 'bill' })}>
<buttton onClick={() => actions.updateName({ name: 'bill' })}>
{state.yourDetail.name}
</div>
</buttton>
);
}

function YourComponentSelectorRender() {
const { state } = useStateMachine({ selector });
return <p>{state.yourDetail.name]</p>;
}

const App = () => (
<StateMachineProvider>
<>
<YourComponent />
</StateMachineProvider>
<YourComponentSelectorRender />
</>
);
```

Expand All @@ -132,26 +129,21 @@ declare module 'little-state-machine' {
}
```

<h2>💁‍♂️ Tutorial</h2>
## ⌨️ Migrate to V5

Quick video tutorial on little state machine.
- `StateMachineProvider` has been removed, simple API

<a href="https://scrimba.com/scrim/ceqRebca">
<img src="https://raw.githubusercontent.com/bluebill1049/little-state-machine/master/docs/tutorial.png" />
</a>

<h2>⚒ DevTool</h2>

[DevTool](https://github.com/bluebill1049/little-state-machine-dev-tools) component to track your state change and action.

```tsx
import { DevTool } from 'little-state-machine-devtools';

<StateMachineProvider>
<DevTool />
</StateMachineProvider>;
```diff
const App = () => (
- <StateMachineProvider>
<YourComponent />
- <StateMachineProvider>
);
```

- Actions now is an object payload `useStateMachine({ actions: { updateName } })`
- Upgrade react >= 18

## By the makers of BEEKAI

We also make [BEEKAI](https://www.beekai.com/). Build the next-generation forms with modern technology and best in class user experience and accessibility.
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
"dist"
],
"version": "4.8.1",
"version": "5.0.0-next.1",
"main": "dist/little-state-machine.js",
"module": "dist/little-state-machine.es.js",
"unpkg": "dist/little-state-machine.umd.js",
Expand All @@ -28,19 +28,19 @@
"author": "<[email protected]>",
"license": "MIT",
"devDependencies": {
"@types/react": "^17.0.39",
"@types/react": "^19.0.0",
"jest": "27.5.0",
"microbundle": "^0.15.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rimraf": "^3.0.2",
"semantic-release": "^19.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.5.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
"react": "^18 || ^19"
}
}
33 changes: 0 additions & 33 deletions src/StateMachineContext.tsx

This file was deleted.

3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { StateMachineProvider } from './StateMachineContext';
import { createStore, useStateMachine } from './stateMachine';
import { GlobalState } from './types';

export { createStore, StateMachineProvider, useStateMachine, GlobalState };
export { createStore, useStateMachine, GlobalState };
17 changes: 17 additions & 0 deletions src/logic/storeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,28 @@ function StoreFactory() {
persist: PERSIST_OPTION.ACTION,
};
let state: GlobalState = {};
const listeners = new Set<() => void>();

const setState = (dispatchAction: ((payload: GlobalState) => GlobalState) | GlobalState) => {
state = typeof dispatchAction === 'function' ? dispatchAction(state) : state;

for (const listener of listeners) {
listener();
}
};

const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
};

try {
options.storageType =
typeof sessionStorage !== 'undefined' ? window.sessionStorage : undefined;
} catch {}

return {
subscribe,
updateStore(defaultValues: GlobalState) {
try {
state =
Expand All @@ -32,6 +47,8 @@ function StoreFactory() {
get state() {
return state;
},
getState: () => state,
setState,
set state(value) {
state = value;
},
Expand Down
44 changes: 37 additions & 7 deletions src/stateMachine.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from 'react';
import { useStateMachineContext } from './StateMachineContext';
import storeFactory from './logic/storeFactory';
import {
StateMachineOptions,
Expand Down Expand Up @@ -63,27 +62,58 @@ const actionTemplate =
export function useStateMachine<
TCallback extends AnyCallback,
TActions extends AnyActions<TCallback>,
>(
actions?: TActions,
): {
TStore,
>({
actions,
selector,
}: {
actions?: TActions;
selector?: ((payload: TStore) => TStore) | undefined;
} = {}): {
actions: ActionsOutput<TCallback, TActions>;
state: GlobalState;
getState: () => GlobalState;
} {
const { state, setState } = useStateMachineContext();
const actionsRef = React.useRef(
Object.entries(actions || {}).reduce(
(previous, [key, callback]) =>
Object.assign({}, previous, {
[key]: actionTemplate(setState, callback),
[key]: actionTemplate(storeFactory.setState, callback),
}),
{} as ActionsOutput<TCallback, TActions>,
),
);

const selectorRef = React.useRef(selector);
const previousSelectedStateRef = React.useRef<TStore | undefined>(undefined);

const getSnapshot = React.useCallback(() => {
const currentStore = storeFactory.getState();

if (!selectorRef.current) return currentStore;

const newSelectedState = selectorRef.current(currentStore as TStore);

const selectedStateHasChanged =
JSON.stringify(previousSelectedStateRef.current) !==
JSON.stringify(newSelectedState);

if (selectedStateHasChanged) {
previousSelectedStateRef.current = newSelectedState;
}

return previousSelectedStateRef.current;
}, []);

React.useSyncExternalStore(
storeFactory.subscribe,
getSnapshot,
() => undefined,
);

return {
actions: actionsRef.current,
state,
state: storeFactory.state,
getState: React.useCallback(() => storeFactory.state, []),
};
}
6 changes: 0 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PERSIST_OPTION } from './constants';
import * as React from 'react';

export interface GlobalState {}

Expand All @@ -17,11 +16,6 @@ export type ActionsOutput<
) => void;
};

export type StateMachineContextValue = {
state: GlobalState;
setState: React.Dispatch<React.SetStateAction<GlobalState>>;
};

export type MiddleWare = (
state: GlobalState,
payload: any,
Expand Down
Loading

0 comments on commit af5ac1f

Please sign in to comment.