Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v5 #152

Merged
merged 12 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading