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

Porting txstate #56

Merged
merged 46 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
44efc1a
feat: better types via txstate port
devanshj May 31, 2021
7a51b27
wip, start from scratch, feat: schema
devanshj Jun 21, 2021
83f1eb4
fix `Machine.EntryEventForStateValue`: honour top-level `on`
devanshj Jun 22, 2021
6a639c2
add tests for top-level on
devanshj Jun 22, 2021
d62f5a6
`on: null` instead of `_: null`
devanshj Jul 3, 2021
58246b9
show hint for single-functiona-property error
devanshj Jul 10, 2021
615b1a5
typestated `State`
devanshj Jul 11, 2021
4044e04
new schema, format
devanshj Jul 12, 2021
bd97115
update runtime tests
devanshj Jul 12, 2021
b25ace2
fix `ExitEventForStateValue`
devanshj Jul 15, 2021
91a79a7
fix some bugs, add twoslash-tester
devanshj Jul 25, 2021
c102335
delete genererated types.test.ts, add to gitignore
devanshj Jul 25, 2021
224f2ad
cherry-pick type changes from rewrite
devanshj Jul 26, 2021
c716aaa
unsolicited rewrite :P
devanshj Jul 12, 2021
d9faebe
Remove external logger file
Jul 23, 2021
2b4c859
Workaround to run TS 4 on TSDX
Jul 23, 2021
0392a24
Update src/extras.ts
cassiozen Jul 23, 2021
111dc15
Update src/index.tsx
cassiozen Jul 23, 2021
fcca7f3
Update src/index.tsx
cassiozen Jul 23, 2021
2e96e9e
Update src/index.tsx
cassiozen Jul 23, 2021
23304ee
Update src/index.tsx
cassiozen Jul 23, 2021
c796df4
Update src/extras.ts
cassiozen Jul 23, 2021
6e172ed
Update src/extras.ts
cassiozen Jul 23, 2021
3785fe6
Couple fixes
Jul 23, 2021
635d1b4
Change size limit
Jul 23, 2021
cb69a9d
Revert error
Jul 23, 2021
54c3ff5
More flexible prettier
Jul 23, 2021
a4ddab0
use symbols for key and value of `R`
devanshj Jul 25, 2021
0af7a7e
First pass of updates to Readme
Jul 25, 2021
c52fcd3
use double quotes for consistency
devanshj Jul 26, 2021
ced89b6
refactor the ternany fucked up by prettier
devanshj Jul 26, 2021
014b5e9
use ts intead of tsx
devanshj Jul 26, 2021
5859df3
refactor to make mockfree
devanshj Jul 26, 2021
ca34dd5
explicit initial event
devanshj Jul 26, 2021
5371cc0
alias `Foo.Impl` to `FooImpl` for better quickinfo
devanshj Jul 26, 2021
9b7f518
minor twoslash-tester fix
devanshj Jul 26, 2021
21acc05
Fixed bug with `nextEvents` coming from old node
Jul 26, 2021
c8320f2
Updated Timer Example
Jul 26, 2021
b24f94e
Updated Form Example
Jul 26, 2021
145db0c
Updated Fetch Example
Jul 26, 2021
c8305e1
Second pass of updates to Readme
Jul 26, 2021
a6d0513
Another pass of updates to Readme
Jul 26, 2021
84eab84
Fix identation
Jul 26, 2021
bb95c9f
intial event types, strict `t` usage
devanshj Jul 27, 2021
47132bc
remove debugging definition
devanshj Jul 27, 2021
9005faf
`nextEventsT` closes #62
devanshj Jul 27, 2021
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
extends: ['react-app', 'prettier/@typescript-eslint', 'plugin:prettier/recommended'],
extends: ['react-app', 'prettier/@typescript-eslint'],
settings: {
react: {
version: 'detect',
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
node_modules
.cache
dist
.parcel-cache
.parcel-cache
test/types.test.ts
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/types.ts
193 changes: 117 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
<img src="https://user-images.githubusercontent.com/33676/111815108-4695b900-88a9-11eb-8b61-3c45b40d4df6.png" width="250" alt=""/>
</p>

**The ½ kb _state machine_ hook for React:**
**The <1 kb _state machine_ hook for React:**

- Feature complete (Entry/exit callbacks, Guarded transitions & Extended State - Context)
- Heavy focus on type inference (you get auto completion for both TypeScript & JavaScript users without having to manually define the typings)
- Idiomatic React patterns (Since it's built on top of React's useReducer & useEffect, might as well...)

<img width="400" alt="size_badge" src="https://user-images.githubusercontent.com/33676/120556214-ce2b9800-c3c1-11eb-9a55-f9fa4e1fbbe4.png">
<img width="354" alt="size badge" src="https://user-images.githubusercontent.com/33676/126902516-51f46526-3023-43c7-afd4-17df2e89a3a1.png">

**This docs are for the 1.0.0 version (currently in beta). [Older 0.x.x docs](https://github.com/cassiozen/useStateMachine/tree/b2eea57d877d3b379aa2b86c5301ebbad7515fd9#readme)**

## Examples

- Examples Walkthrough video: [YouTube](https://youtu.be/1-cu8C8nCzE)
- Complex UI (Hiding and showing UI Elements based on the state) - [CodeSandbox](https://codesandbox.io/s/github/cassiozen/usestatemachine/tree/main/examples/timer?file=/index.tsx) - [Source](./examples/timer)
- State-driven UI (Hiding and showing UI Elements based on the state) - [CodeSandbox](https://codesandbox.io/s/github/cassiozen/usestatemachine/tree/main/examples/timer?file=/index.tsx) - [Source](./examples/timer)
- Async orchestration (Fetch data with limited retry) - [CodeSandbox](https://codesandbox.io/s/github/cassiozen/usestatemachine/tree/main/examples/fetch?file=/index.tsx) - [Source](./examples/fetch)
- Sending data with events (Form) - [CodeSandbox](https://codesandbox.io/s/github/cassiozen/usestatemachine/tree/main/examples/form?file=/index.tsx) - [Source](./examples/form)

Expand All @@ -28,7 +27,7 @@ $ npm install @cassiozen/usestatemachine
## Sample Usage

```typescript
const [state, send] = useStateMachine()({
const [state, send] = useStateMachine({
initial: 'inactive',
states: {
inactive: {
Expand Down Expand Up @@ -57,71 +56,71 @@ send('TOGGLE');
console.log(state); // { value: 'active', nextEvents: ['TOGGLE'] }
```

## What's up with the double parenthesis?

useStateMachine is a curried function (Yummm tasty!) because TypeScript doesn't yet support [partial generics type inference](https://github.com/microsoft/TypeScript/issues/14400).
This workaround allows TypeScript developers to provide a custom type for the context while still having TypeScript infer all the types used in the configuration (Like the state & transitions names, etc...).

# API

## useStateMachine
# useStateMachine

```typescript
const [state, send] = useStateMachine(/* Optional Context */)(/* Configuration */);
const [state, send] = useStateMachine(/* State Machine Definition */);
```

`useStateMachine` takes a JavaScript object as context (optional, see below) and one as the state machine configuration. It returns an array consisting of a `current machine state` object and a `send` function to trigger transitions.
`useStateMachine` takes a JavaScript object as the state machine definition. It returns an array consisting of a `current machine state` object and a `send` function to trigger transitions.

### Machine state
## Machine state

The `state` consists of 4 properties: `value`, `event`, `nextEvents` and `context`.

`value` (string): Returns the name of the current state.

`event` (eventObject: `{type: string}`; Optional): The name of the last sent event that led to this state.
`event` (`{type: string}`; Optional): The name of the last sent event that led to this state.

`nextEvents` (`string[]`): An array with the names of available events to trigger transitions from this state.

`context`: The state machine extended state. See "Extended State" below.

### Send events
## Send events

`send` takes an event as argument, provided in shorthand string format (e.g. "TOGGLE") or as an event object (e.g. `{ type: "TOGGLE" }`)

If the transition exists in the configuration object for that state, and is allowed (see guard), it will change the state machine state and execute effects.
If the current state accepts this event, and it is allowed (see guard), it will change the state machine state and execute effects.

## State Machine configuration
# State Machine definition

The configuration object should contain:
| Key | Required | Description |
| ----------- | ---- |----------- |
| verbose | | If true, will log every context & state changes. Log messages will be stripped out in the production build. |
| schema | | For usage with TypeScript only. Optional strongly-typed context & events. More on schema [below](#schema-context--event-typing) |
| context | | Extended state initial value. More on extended state [below](#extended-state-context) |
| initial | * | The initial state node this machine should be in |
| states | * | Define the possible finite states the state machine can be in. |

- initial: The initial state node this machine should be in
- verbose(optional): If true, will log every context & state changes. Log messages will be stripped out in the production build.
- states: Define each of the possible states:
## Defining States

```typescript
const [state, send] = useStateMachine()({
initial: 'inactive',
verbose: true,
states: {
inactive: {},
active: {},
},
});
```
A finite state machine can be in only one of a finite number of states at any given time. As an application is interacted with, events cause it to change state.

### Events & Transition Syntax
States are defined with the state name as a key and an object with two possible keys: `on` (which events this state responds to) and `effect` (run arbitrary code when entering or exiting this state):

A state transition defines what the next state is, given the current state and event. State transitions are defined on state nodes, in the `on` property:
### On (Events & transitions)

```js
on: {
TOGGLE: 'active';
}
Describes which events this state responds to (and to which other state the machine should transition to when this event is sent):

// (Where TOGGLE stands for an event name that will trigger a transition.)
```typescript
states: {
inactive: {
on: {
TOGGLE: 'active';
}
},
active: {
on: {
TOGGLE: 'inactive';
}
},
},
```

Or using the extended, object syntax, which allows for more control over the transition (like adding guards):
The event definition can also use the extended, object syntax, which allows for more control over the transition (like adding guards):

```js
on: {
Expand All @@ -131,12 +130,39 @@ on: {
};
```

#### Guards

Guards are functions that run before actually making the state transition: If the guard returns false the transition will be denied.

```js
const [state, send] = useStateMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
guard({ context, event }) {
// Return a boolean to allow or block the transition
},
},
},
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
```

The guard function receives an object with the current context and the event. The event parameter always uses the object format (e.g. `{ type: 'TOGGLE' }`).

### Effects (entry/exit callbacks)

Effects are triggered when the state machine enters a given state. If you return a function from your effect, it will be invoked when leaving that state (similarly to how useEffect works in React).

```typescript
const [state, send] = useStateMachine()({
const [state, send] = useStateMachine({
initial: 'active',
states: {
active: {
Expand All @@ -160,7 +186,7 @@ The effect function receives an object as parameter with four keys:
In this example, the state machine will always send the "RETRY" event when entering the error state:

```typescript
const [state, send] = useStateMachine()({
const [state, send] = useStateMachine({
initial: 'loading',
states: {
/* Other states here... */
Expand All @@ -176,41 +202,15 @@ const [state, send] = useStateMachine()({
});
```

### Guards

You can set up a guard per transition, using the transition object syntax. Guard run before actually running the transition: If the guard returns false the transition will be denied.

```js
const [state, send] = useStateMachine()({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
guard({ context, event }) {
// Return a boolean to allow or block the transition
},
},
},
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
```

The guard function receives an object with the current context and the event. The event parameter always uses the object format (e.g. `{ type: 'TOGGLE' }`).

### Extended state (context)
## Extended state (context)

Besides the finite number of states, the state machine can have extended state (known as context).

You can provide the initial context value as the first argument to the State Machine hook, and use the `setContext` function within your effects to change the context:
You can provide the initial context value in the state machine definition, then use the `setContext` function within your effects to change the context:

```js
const [state, send] = useStateMachine({ toggleCount: 0 })({
const [state, send] = useStateMachine({
context: { toggleCount: 0 },
initial: 'inactive',
states: {
inactive: {
Expand All @@ -232,12 +232,20 @@ send('TOGGLE');
console.log(state); // { context: { toggleCount: 1 }, value: 'active', nextEvents: ['TOGGLE'] }
```

#### Context Typing
## Schema: Context & Event Typing

TypeScript will automatically infer your context type; event types are generated automatically.

The context types are inferred automatically in TypeScript, but you can provide you own typing if you want to be more specific:
Still, there are situations where you might want explicit control over the `context` and `event` types: You can provide you own typing using the `t` whithin `schema`:

*Typed Context example*

```typescript
const [state, send] = useStateMachine<{ toggleCount: number }>({ toggleCount: 0 })({
const [state, send] = useStateMachine({
schema: {
context: t<{ toggleCount: number }>()
},
context: { toggleCount: 0 },
initial: 'inactive',
states: {
inactive: {
Expand All @@ -253,7 +261,40 @@ const [state, send] = useStateMachine<{ toggleCount: number }>({ toggleCount: 0
});
```

## Wiki
*Typed Events*

Typing the event inside schema is specially useful if you want to send arbitrary data with the event. Here's a quick example:

```typescript
const [machine, send] = useStateMachine({
schema: {
context: t<{ timeout?: number }>(),
events: {
PING: t<{ value: number }>()
}
},
context: {timeout: undefined},
initial: 'waiting',
states: {
waiting: {
on: {
PING: 'pinged'
}
},
pinged: {
effect({ setContext, event }) {
setContext(c => ({ timeout: event?.value ?? 0 }));
},
}
},
});

send({ type: 'PING', value: 150 })
```

More information about [Sending data with events](https://github.com/cassiozen/useStateMachine/wiki/Sending-data-with-Events).

# Wiki

- [Sending data with events](https://github.com/cassiozen/useStateMachine/wiki/Sending-data-with-Events)
- [Updating from version 0.x.x to 1.0](https://github.com/cassiozen/useStateMachine/wiki/Updating-from-0.X.X-to-1.0.0)
Expand All @@ -262,7 +303,7 @@ const [state, send] = useStateMachine<{ toggleCount: number }>({ toggleCount: 0
- [Source code walkthrough video](https://github.com/cassiozen/useStateMachine/wiki/Source-code-walkthrough-video)
- [Usage with Preact](https://github.com/cassiozen/useStateMachine/wiki/Usage-with-Preact)

## Contributors ✨
# Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

Expand Down
21 changes: 20 additions & 1 deletion examples/fetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ To run this example:

## Local Development

For local development, add this section to `package.json` and run `npm start` on both the root library and on this example folder:
For local development, you'll have to update `package.json` and `tsconfig.json`:

**package.json**

1. Remove everything but "react-dom" from `dependencies`

2. Add an `alias` section pointing to usestatemachine in the dist folder and React from the main node_modules.

```json
"alias": {
Expand All @@ -16,3 +22,16 @@ For local development, add this section to `package.json` and run `npm start` on
"@cassiozen/usestatemachine": "../../dist"
},
```

**tsconfig.json**

1. Add a `paths` section pointing to usestatemachine in the dist folder.

```json
"paths": {
"@cassiozen/usestatemachine": ["../../dist/index"],
},
```


Finally, run `npm start` on both the root library and on this example folder.
8 changes: 6 additions & 2 deletions examples/fetch/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import useStateMachine from '@cassiozen/usestatemachine';
import useStateMachine, {t} from '@cassiozen/usestatemachine';
import './index.css';
import Cup from './Cup';

Expand All @@ -16,7 +16,11 @@ type Coffee = {
};

function App() {
const [machine, send] = useStateMachine<{ retryCount: number; data?: Coffee[]; error?: string }>({ retryCount: 0 })({
const [machine, send] = useStateMachine({
schema: {
context: t<{ retryCount: number; data?: Coffee[]; error?: string }>()
},
context: { retryCount: 0 },
initial: 'loading',
verbose: true,
states: {
Expand Down
2 changes: 1 addition & 1 deletion examples/fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "parcel build index.html"
},
"dependencies": {
"@cassiozen/usestatemachine": "^1.0.0-beta.1",
"@cassiozen/usestatemachine": "^1.0.0-beta.2",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
Expand Down
Loading