diff --git a/.gitbook/assets/image (1) (1) (1).png b/.gitbook/assets/image (1) (1) (1).png new file mode 100644 index 0000000..00b003f Binary files /dev/null and b/.gitbook/assets/image (1) (1) (1).png differ diff --git a/.gitbook/assets/image (1) (1).png b/.gitbook/assets/image (1) (1).png new file mode 100644 index 0000000..00b003f Binary files /dev/null and b/.gitbook/assets/image (1) (1).png differ diff --git a/.gitbook/assets/image (1).png b/.gitbook/assets/image (1).png new file mode 100644 index 0000000..00b003f Binary files /dev/null and b/.gitbook/assets/image (1).png differ diff --git a/README.md b/README.md index 42869bd..3fdf705 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ description: frictionless state management > Web application development is about **defining**, **changing** and **consuming state** to produce a user experience. Overmind aims for a developer experience where that is all you focus on, reducing the orchestration of state management to a minimum. Making you a **happier** and more **productive** developer! -{% embed url="https://overmindjs.changefeed.app/general/v22.0.0" %} +{% embed url="https://overmindjs.changefeed.app/general/v26" caption="" %} ## APPLICATION INSIGHT @@ -16,7 +16,7 @@ Develop the application state, effects and actions without leaving [VS Code](htt ## A SINGLE STATE TREE -Building your application with a single state tree is the most straight forward mental model. You get a complete overview, but can still organize the state by namespacing it into domains. The devtools allows you to edit and mock out state. +Building your application with a single state tree is the most straight forward mental model. You get a complete overview, but can still organize the state by namespacing it into domains. This gives you the benefit of being able to explore all the state of your application from a single point. With Typescript it is even documented. The devtools allows you to edit and mock out state. ```typescript { @@ -57,7 +57,7 @@ export const loadApp = ({ state, effects }) => { ## SAFE AND PREDICTABLE CHANGES -When you build applications that perform many state changes things can get out of hand. In Overmind you can only perform state changes from **actions** and all changes are tracked by the development tool. +When you build applications that perform many state changes things can get out of hand. In Overmind you can only perform state changes from **actions** and all changes are tracked by the development tool. Even effects are tracked and reactions are tracked. ```javascript export const getItems = async ({ state, effects }) => { @@ -69,7 +69,7 @@ export const getItems = async ({ state, effects }) => { ## COMPLEXITY TOOLS -Even though Overmind can create applications with only plain **state** and **actions**, you can use **opt-in** tools like **functional operators**, **statecharts, statemachines** and state values defined as a **class,** to manage complexities of your application. +Even though Overmind can create applications with plain **state** and **actions**, you can use **opt-in** tools like **functional operators**,**, statemachines** and state values defined as a **class,** to manage complexities of your application. {% tabs %} {% tab title="Operators" %} @@ -89,54 +89,18 @@ export const search = pipe( ``` {% endtab %} -{% tab title="Statechart" %} -```javascript -const loginChart = { - initial: 'LOGIN', - states: { - LOGIN: { - on: { - changeUsername: null, - changePassword: null, - login: 'AUTHENTICATING' - } - }, - AUTHENTICATING: { - on: { - resolveUser: 'AUTHENTICATED', - rejectUser: 'ERROR' - } - }, - AUTHENTICATED: { - on: { - logout: 'LOGIN' - } - }, - ERROR: { - on: { - tryAgain: 'LOGIN' - } - } - } -} -``` -{% endtab %} - {% tab title="Statemachines" %} ```typescript -export const state = { - mode: statemachine({ - initial: 'unauthenticated', - states: { - unauthenticated: ['authenticating'], - authenticating: ['unauthenticated', 'authenticated'], - authenticated: ['unauthenticating'], - unauthenticating: ['unauthenticated', 'authenticated'] - } - }), +export const state = statemachine({ + unauthenticated: ['authenticating'], + authenticating: ['unauthenticated', 'authenticated'], + authenticated: ['unauthenticating'], + unauthenticating: ['unauthenticated', 'authenticated'] +}, { + state: 'unauthenticated', user: null, error: null -} +}) ``` {% endtab %} diff --git a/SUMMARY.md b/SUMMARY.md index 62acb64..d904dfb 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -15,7 +15,6 @@ * [Actions](core/writing-application-logic.md) * [Effects](core/running-side-effects.md) * [Operators](core/going-functional.md) -* [Statecharts](core/statecharts.md) * [Server Side Rendering](core/server-side-rendering.md) * [Typescript](core/typescript.md) @@ -24,19 +23,22 @@ * [React](views/react.md) * [Angular](views/angular.md) * [Vue](views/vue.md) +* [Svelte](views/svelte.md) ## Addons * [GraphQL](addons/graphql.md) +* [Statechart](addons/statecharts.md) ## Guides +* [Using state machines](guides-1/using-state-machines.md) * [Connecting components](guides-1/connecting-components.md) -* [Connecting to React Native](https://dev.to/brasilikum/how-to-setup-overmind-with-react-native-expo-optional-4mk5) * [Managing lists](guides-1/managing-lists.md) * [State first routing](guides-1/state-first-routing.md) * [Move to Typescript](guides-1/move-to-typescript.md) * [Testing](guides-1/testing.md) +* [Connecting to React Native](https://dev.to/brasilikum/how-to-setup-overmind-with-react-native-expo-optional-4mk5) ## API @@ -46,8 +48,9 @@ * [createOvermind](api-1/createovermind.md) * [createOvermindMock](api-1/createovermindmock.md) * [createOvermindSSR](api-1/createovermindssr.md) -* [derive](api-1/derive.md) +* [derived](api-1/derive.md) * [effects](api-1/effects.md) +* [events](api-1/events.md) * [json](api-1/json.md) * [lazy](api-1/lazy.md) * [merge](api-1/merge.md) @@ -56,5 +59,5 @@ * [operators](api-1/operators.md) * [reaction](api-1/reaction.md) * [rehydrate](api-1/rehydrate.md) -* [statecharts](api-1/statecharts.md) +* [statemachine](api-1/statemachine.md) diff --git a/addons/graphql.md b/addons/graphql.md index 892dce9..f01c2db 100644 --- a/addons/graphql.md +++ b/addons/graphql.md @@ -6,10 +6,6 @@ Using Graphql with Overmind gives you the following benefits: * **Cache:** You integrate the data from Graphql with your existing state, allowing you to control when new data is needed * **Optimistic updates:** With the data integrated with your Overmind state you can also optimistically update that state before running a mutation query -{% hint style="info" %} -The Graphql package does not support **subscriptions** currently -{% endhint %} - ## Get up and running Install the separate package: @@ -20,7 +16,7 @@ npm install overmind-graphql ### Initial state -The Graphql package is a _configuration factory_. That means you need some existing configuration before going: +The Graphql package is an _effect_. Though since we are operating on state, let us prepare some: {% tabs %} {% tab title="overmind/index.js" %} @@ -35,7 +31,6 @@ export const config = { {% tab title="overmind/state.js" %} ```typescript - export const state = { posts: [] } @@ -43,29 +38,57 @@ export const state = { {% endtab %} {% endtabs %} -### The factory +### The effect -Now let us introduce the factory: +Now let us introduce the effect: {% tabs %} {% tab title="overmind/index.js" %} ```typescript +import { state } from './state' +import { onInitialize } from './onInitialize' +import { gql } from './effects/gql' + +export const config = { + onInitialize, + state, + effects: { + gql + } +} +``` +{% endtab %} + +{% tab title="overmind/onInitialize.js" %} +```javascript +export const onInitialize = ({ effects }) => { + effects.gql.initialize({ + // query and mutation options + endpoint: 'http://some-endpoint.dev', + }, { + // subscription options + endpoint: 'ws://some-endpoint.dev', + }) +} +``` +{% endtab %} + +{% tab title="overmind/effects/gql/index.js" %} +```javascript import { graphql } from 'overmind-graphql' import * as queries from './queries' import * as mutations from './mutations' -import { state } from './state' +import * as subscriptions from './subscriptions' -export const config = graphql({ - state -}, { - endpoint: 'http://some-endpoint.dev', +export const gql = graphql({ queries, - mutations + mutations, + subscriptions }) ``` {% endtab %} -{% tab title="overmind/queries.js" %} +{% tab title="overmind/effects/gql/queries.js" %} ```typescript import { gql } from 'overmind-graphql' @@ -80,7 +103,7 @@ export const posts = gql` ``` {% endtab %} -{% tab title="overmind/mutations.js" %} +{% tab title="overmind/effects/gql/mutations.js" %} ```typescript import { gql } from 'overmind-graphql' @@ -93,60 +116,82 @@ export const createPost = gql` ` ``` {% endtab %} + +{% tab title="overmind/effects/gql/subscriptions.js" %} +```javascript +import { gql } from 'overmind-graphql' + +export const onPostAdded = gql` + subscription PostAdded() { + postAdded() { + id + title + } + } +` +``` +{% endtab %} {% endtabs %} -You define **queries** and **mutations** as part of the second argument to the factory, with what **endpoint** you want to connect to. These queries and mutations are converted into Overmind effects that you can call from your actions. +You define **queries,** **mutations** and **subscriptions** with the effect. That means you can have multiple effects holding different queries and even endpoints. The endpoints are defined when you initialize the effect. This allows you to dynamically create the endpoints based on state, and also pass state related to requests to the endpoints. The queries, mutations and subscriptions are converted into Overmind effects that you can call from your actions. ## Query To call a query you will typically use an action. Let us create an action that uses our **posts** query. {% tabs %} -{% tab title="overmind/index.js" %} +{% tab title="overmind/actions.js" %} ```typescript -import { graphql } from 'overmind-graphql' -import * as actions from './actions' -import * as queries from './queries' -import * as mutations from './mutations' -import { state } from './state' +export const getPosts = async ({ state, effects }) => { + const { posts } = await effects.gql.queries.posts() -export const config = graphql({ - state, - actions -}, { - endpoint: 'http://some-endpoint.dev', - queries, - mutations -}) + state.posts = posts +} ``` {% endtab %} +{% endtabs %} + +## Mutate + +Mutation queries are basically the same as normal queries. You would typically also call these from an action. +{% tabs %} {% tab title="overmind/actions.js" %} ```typescript export const getPosts = async ({ state, effects }) => { - const { posts } = await effects.queries.posts() + const { posts } = await effects.gql.queries.posts() state.posts = posts } + +export const addPost = async ({ effects }, title) => { + await effects.gql.mutations.createPost({ title }) +} ``` {% endtab %} {% endtabs %} -## Mutate +## Subscription -Mutation queries are basically the same as normal queries. You would typically also call these from an action. +Subscriptions are also available via actions. You typically give them an action which triggers whenever the subscription triggers. {% tabs %} {% tab title="overmind/actions.js" %} ```typescript -export const getPosts = async ({ state, effects }) => { - const { posts } = await effects.queries.posts() +export const getPosts = async ({ state, effects, actions }) => { + const { posts } = await effects.gql.queries.posts() state.posts = posts + + effects.gql.subscriptions.onPostAdded(actions.onPostAdded) } export const addPost = async ({ effects }, title) => { - await effects.mutations.createPost({ title }) + await effects.gql.mutations.createPost({ title }) +} + +export const onPostAdded = ({ state }, post) => { + state.posts.push(post) } ``` {% endtab %} @@ -193,58 +238,74 @@ There are two points of options in the Graphql factory. The **headers** and the The headers option is a function which receives the state of the application. That means you can produce request headers dynamically. This can be useful related to authentciation. {% tabs %} -{% tab title="overmind/index.js" %} +{% tab title="overmind/onInitialize.js" %} ```typescript -import { graphql } from 'overmind-graphql' -import * as queries from './queries' -import * as mutations from './mutations' -import { state } from './state' +export const onInitialize = ({ state, effects }) => { + effects.gql.initialize({ + endpoint: 'http://some-endpoint.dev', + // This runs on every request + headers: () => ({ + authorization: `Bearer ${state.auth.token}` + }), + // The options are the options passed to GRAPHQL-REQUEST + options: { + credentials: 'include', + mode: 'cors', + }, + }, { + endpoint: 'ws://some-endpoint.dev', + // This runs on every connect + params: () => ({ + token: state.auth.token + }) + }) +} +``` +{% endtab %} +{% endtabs %} -export const config = graphql({ - state -}, { - endpoint: 'http://some-endpoint.dev', - headers: (state) => ({ - authorization: `Bearer ${state.auth.token}` - }), - queries, - mutations -}) +## Custom subscription socket + +If you want to define your own socket for connecting to subscriptions, a function can be used instead: + +{% tabs %} +{% tab title="overmind/onInitialize.js" %} +```javascript +export const onInitialize = ({ effects }) => { + effects.gql.initialize( + { + endpoint: 'http://some-endpoint.dev', + }, + () => new Websocket('ws://some-other-endpoint.dev') + ) +} ``` {% endtab %} {% endtabs %} -The options are the options passed to [GRAPHQL-REQUEST](https://github.com/prisma-labs/graphql-request). +## Disposing subscriptions + +You can dispose any subscriptions in any action. There are two ways to dispose: {% tabs %} -{% tab title="overmind/index.js" %} +{% tab title="overmind/actions.js" %} ```typescript -import { graphql } from 'overmind-graphql' -import * as queries from './queries' -import * as mutations from './mutations' -import { state } from './state' - -export const config = graphql({ - state -}, { - endpoint: 'http://some-endpoint.dev', - headers: (state) => ({ - authorization: `Bearer ${state.auth.token}` - }), - options: { - credentials: 'include', - mode: 'cors', - }, - queries, - mutations -}) +export const disposeSubscriptions = async ({ state, effects }) => { + // Disposes all subscriptions on "onPostAdded" + effects.gql.subscriptions.onPostAdded.dispose() + // If the subscription takes a payload, you can dispose specific + // subscriptions + effects.gql.subscriptions.onPostChange.disposeWhere( + data => data.id === state.currentPostId + ) +} ``` {% endtab %} {% endtabs %} ## Typescript -There is only a single type exposed by the library, **Query**. It is used for both queries and mutations. +There is only a single type exposed by the library, **Query**. It is used for queries, mutations and subscriptions. {% tabs %} {% tab title="overmind/queries.ts" %} @@ -298,7 +359,7 @@ Now you can create a script in your **package.json** file that looks something l ```typescript { "scripts": { - "schema": "apollo schema:download --endpoint=http://some-endpoint.dev graphql-schema.json && apollo codegen:generate --localSchemaFile=graphql-schema.json --target=typescript --includes=src/overmind/**/*.ts --tagName=gql --no-addTypename --globalTypesFile=src/overmind/graphql-global-types.ts graphql-types" + "schema": "apollo schema:download --header='X-Hasura-Admin-Secret: password' --endpoint=http://some-endpoint.dev graphql-schema.json && apollo codegen:generate --localSchemaFile=graphql-schema.json --target=typescript --includes=src/overmind/**/*.ts --tagName=gql --no-addTypename --globalTypesFile=src/overmind/graphql-global-types.ts graphql-types" } } ``` diff --git a/addons/statecharts.md b/addons/statecharts.md new file mode 100644 index 0000000..0c4457f --- /dev/null +++ b/addons/statecharts.md @@ -0,0 +1,808 @@ +# Statechart + +{% hint style="info" %} +Before you dive into statecharts it can be a good idea to explore [**statemachines**](../core/defining-state.md#statemachines). These are lower level and more flexible and can in most situations be exactly what you need. +{% endhint %} + +Just like [OPERATORS](../core/going-functional.md) is a declarative abstraction over plain actions, **statecharts** is a declarative abstraction over an Overmind configuration of **state** and **actions**. That means you will define your charts by: + +```typescript +const configWithStatechart = statechart(config, chart) +``` + +There are several benefits to using statecharts: + +1. You will have a declarative description of what actions should be available in certain states of the application +2. Less bugs because an invalid action will not be executed if called +3. You will be able to implement and test an interaction flow without building the user interface for it +4. Your state definition is cleaned up as your **isLoading** types of state is no longer needed +5. You have a tool to do “top down” implementation instead of “bottom up” + +You can basically think of a statechart as a way of limiting what actions are available to be executed in certain states of the application. This concept is very old and was originally used to design machines where the user was exposed to all points of interaction, all buttons and switches, at any time. Statecharts would help make sure that at certain states certain buttons and switches would not operate. + +A simple example of this is a Walkman. When the Walkman is in a **playing** state you should not be able to hit the **eject** button. On the web this might seem unnecessary as points of interaction is dynamic. We simply hide and/or disable buttons. But this is the exact problem. It is fragile. It is fragile because the UI implementation itself is all you depend on to prevent logic from running when it should not. A statechart is a much more resiliant way to ensure what logic can actually run in any given state. + +In Overmind we talk about these statechart states as **transition states**. + +## Get up and running + +Install the separate package: + +```text +npm install overmind-statechart +``` + +## Defining a statechart + +Let us imagine that we have a login flow. This login flow has 4 different **transition states**: + +1. **LOGIN**. We are at the point where the user inserts a username and password +2. **AUTHENTICATING**. The user has submitted +3. **AUTHENTICATED**. The user has successfully logged in +4. **ERROR**. Something wrong happened + +Let us do this properly and design this flow “top down”: + +{% tabs %} +{% tab title="overmind/login/index.js" %} +```typescript +import { statechart } from 'overmind-statechart' +import * as actions from './actions' +import { state } from './state' + +const config = { + state, + actions +} + +const loginChart = { + initial: 'LOGIN', + states: { + LOGIN: { + on: { + changeUsername: null, + changePassword: null, + login: 'AUTHENTICATING' + } + }, + AUTHENTICATING: { + on: { + resolveUser: 'AUTHENTICATED', + rejectUser: 'ERROR' + } + }, + AUTHENTICATED: { + on: { + logout: 'LOGIN' + } + }, + ERROR: { + on: { + tryAgain: 'LOGIN' + } + } + } +} + +export default statechart(config, loginChart) +``` +{% endtab %} +{% endtabs %} + +As you can see we have defined what transition states our login flow can be in and what actions we want available to us in each transition state. If the action points to **null** it means we stay in the same transition state. If it points to an other transition state, the execution of that action will cause that transition to occur. + +Since our initial state is **LOGIN**, a call to actions defined in the other transition states would simply be ignored. + +{% hint style="info" %} +You might expect actions to throw an error if they are called, but not allowed to do so. This is not the case with statecharts. During development you will get a warning when this happens, but in production absolutely nothing happens. Hitting a submit button multiple times might be perfectly okay, but after the first submit the chart moves to a new state, preventing any further execution of logic on the following submits. +{% endhint %} + +## Transitions + +If you are familiar with the concept of statemachines you might ask the question: _“Where are the transitions?”_. In Overmind we use actions to define transitions instead of having explicit transition types. That means you think about statecharts in Overmind as: + +```text +TRANSITION STATE -> ACTION -> NEW TRANSITION STATE +``` + +as opposed to: + +```text +TRANSITION STATE -> TRANSITION TYPE -> { NEW TRANSITION STATE, ACTION } +``` + +This approach has three benefits: + +1. It is more explicit in the definition that a transition state configures what actions are available +2. When typing your application the actions already has a typed input, which would not be possible with a generic **transition** action +3. It is simpler concept both in code and for your brain + +What to take notice of is that the **action** causing the transition is run before the transition actually happens. That means the action runs in the context of the current transition state and any synchronous calls to another action will obey its rules. If the action does something asynchronous, like doing an HTTP request, the transition will be performed and the asynchronous logic will run in the context of the new transition state. + +```typescript +const myTransitionAction = async ({ actions }) => { + // I am still in the current transition state + actions.someOtherAction() + + await Promise.resolve() + + // I am in the new transition state + actions.someOtherAction() +} +``` + +## Nested statecharts + +With a more complicated UI we can create nested statecharts. An example of this would be a workspace UI with different tabs. You only want to allow certain actions when the related tab is active. Let us explore an example: + +{% tabs %} +{% tab title="overmind/dashboard/index.js" %} +```typescript +import { statechart } from 'overmind-statechart' +import * as actions from './actions' +import { state } from './state' + +const config = { + state, + actions +} + +const issuesChart = { + initial: 'LOADING', + states: { + LOADING: { + entry: 'fetchIssues', + exit: 'abortFetchIssues', + on: { + resolveIssues: 'LIST', + rejectIssues: 'ERROR' + } + }, + LIST: { + on: { + toggleIssueCompleted: null + } + }, + ERROR: { + on: { + retry: 'LOADING' + } + }, + } +} + +const projectsChart = { + initial: 'LOADING', + states: { + LOADING: { + entry: 'fetchProjects', + exit: 'abortFetchProjects', + on: { + resolveIssues: 'LIST', + rejectIssues: 'ERROR' + } + }, + LIST: { + on: { + expandAttendees: null + } + }, + ERROR: { + on: { + retry: 'LOADING' + } + }, + } +} + +const dashboardChart = { + initial: 'ISSUES', + states: { + ISSUES: { + on: { + openProjects: 'PROJECTS' + }, + chart: issuesChart + }, + PROJECTS: { + on: { + openIssues: 'ISSUES' + }, + chart: projectsChart + } + } +} + +export default statechart(config, dashboardChart) +``` +{% endtab %} +{% endtabs %} + +What to take notice of in this example is that all chart states has its own **chart** property, which allows them to be nested. The nested charts has access to the same actions and state as the parent chart. + +In this example we also took advantage of the **entry** and **exit** hooks of a transition state. These also points to actions. When a transition is made into the transition state, the **entry** will run. This behavior is nested. When an **exit** hook exists and a transition is made away from the transition state, it will also run. This behavior is also nested of course. + +## Parallel statecharts + +It is also possible to define your charts in a parallel manner. You do this by simply using an object of keys where the key represents an ID of the chart. The **chart** property on a transition state allows the same. Either a single chart or an object of multiple charts where the key represents an ID of the chart. + +```typescript +export default statechart(config, { + issues: issuesChart, + projects: projectsChart +}) +``` + +## Conditions + +In our chart above we let the user log in even though there is no **username** or **password**. That seems a bit silly. In statecharts you can define conditions. These conditions receives the state of the configuration and returns true or false. + +{% tabs %} +{% tab title="overmind/login/index.js" %} +```typescript +import { statechart } from 'overmind-statechart' +import * as actions from './actions' +import { state } from './state' + +const config = { + state, + actions +} + +const loginChart = { + initial: 'LOGIN', + states: { + LOGIN: { + on: { + changeUsername: null, + changePassword: null, + login: { + target: 'AUTHENTICATING', + condition: state => Boolean(state.username && state.password) + } + } + }, + ... + } +} + +export default statechart(config, loginChart) +``` +{% endtab %} +{% endtabs %} + +Now the **login** action can only be executed when there is a username and password inserted, causing a transition to the new transition state. + +## State + +Our initial state defined for this configuration is: + +{% tabs %} +{% tab title="overmind/login/state.js" %} +```typescript +export const state = { + username: '', + password: '', + user: null, + authenticationError: null +} +``` +{% endtab %} +{% endtabs %} + +As you can see we have no state indicating that we have received an error, like **hasError**. We do not have **isLoggingIn** either. There is no reason, because we have our transition states. That means the configuration is populated with some additional state by the statechart. It will actually look like this: + +```typescript +{ + username: '', + password: '', + user: null, + authenticationError: null, + states: [['CHART', 'LOGIN']], + actions: { + changeUsername: true, + changePassword: true, + login: false, + logout: false, + tryAgain: false + } +} +``` + +The **states** state is the current transition states. It is defined as an array of arrays. This indicates that we can have parallel and nested charts. The **CHART** symbol in the array indicates that you have defined an immediate chart. If you rather defined parallel charts you would define your own ids. + +The **actions** state is a derived state. That means it automatically updates based on the current state of the chart. This is helpful for your UI implementation. It can use it to disable buttons etc. to help the user understand when certain actions are possible. + +### Identifying states + +There is also a third derived state called **matches**. This derived state returns a function that allows you to figure out what state you are in. This is also the API you use in your components to identify the state of your application:[EDIT ON GITHUB](https://github.com/cerebral/overmind/edit/next/packages/overmind-website/examples/guide/statecharts/matches.ts.ts) + +```typescript +state.login.matches({ + LOGIN: true +}) +``` + +You can also do more complex matches related to parallel and nested charts:[EDIT ON GITHUB](https://github.com/cerebral/overmind/edit/next/packages/overmind-website/examples/guide/statecharts/matches_multiple.ts.ts) + +```typescript +// Nested +const isSearching = state.dashboard.matches({ + LIST: { + search: { + SEARCHING: true + } + } +}) + +// Parallel +const isDownloadingAndUploading = state.files.matches({ + download: { + LOADING: true + }, + upload: { + LOADING: true + } +}) + +// Complex match +const isOnlyDownloading = state.files.matches({ + download: { + LOADING: true + }, + upload: { + LOADING: false + } +}) +``` + +### Actions + +Our actions are defined something like: + +{% tabs %} +{% tab title="overmind/login/actions.js" %} +```typescript +export const changeUsername = ({ state }, username) => { + state.login.username = username +} + +export const changePassword = ({ state }, password) => { + state.login.password = password +} + +export const login = ({ state, actions, effects }) => { + try { + const user = await effects.api.login(state.username, state.password) + actions.login.resolveUser(user) + } catch (error) { + actions.login.rejectUser(error) + } +} + +export const resolveUser = ({ state }, user) => { + state.login.user = user +} + +export const rejectUser = ({ state }, error) => { + state.login.authenticationError = error.message +} + +export const logout = ({ effects }) => { + effects.api.logout() +} + +export const tryAgain = () => {} +``` +{% endtab %} +{% endtabs %} + +What to take notice of here is that with traditional Overmind we would most likely just set the **user** or the **authenticationError** directly in the **login** action. That is not the case with statcharts because our actions are the triggers for transitions. That means whenever we want to deal with transitions we create an action for it, even completely empty actions like **tryAgain**. This simplifies our chart definition and also we avoid having a generic **transition** action that would not be typed in TypeScript. + +Now these two charts would operate individually. This is also the case for the **chart** property on the states of a chart. + +## Devtools + +The Overmind devtools understands statecharts. That means you are able to get an overview of available statecharts and even manipulate them directly in the devtools. + +![](../.gitbook/assets/statecharts.png) + +You will see what transition states and actions are available, and active, within each of them. You can click any active action to select it and click again to execute, or insert at payload at the top before execution. + +## Typescript + +To type a statechart you use the **Statechart** type: + +{% tabs %} +{% tab title="overmind/someNamespace/index.ts" %} +```typescript +import { Statechart, statechart } from 'overmind-statechart' +import * as actions from './actions' +import { state } from './state' + +const config = { + state, + actions +} + +const someChart: Statechart = { + initial: 'FOO', + states: { + FOO: {}, + BAR: {} + } +} + +export default statechart(config, someChart) +``` +{% endtab %} +{% endtabs %} + +The **void** type just defines that there are no nested charts. All the states and points of inserting an action name is now typed. Also the **condition** callback is typed. Even the **matches** API is typed correctly. + +### Nested chart + +{% tabs %} +{% tab title="overmind/someNamespace/index.ts" %} +```typescript +import { Statechart, statechart } from 'overmind-statechart' +import * as actions from './actions' +import { state } from './state' + +const config = { + state, + actions +} + +const someNestedChart: Statechart = { + initial: 'NESTED_FOO', + states: { + NESTED_FOO: {}, + NESTED_BAR: {} + } +} + +const someChart: Statechart = { + initial: 'FOO', + states: { + FOO: { + chart: someNestedChart + }, + BAR: {} + } +} + +export default statechart(config, someChart) +``` +{% endtab %} +{% endtabs %} + +## Summary + +The point of statecharts in Overmind is to give you an abstraction over your configuration that ensures the actions can only be run in certain states. Just like operators you can choose where you want to use it. Maybe only one namespace needs a statechart, or maybe you prefer using it on all of them. The devtools has its own visualizer for the charts, which allows you to implement and test them without implementing any UI.w + +## API + +### statechart + +The factory function you use to wrap an Overmind configuration. You add one or multiple charts to the configuration, where the key is the _id_ of the chart. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = {} + +export default statechart(config, chart) +``` + +### initial + +Define the initial state of the chart. When a parent chart enters a transition state, any nested chart will move to its initial transition state. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A' +} + +export default statechart(config, chart) +``` + +### states + +Defines the transition states of the chart. The chart can only be in one of these states at any point in time. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: {}, + STATE_B: {} + } +} + +export default statechart(config, chart) +``` + +### entry + +When a transition state is entered you can optionally run an action. It also runs if it is the initial state. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: { + entry: 'someActionName' + }, + STATE_B: {} + } +} + +export default statechart(config, chart) +``` + +{% hint style="info" %} +If you want to transition to a new state using an **entry**, you are free to call the action causing that transition from the **entry** action. +{% endhint %} + +### exit + +When a transition state is changed, any exit defined in current transition state will be run first. Nested charts in a transition state with an exit defined will run before parents. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: { + entry: 'someActionName', + exit: 'someOtherActionName' + }, + STATE_B: {} + } +} + +export default statechart(config, chart) +``` + +### on + +Unlike traditional statecharts Overmind uses its actions as transition types. This keeps a cleaner chart definition and when using Typescript the actions will have correct typing related to their payload. The actions defined are the only actions allowed to run. They can optionally lead to a new transition state, even conditionally lead to a new transition state. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: { + on: { + // Allow execution, but stay on this transition state + someAction: null, + + // Move to new transition state when executed + someOtherAction: 'STATE_B', + + // Conditionally move to a new transition state + someConditionalAction: { + target: 'STATE_B', + condition: state => state.isTrue + } + } + }, + STATE_B: {} + } +} + +export default statechart(config, chart) +``` + +### nested + +A nested statechart will operate within its parent transition state. The means when the parent transition state is entered or exited any defined **entry** and **exit** actions will be run. When the parent enters its transition state the **initial** state of the child statechart\(s\) will be activated. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const nestedChart: Statechart = { + initial: 'FOO', + states: { + FOO: { + on: { + transitionToBar: 'BAR' + } + }, + BAR: { + on: { + transitionToFoo: 'FOO' + } + } + } +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: { + on: { + transitionToStateB: 'STATE_B' + }, + chart: nestedChart + }, + STATE_B: { + on: { + transitionToStateA: 'STATE_A' + } + } + } +} + +export default statechart(config, chart) +``` + +### parallel + +Multiple statecharts will run in parallel. Either for the factory configuration or nested charts. You can add the same chart multiple times behind different ids. + +```typescript +import { Statechart, statechart } from 'overmind/config' +import * as actions from './actions' +import { state } from './state' + +const config = { + actions, + state +} + +const chart: Statechart = { + initial: 'STATE_A', + states: { + STATE_A: { + on: { + transitionToStateB: 'STATE_B' + } + }, + STATE_B: { + on: { + transitionToStateA: 'STATE_A' + } + } + } +} + +export default statechart(config, { + chartA: chart, + chartB: chart +}) +``` + +{% hint style="info" %} +Also the nested **chart** property of charts can contain parallel charts +{% endhint %} + +### matches + +The matches API is used in your components to identify what state your charts are in. It is accessed on the **state**. + +```typescript +// Given that you have added statecharts to the root configuration +state.matches({ + STATE_A: true +}) + +// Nested chart +state.matches({ + STATE_A: { + FOO: true + } +}) + +// Parallel +state.matches({ + chartA: { + STATE_A: true + }, + chartB: { + STATE_B: true + } +}) + +// Negative check +state.matches({ + chartA: { + STATE_A: true + }, + chartB: { + STATE_B: false + } +}) +``` + diff --git a/api-1/addmutationlistener.md b/api-1/addmutationlistener.md index 2baeb00..985de9a 100644 --- a/api-1/addmutationlistener.md +++ b/api-1/addmutationlistener.md @@ -1,6 +1,6 @@ # addMutationListener -It is possible to listen to all mutations performed in Overmind. This allows you to create special effects based on mutations within a certain domain of your app, or whatever else you come up with. Note that this method triggers before the mutation occurs, you might rather want to use **addFlushListener** to be notified about batched changes, like the components does. +It is possible to listen to all mutations performed in Overmind. This allows you to create special effects based on mutations within a certain domain of your app, or whatever else you come up with. Note that this method triggers right after any mutation occurs, you might rather want to use **addFlushListener** to be notified about batched changes, like the components does. {% tabs %} {% tab title="overmind/onInitialize.ts" %} diff --git a/api-1/createovermind.md b/api-1/createovermind.md index bb2c921..a5875d0 100644 --- a/api-1/createovermind.md +++ b/api-1/createovermind.md @@ -70,25 +70,23 @@ const overmind = createOvermind(config, { }) ``` -## events +## options.delimiter -Overmind emits events during execution of actions and similar. It can be beneficial to listen to these events for analytics or maybe you want to create a custom debugging experience. The following events can be listened to by adding a listener to the eventHub: +By default Overmind will create state paths using `.` as delimiter. This is used to give each state value an address and is used with the devtools. If any state keys uses `.` you will get weird behaviour in the devtools. You can now change this delimiter to a safe value, typically `' '` or `'|'` : ```typescript -overmind.eventHub.on('action:start', (execution) => {}) -overmind.eventHub.on('action:end', (execution) => {}) -overmind.eventHub.on('operator:start', (execution) => {}) -overmind.eventHub.on('operator:end', (execution) => {}) -overmind.eventHub.on('operator:async', (execution) => {}) -overmind.eventHub.on('mutations', (executionAndMutations) => {}) -overmind.eventHub.on('derived', (derived) => {}) -overmind.eventHub.on('derived:dirty', (derivedPathAndFlush) => {}) - -// Only during development -overmind.eventHub.on('effect', (effectDetails) => {}) -overmind.eventHub.on('getter', (getterDetails) => {}) -overmind.eventHub.on('component:add', (componentDetails) => {}) -overmind.eventHub.on('component:update', (componentDetails) => {}) -overmind.eventHub.on('component:remove', (componentDetails) => {}) +const overmind = createOvermind(config, { + delimiter: '.' +}) +``` + +## options.devEnv + +The default development environment in Overmind is called `development` , but you can change this to a custom name: + +```typescript +const overmind = createOvermind(config, { + devEnv: 'dev' +}) ``` diff --git a/api-1/createovermindmock.md b/api-1/createovermindmock.md index 64995a9..541e2a4 100644 --- a/api-1/createovermindmock.md +++ b/api-1/createovermindmock.md @@ -44,5 +44,7 @@ describe('Actions', () => { {% endtab %} {% endtabs %} - +{% hint style="warning" %} +It is important that you separate your **config** from the instantiation of Overmind, meaning that **createOvermind** should not be used in the same file as the config you see imported here, it should rather be used where you render your application. This allows the config to be used for multiple purposes. +{% endhint %} diff --git a/api-1/derive.md b/api-1/derive.md index 6a1ad4b..1d0d412 100644 --- a/api-1/derive.md +++ b/api-1/derive.md @@ -1,65 +1,25 @@ -# derive +# derived You can add derived state to your application. You access derived state like any other value, there is no need to call it as a function. The derived value is cached and will only update when any accessed state changes. {% tabs %} {% tab title="overmind/state.ts" %} ```typescript -import { Derive } from 'overmind' - -export type Item = { - title: string - completed: boolean -} - -export type State = { - items: Item[] - completedItems: Derive -} +import { derived } from 'overmind' export const state: State = { items: [], - completedItems: (state, rootState) => - state.items.filter(item => item.completed) + completedItems: derived((state, rootState) => { + return state.items.filter(item => item.completed) + }) } ``` {% endtab %} {% endtabs %} -The function defining your derived state receives two arguments. The first argument is the object the derived function is attached to. Ideally you use this argument to produce your derived state, though you can access the second argument which is the root state of the application. The root state allows you to access any state. +The function defining your derived state receives two arguments. The first argument is the object the derived function is attached to. Ideally you use this argument to produce the derived state, though you can access the second argument which is the root state of the application. The root state allows you to access any state. {% hint style="info" %} Accessing **rootState** might cause unnecessary updates to the derived function as it will track more state, though typically not an issue {% endhint %} -An other use case for derived is to return a function. This allows you to insert functions into your state tree which can execute logic, even based on existing state. Even the function itself might be changed out based on the state of the application. - -{% tabs %} -{% tab title="overmind/state.ts" %} -```typescript -import { Derive } from 'overmind' - -export type User = { - name: string -} - -export type State = { - user: User - tellUser: Derive {}> -} - -export const state: State = { - user: { - name: 'John' - }, - tellUser: (state) => - (message) => console.log(message, ', ' + state.user.name) -} -``` -{% endtab %} -{% endtabs %} - -{% hint style="info" %} -Using state inside the returned function will not be tracked. You have to access the state in the scope of the derived function -{% endhint %} - diff --git a/api-1/events.md b/api-1/events.md new file mode 100644 index 0000000..89a6ad5 --- /dev/null +++ b/api-1/events.md @@ -0,0 +1,22 @@ +# events + +Overmind emits events during execution of actions and similar. It can be beneficial to listen to these events for analytics or maybe you want to create a custom debugging experience. The following events can be listened to by adding a listener to the eventHub: + +```typescript +overmind.eventHub.on('action:start', (execution) => {}) +overmind.eventHub.on('action:end', (execution) => {}) +overmind.eventHub.on('operator:start', (execution) => {}) +overmind.eventHub.on('operator:end', (execution) => {}) +overmind.eventHub.on('operator:async', (execution) => {}) +overmind.eventHub.on('mutations', (executionAndMutations) => {}) +overmind.eventHub.on('derived', (derived) => {}) +overmind.eventHub.on('derived:dirty', (derivedPathAndFlush) => {}) + +// Only during development +overmind.eventHub.on('effect', (effectDetails) => {}) +overmind.eventHub.on('getter', (getterDetails) => {}) +overmind.eventHub.on('component:add', (componentDetails) => {}) +overmind.eventHub.on('component:update', (componentDetails) => {}) +overmind.eventHub.on('component:remove', (componentDetails) => {}) +``` + diff --git a/api-1/lazy.md b/api-1/lazy.md index eed4e4d..59945ea 100644 --- a/api-1/lazy.md +++ b/api-1/lazy.md @@ -3,40 +3,29 @@ You can lazy load configurations. You do this by giving each configuration a key with a function that returns the config when called. To actually load the configurations you can either call an effect or an action with the key of the configuration to load. {% tabs %} -{% tab title="overmind/index.ts" %} +{% tab title="overmind/index.js" %} ```typescript -import { IConfig } from 'overmind' import { lazy } from 'overmind/config' -import { Config as ModuleAConfig } from './moduleA' export const config = lazy({ - moduleA: async (): Promise => await import('./moduleA').config + moduleA: async () => await import('./moduleA').config }) - -declare module 'overmind' { - interface Config extends IConfig {} -} ``` {% endtab %} -{% tab title="overmind/moduleA/index.ts" %} +{% tab title="overmind/moduleA/index.js" %} ```typescript -import { IConfig } from 'overmind' import { state } from './state' export const config = { state } - -export interface Config extends IConfig {} ``` {% endtab %} -{% tab title="overmind/actions.ts" %} +{% tab title="overmind/actions.js" %} ```typescript -import { AsyncAction } from 'overmind' - -export const loadModule: AsyncAction = async ({ actions }) => { +export const loadModule = async ({ actions }) => { await actions.lazy.loadConfig('moduleA') } ``` diff --git a/api-1/namespaced.md b/api-1/namespaced.md index 2138ace..85294a1 100644 --- a/api-1/namespaced.md +++ b/api-1/namespaced.md @@ -2,6 +2,10 @@ Allows you to namespace configurations by a key. +The point of namespaces is to structure your code into domains, not isolate them. The reason being is that we more often than not design our namespaces wrong. We have no idea how the final app will look and getting into the issue of "cross domain logic and state" is a pain to refactor all the time due to wrong isolation. + +So in Overmind isolation is a discipline, not a technical restriction. + {% tabs %} {% tab title="overmind/index.ts" %} ```typescript diff --git a/api-1/operators.md b/api-1/operators.md index 7d71426..a4a68e9 100644 --- a/api-1/operators.md +++ b/api-1/operators.md @@ -153,7 +153,7 @@ export const getEventTargetValue: () => Operator = () => **async** -You use this operator whenever you want to change the state of the app, but you can run effects as well. Any returned value is ignored. +You use this operator whenever you want to change the state of the app. Any returned value is ignored. ```typescript import { Operator, mutate } from 'overmind' diff --git a/api-1/statemachine.md b/api-1/statemachine.md new file mode 100644 index 0000000..7b45323 --- /dev/null +++ b/api-1/statemachine.md @@ -0,0 +1,79 @@ +# statemachine + +A statematchine allows you to wrap state with transitions. That means you can protect your logic from running in invalid states of the application. + +## Create a statemachine + +You define a whole namespace as a statemachine, you can have a nested statemachine or you can even put statemachines inside statemachines. + +```javascript +import { statemachine } from 'overmind' + +export const state = statemachine({ + UNAUTHENTICATED: ['AUTHENTICATING'], + AUTHENTICATING: ['UNAUTHENTICATED', 'AUTHENTICATED'], + AUTHENTICATED: ['UNAUTHENTICATED'] +}, { + state: 'UNAUTHENTICATED' +}) +``` + +Instead of only defining state, you first define a set of transitions. The key represents a transition state, here **UNAUTHENTICATED**, **AUTHENTICATING** and **AUTHENTICATED**. Then we define an array which shows the next transition state can occur in the given transition state. When **UNAUTHENTICATED** we can move into the **AUTHENTICATING** state for example. When in **AUTHENTICATING** state we can move either back to **UNAUTHENTICATED** due to an error or we might move to **AUTHENTICATED**. The point is... when you are **UNAUTHENTICATED**, you can not run logic related to being **AUTHENTICATED**. And when **AUTHENTICATING** you can not run that logic again until you are back in **UNAUTHENTICATED**. + +As actual state values we define the initial transition state of **UNAUTHENTICATED**. + +If we wanted we could extend the state with other values, as normal. + +```javascript +import { statemachine } from 'overmind' + +export const state = statemachine({ + UNAUTHENTICATED: ['AUTHENTICATED'], + AUTHENTICATING: ['UNAUTHENTICATED', 'AUTHENTICATED'], + AUTHENTICATED: ['UNAUTHENTICATED'] +}, { + state: 'UNAUTHENTICATED', + todos: {}, + filter: 'all' +}) +``` + +## Transition between states + +The transition states are also part of the resulting **state** object, in this case: + +```javascript +// The resulting state object +export const state = { + UNAUTHENTICATED: () => {...}, + AUTHENTICATING: () => {...}, + AUTHENTICATED: () => {...}, + state: 'UNAUTHENTICATED', + todos: {}, + filter: 'all' +} +``` + +That means you can call **UNAUTHENTICATED**, **AUTHENTICATING** and **AUTHENTICATED** as functions to transition into the new states. And this is an example of how you would use them: + +```javascript +export const login = ({ state, effects }) => { + return state.AUTHENTICATING(() => { + try { + const user = await effects.api.login() + return state.AUTHENTICATED(() => { + state.user = user + }) + } catch (error) { + return state.UNAUTHENTICATED(() => { + state.error = error + }) + } + }) +} +``` + +When a component, or something else, calls the **login** action it will first try to move into the **AUTHENTICATING** state. If this is not possible, nothing else will happen. Then we go ahead an login, which returns a user. If we were to try to set the user immediately an error would be thrown, because it is being set "out of scope of the transition" \(asynchronously\). To actually set the user we first transition to **AUTHENTICATED** and given that is a valid transition the user will be set. + +What we accomplish in practice here is to ensure that changes to state is guarded by these transitions, which results in more predictable and safer code. + diff --git a/core/defining-state.md b/core/defining-state.md index ee61b84..d22c82b 100644 --- a/core/defining-state.md +++ b/core/defining-state.md @@ -6,29 +6,23 @@ The mechanism of communicating from the application to the user interface is cal ![](../.gitbook/assets/state-ui.png) -## Core values +## State tree -In JavaScript we can create all sorts of abstractions to describe values, but in Overmind we lean on the core serializable values. These are **objects**, **arrays**, **strings**, **numbers**, **booleans** and **null**. Serializable values means that we can easily convert the state into a string and back again. This is fundamental for creating great developer experiences, passing state between client and server and other features. You can describe any application state with these core values. - -Let us talk a little bit about what each value helps us represent in our application. - -### Objects - -The root value of your state tree is an object, because objects are great for holding other values. An object has keys that point to values. Most of these keys point to values that are the actual state of the application, but these keys can also represent domains of the application. A typical state structure could be: +Overmind is structured as a single state tree. That means all of your state can be accessed through a single object, called the **state**. This state tree will hold values which describes different states of your application. The tree branches out using plain objects, which can be considered **branches** of your state tree. ```javascript -{ +{ // branch modes: ['issues', 'admin'], currentModeIndex: 0, - admin: { + admin: { // branch currentUserId: null, - users: { + users: { // branch isLoading: false, data: {}, error: null }, }, - issues: { + issues: { // branch sortBy: 'name', isLoading: false, data: {}, @@ -37,6 +31,14 @@ The root value of your state tree is an object, because objects are great for ho } ``` +## State tree values + +The following are values to be used with the state tree. + +### Objects + +The plain objects are what **branches** out the tree. It is not really considered a value in itself, it is a state branch holding values. + ### Arrays Arrays are similar to objects in the sense that they hold other values, but instead of keys pointing to values you have indexes. That means it is ideal for iteration. But more often than not objects are actually better at managing lists of values. We can actually do fine without arrays in our state. It is when we produce the actual user interface that we usually want arrays. You can learn more about this in the [MANAGING LISTS](../guides-1/managing-lists.md) guide. @@ -70,14 +72,51 @@ Are things loading or not, is the user logged in or not? These are typical uses All values, with the exception of booleans, can also be **null**. Non-existing. You can have a non-existing object, array, string or number. It means that if we haven’t selected a mode, both the string version and number version would have the value **null**. -## Undefined +### Derived -You might wonder why **undefined** is not part of the core value types. Well, there are two reasons: +When you need to derive state you can add a derived function to your tree. Overmind treats these functions like a **getter**, but the returned value is cached and they can also access the root state of the application. A simple example of this would be: -1. It is not a serializable value. That means if you explicitly set a value to _undefined_ it will not show up in the devtools -2. Undefined values can not be tracked. That means if you were to iterate an object and look at the keys of that object, any undefined values will not be tracked. This can cause unexpected behaviour +{% tabs %} +{% tab title="overmind/state.js" %} +```typescript +import { derived } from 'overmind' -## Class values +export const state = { + title: 'My awesome title', + upperTitle: derived(state => state.title.toUpperCase()) +} +``` +{% endtab %} +{% endtabs %} + +The first argument of the function is the state the derived function is attached to. A second argument is also passed and that is the root state of the application, allowing you to access whatever you would need. + +{% hint style="info" %} +Even though derived state is defined as functions you consume them as plain values. You do not have to call the derived function to get the value. Derived functions can also be dynamically added. +{% endhint %} + +You can also return a function from the derived. For example: + +{% tabs %} +{% tab title="overmind/state.js" %} +```typescript +import { derived } from 'overmind' + +export const state = { + users: {}, + getUserById: derived(state => id => state.users[id]) +} +``` +{% endtab %} +{% endtabs %} + +The returned value here is indeed a function you call. The cool thing is that the function itself will never change, but whatever state you access when calling the function will be tracked by the caller of the function. So for example if a component uses **getUserById** during rendering it will track what is accessed in the function and continue tracking whatever you access on the returned value. + +{% hint style="info" %} +You may use a derived for all sorts of calculations. But sometimes it's better to just use a plain action to manipulate some state than using a derived. Why? Imagine a table component having a lot of rows and columns. We assume the table component also takes care of sorting and filtering and is capable of adding new rows. Now if you solve the sorting and filtering using a derived the following could happen: User adds a new row but it is not displayed in the list because the derived immediately kicked in and filtered it out. Thats not a good user experience. Also in this case the filtering and sorting is clearly started by a simple user interaction \(setting a filter value, clicking on a column,...\) so why not just start an action which creates the new list of sorted and filtered keys? Also the heavy calculation is now very predictable and doesn't cause performance issues because the derived kickes in too often \(Because it could have many dependencies you might didn't think of\) +{% endhint %} + +### Class instances Overmind also supports using class instances as state values. Depending on your preference this can be a powerful tool to organize your logic. What classes provide is a way to co locate state and logic for changing and deriving that state. In functional programming the state and the logic is separated and it can be difficult to find a good way to organize the logic operating on that state. @@ -91,7 +130,7 @@ class LoginForm { this.username = '' this.password = '' } - isValid() { + get isValid() { return Boolean(this.username && this.password) } reset() { @@ -114,16 +153,12 @@ export const state = { {% endtabs %} {% hint style="warning" %} -It is import that you do **NOT** use arrow functions on your methods. The reason is that this binds the context of the method to the instance itself, meaning that Overmind is unable to proxy access and allow you to do tracked mutations +It is import that you do **NOT** use arrow functions on your methods. The reason is that this binds the context of the method to the instance itself, meaning that Overmind is unable to proxy access and track mutations {% endhint %} You can now use this instance as normal and of course create new ones. -{% hint style="info" %} -Even though you can use **getters** as normal, they do not cache like **derived**. **Derived** is a concept of the state tree itself. It is unlikely that you need heavy computation within a single class instance though, it is typically across class instances, where **derived** fits the bill -{% endhint %} - -### Serializing class values +#### Serializing class values If you have an application that needs to serialize the state, for example to local storage or server side rendering, you can still use class instances with Overmind. By default you really do not have to do anything, but if you use **Typescript** or you choose to use **toJSON** on your classesOvermind exposes a symbol called **SERIALIZE** that you can attach to your class. @@ -160,9 +195,9 @@ class User { The **SERIALIZE** symbol will not be part of the actual serialization done with **JSON.stringify** {% endhint %} -### Rehydrating classes +#### Rehydrating classes -The [**rehydrate**](../api-1/rehydrate.md) ****utility of Overmind allows you to rehydrate state either by a list of mutations or a state object, like the following: +The [**rehydrate**](../api-1/rehydrate.md) \_\*\*\_utility of Overmind allows you to rehydrate state either by a list of mutations or a state object, like the following: {% tabs %} {% tab title="overmind/actions.js" %} @@ -270,77 +305,10 @@ export const updateState = ({ state }) => { {% endtabs %} {% hint style="info" %} -Note that **rehydrate** gives you full type safety when adding the **SERIALIZE** symbol to your classes. This is a huge benefit as Typescript will yell at you when the state structure changes, related to the rehydration -{% endhint %} - -## Deriving state - -### Getter - -A concept in Javascript called a [GETTER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) allows you to intercept accessing a property in an object. A getter is just like a plain value, it can be added or removed at any point. Getters do **not** cache the result for that very reason, but whatever state they access is tracked. - -{% tabs %} -{% tab title="overmind/state.js" %} -```javascript -export const state = { - user: { - id: 1, - firstName: 'Bob', - lastName: 'Jackson', - jwt: '1234567' - }, - get isLoggedIn() { - return Boolean(this.user && this.user.jwt) - } -} -``` -{% endtab %} -{% endtabs %} - -### Cached getter - -When you need to do more heavy calculation or combine state from different parts of the tree you can use a plain function instead. Overmind treats these functions like a **getter**, but the returned value is cached and they can also access the root state of the application. A simple example of this would be: - -{% tabs %} -{% tab title="overmind/state.js" %} -```typescript -export const state: State = { - title: 'My awesome title', - upperTitle: state => state.title.toUpperCase() -} -``` -{% endtab %} -{% endtabs %} - -The first argument of the function is the state the derived function is attached to. A second argument is also passed and that is the root state of the application, allowing you to access whatever you would need. Two important traits of the derived function is: - -1. The state accessed is tracked -2. The value returned is cached - -That means the function only runs when accessed and the depending state has changed since last access. - -{% hint style="info" %} -Even though derived state is defined as functions you consume them as plain values. You do not have to call the derived function to get the value. Derived state can not be dynamically added. They have to be defined and live in the tree from start to end of your application lifecycle. +Note that **rehydrate** gives you full type safety when adding the **SERIALIZE** symbol to your classes. This is a huge benefit as Typescript will yell at you when the state structure changes, related to the rehydration {% endhint %} -### Dynamic getter - -Sometimes you want to derive state based on some value coming from the user interface. You can do this by creating a function that returns a function. This can be useful for helper functions: - -{% tabs %} -{% tab title="overmind/state.js" %} -```javascript -export const state = { - users: {}, - userById: ({ users }) => id => users[id] -} - -// state.userById('123') -``` -{% endtab %} -{% endtabs %} - -## Statemachines +### Statemachines Very often you get into a situation where you define states as **isLoading**, **hasError** etc. Having these kinds of state can cause **impossible states**. For example: @@ -353,124 +321,7 @@ const state = { You can not be authenticating and be authenticated at the same time. This kind of logic very often causes bugs in applications. That is why Overmind allows you to define statemachines. It sounds complicated, but is actually very simple. -### Defining - -{% tabs %} -{% tab title="overmind/state.js" %} -```typescript -import { statemachine } from 'overmind' - -export const state = { - mode: statemachine({ - initial: 'unauthenticated', - states: { - unauthenticated: ['authenticating'], - authenticating: ['unauthenticated', 'authenticated'], - authenticated: ['unauthenticating'], - unauthenticating: ['unauthenticated', 'authenticated'] - } - }), - user: null, - error: null -} -``` -{% endtab %} -{% endtabs %} - -You set an **initial** state and then you create a relationship between the different states and what states they can transition into. So when **unauthenticated** is the state, only logic triggered with an **authenticating** transition will run, any other transition triggered will not run its logic. - -### Transitioning - -{% tabs %} -{% tab title="overmind/actions.js" %} -```typescript -export const login = async ({ state, effects }) => { - return state.mode.authenticating(async () => { - try { - const user = await effects.api.getUser() - return state.mode.authenticated(() => { - state.user = user - }) - } catch (error) { - return state.mode.unauthenticated(() => { - state.error = error - }) - } - }) -} - -export const logout = async ({ state, effects }) => { - return state.mode.unauthenticating(async () => { - try { - await effects.api.logout() - return state.mode.unauthenticated() - } catch (error) { - return state.mode.authenticated(() => { - state.error = error - }) - } - }) -} -``` -{% endtab %} -{% endtabs %} - -{% hint style="warning" %} -There are two important rules for predictable transitions: - -1. The transition should be **returned** if the logic or logic runs asynchronously. This is the same as with actions in general -2. Only **synchronous** transitions can mutate the state, any async mutation will throw an error -{% endhint %} - -What is important to realize here is that our logic is separated into **allowable** transitions. That means when we are waiting for the user on **line 4** and some other logic has changed the state to **unauthenticated** in the meantime, the user will not be set, as the **authenticated** transition is now not possible. This is what state machines do. They group logic into states that are allowed to run, preventing invalid logic to run. - -### Current state - -The current state is accessed, related to this example, by: - -```typescript -state.mode.current -``` - -### Exit - -It is also possible to run logic when a transition exits. An example of this is for example if a transition sets up a subscription. This subscription can be disposed when the transition is exited. - -{% tabs %} -{% tab title="overmind/actions.js" %} -```typescript -export const login = async ({ state, effects }) => { - return state.mode.authenticating(async () => { - try { - const user = await effects.api.getUser() - let disposeSubscription - state.mode.authenticated( - () => { - disposeSubscription = effects.api.subscribeNotifications() - state.user = user - }, - () => { - disposeSubscription() - } - ) - } catch (error) { - state.mode.unauthenticated(() => { - state.error = error - }) - } - }) -} -``` -{% endtab %} -{% endtabs %} - -### Reset - -You can reset the state of a statemachine, which also runs the exit of the current transition: - -```typescript -state.mode.reset() -``` +To properly understand state machines, please read the guide [**Using state machines**](../guides-1/using-state-machines.md). ## References diff --git a/core/devtools.md b/core/devtools.md index fe71a09..2924129 100644 --- a/core/devtools.md +++ b/core/devtools.md @@ -1,18 +1,8 @@ # Devtools -## VS Code - -For the best experience you should install the [OVERMIND DEVTOOLS](https://marketplace.visualstudio.com/items?itemName=christianalfoni.overmind-devtools-vscode) extension. This will allow you to work on your application without leaving the IDE at all. - -![](../.gitbook/assets/amazing_devtools.png) - -{% hint style="info" %} -If you are using the **Insiders** version of VSCode the extension will not work. It seems to be some extra security setting. -{% endhint %} - ## Standalone app -Alternatively you can install the standalone application of the devtools. You can start it with the NPM executor as: +You can start the devtools by using the NPM executor: ```javascript npx overmind-devtools@latest @@ -46,12 +36,22 @@ npm install overmind-devtools concurrently ``` {% endcode %} +## VS Code + +You can also install the [OVERMIND DEVTOOLS](https://marketplace.visualstudio.com/items?itemName=christianalfoni.overmind-devtools-vscode) extension. This will allow you to work on your application without leaving the IDE at all. + +![](../.gitbook/assets/amazing_devtools.png) + +{% hint style="info" %} +If you are using the **Insiders** version of VSCode the extension will not work. It seems to be some extra security setting. +{% endhint %} + ## Connecting from the application When you create your application it will automatically connect through **localhost:3031**, meaning that everything should just work out of the box. If you need to change the port, connect the application over a network \(mobile development\) or similar, you can configure how the application connects: ```javascript -import { createOvermind } from 'overmind' +import { createOvermind } from 'overmind' import { config } from './overmind' const overmind = createOvermind(config, { @@ -63,9 +63,63 @@ const overmind = createOvermind(config, { ChromeOS does not expose localhost as normal. That means you need to connect with **penguin.termina.linux.test:3031**, or you can use the following plugin to forward **localhost:** -{% embed url="https://chrome.google.com/webstore/detail/connection-forwarder/ahaijnonphgkgnkbklchdhclailflinn/related?hl=en-US" %} +{% embed url="https://chrome.google.com/webstore/detail/connection-forwarder/ahaijnonphgkgnkbklchdhclailflinn/related?hl=en-US" caption="" %} ## Hot Module Replacement A popular concept introduced by Webpack is [HMR](https://webpack.js.org/concepts/hot-module-replacement/). It allows you to make changes to your code without having to refresh. Overmind automatically supports HMR. That means when **HMR** is activated Overmind will make sure it updates and manages its state, actions and effects. Even the devtools will be updated as you make changes. +Typically you add this, here showing with React: + +```typescript +import React from 'react' +import { render } from 'react-dom' +import { createOvermind } from 'overmind' +import { Provider } from 'overmind-react' +import { config } from './overmind' +import { App } from './components/App' + + +const overmind = createOvermind(config) + +render(, document.querySelector('#app')) + +// Allows this module to run again without refresh, +// meaning "createOvermind" runs again and automatically +// reconfigures +if (module.hot) { + module.hot.accept() +} +``` + +Though you can also manually only update Overmind by: + +{% tabs %} +{% tab title="index.js" %} +```typescript +import React from 'react' +import { render } from 'react-dom' +import { overmind } from './overmindInstance' +import { Provider } from 'overmind-react' +import { App } from './components/App' + +render(, document.querySelector('#app')) +``` +{% endtab %} + +{% tab title="overmindInstance.js" %} +```javascript +import { createOvermind } from 'overmind' +import { config } from './overmind' + +export const overmind = createOvermind(config) + +// When this module runs again "createOvermind" is run +// and it automatically reconfigures +if (module.hot) { + module.hot.accept() +} +``` +{% endtab %} +{% endtabs %} + diff --git a/core/server-side-rendering.md b/core/server-side-rendering.md index 51c5ac5..d3105e4 100644 --- a/core/server-side-rendering.md +++ b/core/server-side-rendering.md @@ -155,3 +155,119 @@ export default async (req, res) => { {% endtab %} {% endtabs %} +## Next.js + +The idea behind setting up overmind in `next.js` is the same as a standard express server but we have a lot of help from next to get us going. + +Let's start by adding a `_document.js` and this is where we will initialize the SSR version of Overmind: + +{% tabs %} +{% tab title="src/\_document.js" %} +```javascript +import App from "next/app"; +import { createOvermind, createOvermindSSR, rehydrate } from "overmind"; +import { Provider } from "overmind-react"; +import { config } from "../overmind"; + +export default class MyApp extends App { + // CLIENT: On initial route + // SERVER: On initial route + constructor(props) { + super(props); + + const mutations = props.pageProps.mutations || []; + + if (typeof window !== "undefined") { + // On the client we just instantiate the Overmind instance and run + // the "changePage" action + this.overmind = createOvermind(config); + this.overmind.actions.changePage(mutations); + } else { + // On the server we rehydrate the mutations to an SSR instance of Overmind, + // as we do not want to run any additional logic here + this.overmind = createOvermindSSR(config); + rehydrate(this.overmind.state, mutations); + } + } + // CLIENT: After initial route, on page change + // SERVER: never + componentDidUpdate() { + // This runs whenever the client routes to a new page + this.overmind.actions.changePage(this.props.pageProps.mutations || []); + } + render() { + const { Component, pageProps } = this.props; + return ( + + + + ); + } +} +``` +{% endtab %} +{% endtabs %} + +And then let's create a standard `Overmind` instance: + +```javascript +import { rehydrate } from "overmind"; +import { createHook } from "overmind-react"; + +export const config = { + state: {}, + actions: { + add + changePage({ state }, mutations) { + rehydrate(state, mutations || []); + } + } +}; + +export const useOvermind = createHook(); +``` + +And you are all set to get going with `overmind` and `next.js`. You can also take a look at [this example in the next.js examples directory](https://github.com/vercel/next.js/tree/canary/examples/with-overmind) if you need some help. + +## Gatsby + +When it comes to gatsby we need to prepare Overmind for static extraction and the idea is about the same. + +We need first to wrap our whole app in the Overmind provider and we can do that in `gatsby-browser.js`: + +```javascript +import React from "react" +import { createOvermind } from "overmind" +import { Provider } from "overmind-react" +import { config } from "./src/overmind" + +const overmind = createOvermind(config); + +export const wrapPageElement = ({ element }) => ( + + {element} + +) +``` + +After this is done we can do the same thing for the server render and add that code in the `gatsby-ssr.js` file: + +```javascript +import React from "react" +import { Provider } from "overmind-react" +import { createOvermindSSR } from "overmind" +import { ThemeProvider as ChakraProvider } from "@chakra-ui/core" +import { theme } from "@chakra-ui/core" +import { config } from "./src/overmind" + +const overmind = createOvermindSSR(config) + +export const wrapPageElement = ({ element }) => ( + + {element} + +) +``` + +As you can see the only difference we have here is that we createOvermindSSR in the `gatsby-ssr.js` + diff --git a/core/structuring-the-app.md b/core/structuring-the-app.md index 631b8c8..79f1569 100644 --- a/core/structuring-the-app.md +++ b/core/structuring-the-app.md @@ -6,9 +6,11 @@ Overmind is based on a core concept of: This data structure is called **the configuration** of your application. If it is a simple application you might have a single configuration, but typically you will create multiple of them and use tools to merge them together into one big configuration. But before we look at the scalability of Overmind, let’s talk about file structure. -## Domains +## Namspaces -As your application grows you start to separate it into different domains. A domain might be closely related to a page in your application, or maybe it is strictly related to managing some piece of data. It does not matter. You define the domains of your application and they probably change over time as well. What matters in the context of Overmind though is that each of these domains will contain their own state, actions and effects. So imagine a file structure of: +![](../.gitbook/assets/image%20%281%29%20%281%29.png) + +As your application grows you start to separate it into different namespaces. A namespace might be closely related to a page in your application, or maybe it is strictly related to managing some piece of data. It does not matter. You define the namespaces of your application and they probably change over time as well. What matters in the context of Overmind though is that each of these namespaces will contain their own state, actions and effects. So imagine a file structure of: ```text overmind/ @@ -20,7 +22,7 @@ overmind/ In this structure we are splitting up the different components of the configuration. This is a good first step. The **index** file acts as the file that brings the **state**, **actions** and **effects** together. -But if we want to split up into actual domains it would look more like this: +But if we want to split up into actual namespaces it would look more like this: ```text overmind/ @@ -37,7 +39,7 @@ overmind/ index.ts ``` -In this case each domain **index** file bring its own state, actions and effects together and the **overmind/index** file is responsible for bringing the whole configuration together. +In this case each namespace **index** file bring its own state, actions and effects together and the **overmind/index** file is responsible for bringing the whole configuration together. ## The state file @@ -174,9 +176,9 @@ export const config = namespaced({ {% endtab %} {% endtabs %} -We used the **namespaced** function to put the state, actions and effects from each domain behind a key. In this case the key is the same as the name of the domain itself. This is an effective way to split up your app. +We used the **namespaced** function to put the state, actions and effects from each namespace behind a key. In this case the key is the same as the name of the namespace itself. This is an effective way to split up your app. -You can also combine this with the **merge** tool to have a top level domain. +You can also combine this with the **merge** tool to have a top level namespace. {% tabs %} {% tab title="overmind/index.js" %} @@ -200,6 +202,6 @@ export const config = merge( {% endtabs %} {% hint style="info" %} -Even though you split up into different domains each domain has access to the state of the whole application. This is an important feature of Overmind which allows you to scale up and explore the domains of the application without having to worry about isolation. +Even though you split up into different namespaces each namespace has access to the state, actions and effects of the whole application. This is an important feature of Overmind which allows you to scale up and explore the domains of the application without having to worry about isolation. {% endhint %} diff --git a/core/typescript.md b/core/typescript.md index ea9cbcd..59b90ca 100644 --- a/core/typescript.md +++ b/core/typescript.md @@ -18,8 +18,15 @@ import { IConfig } from 'overmind' const config = {} declare module 'overmind' { - // tslint:disable:interface-name - interface Config extends IConfig {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Config extends IConfig<{ + state: typeof config.state, + actions: typeof config.actions, + effects: typeof config.effects + }> {} + // Due to circular typing we have to define an + // explicit typing of state, actions and effects since + // TS 3.9 } ``` {% endtab %} @@ -29,9 +36,8 @@ Now you can import any type directly from Overmind and it will understand the co ```typescript import { - Action, - Operator, - Derive, + Context, + RootState, pipe, map, filter, @@ -49,25 +55,26 @@ You can also explicitly type your application. This gives more flexibility. import { IConfig, IOnInitialize, - IAction, - IOperator, - IDerive, - IState + IContext, } from 'overmind' export const config = {} -export interface Config extends IConfig {} +// Due to circular typing we have to define an +// explicit typing of state, actions and effects since +// TS 3.9 +export interface Config extends IConfig<{ + state: typeof config.state, + actions: typeof config.actions, + effects: typeof config.effects +}> {} export interface OnInitialize extends IOnInitialize {} -export interface Action extends IAction {} +export interface Context extends IContext {} -export interface AsyncAction extends IAction> {} - -export interface Operator extends IOperator {} - -export interface Derive extends IDerive {} +// Used with derived +export type RootState = Context['state'] ``` {% endtab %} {% endtabs %} @@ -105,7 +112,7 @@ export const state: State = { {% endtabs %} {% hint style="info" %} -It is important that you use a **type** and not an **interface.** This has to do with the way Overmind resolves the state typing. **** +It is important that you use a **type** and not an **interface.** This has to do with the way Overmind resolves the state typing. _\*\*_ {% endhint %} When writing Typescript you should **not** use optional values for your state \(**?**\), or use **undefined** in a union type. In a serializable state store world **null** is the value indicating _“there is no value”._ @@ -130,100 +137,104 @@ export const state: State = { } ``` -### Getter +### Derived {% tabs %} {% tab title="overmind/state.ts" %} ```typescript +import { derived } from 'overmind' + type State = { foo: string - shoutedFoo string + shoutedFoo: string } export const state: State = { foo: 'bar', - get shoutedFoo(this: State) { - return this.foo + '!!!' - } + shoutedFoo: derived((state: State) => state.foo + '!!!') } ``` {% endtab %} {% endtabs %} -### Derived +Note that the type argument you pass is the object the derived is attached to, so with nested derived: {% tabs %} {% tab title="overmind/state.ts" %} ```typescript -import { Derived } from 'overmind' +import { derived } from 'overmind' type State = { foo: string - shoutedFoo Derived + nested: { + shoutedFoo: string + } } export const state: State = { foo: 'bar', - shoutedFoo: state => state.foo + '!!!' + nested: { + shoutedFoo: derived((state: State['nested']) => state.foo + '!!!') + } } ``` {% endtab %} {% endtabs %} -## Actions - -The action type takes either an input type, an output type, or both. +Note that with **Explicit Typing** you need to also pass the a third argument to the **derived** function, the **Config** type created in your main **index.ts** file. +{% tabs %} +{% tab title="overmind/state.ts" %} ```typescript -import { Action } from 'overmind' +import { RootState } from 'overmind' -export const noArgAction: Action = (context, value) => { - value // this becomes "void" +type State = { + foo: string + shoutedFoo: string } -export const argAction: Action = (context, value) => { - value // this becomes "string" +export const state: State = { + foo: 'bar', + shoutedFoo: derived( + (state: State, rootState: RootState) => state.foo + '!!!' + ) } +``` +{% endtab %} +{% endtabs %} -export const noArgWithReturnTypeAction: Action = (context, value) => { - value // this becomes "void" - - return 'foo' -} +### Statemachine -export const argWithReturnTypeAction: Action = (context, value) => { - value // this becomes "string" +Read the guide on [**Using state machines**](../guides-1/using-state-machines.md) to understand how to type them. - return value + '!!!' -} -``` +## Actions -You also have an **async** version of this type. You use this when you want to define an **async** function, which implicitly returns a promise, or use it on a function that explicitly returns a promise. +You type your actions with the **Context** and an optional value. Any return type will be inferred. ```typescript -import { AsyncAction } from 'overmind' +import { Context } from 'overmind' -export const noArgAction: AsyncAction = async (context, value) => { - value // this becomes "void" +export const noArgAction = (context: Context) => { + // actions.noArgAction() } -export const argAction: AsyncAction = async (context, value) => { - value // this becomes "string" +export const argAction = (context: Context, value: string) => { + // actions.argAction("foo"), requires "string" } -export const noArgWithReturnTypeAction: AsyncAction = async (context, value) => { - value // this becomes "void" - +export const noArgWithReturnTypeAction = (context: Context) => { + // actions.noArgWithReturnTypeAction(), with return type "string" return 'foo' -} // returns Promise - -export const argWithReturnTypeAction: AsyncAction = (context, value) => { - value // this becomes "string" +} - return Promise.resolve(value + '!!!') -} // returns Promise +export const argWithReturnTypeAction = (context: Context, value: string) => { + // actions.argWithReturnTypeAction("foo"), requires "string" and returns "string" + return value + '!!!' +} ``` +Any of these actions could be defined as an **async** function or simply return a promise to be typed that way. + ## Effects There are no Overmind specific types related to effects, you just type them in general. @@ -234,7 +245,7 @@ There are no Overmind specific types related to effects, you just type them in g export const api = { getUser: async (): Promise => { const response = await fetch('/user') - + return response.json() } } @@ -244,261 +255,43 @@ export const api = { ## Operators -Operators is like the **Action** type: it can take an optional input, but it always produces an output. By default the output of an operator is the same as the input. - -{% tabs %} -{% tab title="overmind/operators.ts" %} -```typescript -import { Operator, mutate, filter, map } from 'overmind' - -// You do not need to define any types, which means it defaults -// its input and output to "void" -export const changeSomeState: () => Operator = () => - mutate(function changeSomeState({ state }) { - state.foo = 'bar' - }) - -// The second type argument is not set, but will default to "User" -// The output is the same as the input -export const filterAwesomeUser: () => Operator = () => - filter(function filterAwesomeUser(_, user) { - return user.isAwesome - }) - -// "map" produces a new output so we define that as the second -// type argument -export const toNumber: () => Operator = () => - map(function toNumber(_, value) { - return Number(value) - }) -``` -{% endtab %} -{% endtabs %} - -The **Operator** type is used to type all operators. The type arguments you give to **Operator** have to match the specific operator you use though. So for example if you type a **mutate** operator with a different output than the input: - -```typescript -import { Operator, mutate } from 'overmind' - -export const doThis: () => Operator = () => - mutate(function doThis() { - - }) -``` - -Typescript yells at you, because this operator just passes the value straight through. - -Typically you do not think about this and Typescript rather yells at you when the value you are passing through your operators is not matching up. - -### Generic input - -You might create an operator that does not care about its input. For example: - -{% tabs %} -{% tab title="overmind/operators.ts" %} -```typescript -import { Operator, mutate } from 'overmind' - -export const doSomething: () => Operator = () => - mutate(function doSomething({ state }) { - state.foo = 'bar' - }) -``` -{% endtab %} - -{% tab title="overmind/actions.ts" %} -```typescript -import { Operator, pipe, action } from 'overmind' -import * as o from './operators' - -export const setInput: Operator = pipe( - o.doSomething(), - o.setValue() -) -``` -{% endtab %} -{% endtabs %} - -Composing **doSomething** into the **pipe** gives an error, cause the action is typed with a **string** input, but the **doSomething** operator is typed with **void**. - -To fix this we just add a generic type to the definition of our operator: - -{% tabs %} -{% tab title="overmind/operators.ts" %} -```typescript -import { Operator, mutate } from 'overmind' - -export const doSomething: () => Operator = () => - mutate(function doSomething({ state }) { - state.foo = 'bar' - }) -``` -{% endtab %} -{% endtabs %} - -Now Typescript infers the input type of the operator and passes it along. - -### Partial input - -For example: +Operators is like the action: it can take an optional value, but it always produces an output. By default the output of an operator is the same as the input. {% tabs %} {% tab title="overmind/operators.ts" %} ```typescript -import { Operator, filter } from 'overmind' - -export const filterAwesome: () => Operator<{ isAwesome: boolean }> = () => - filter(function filterAwesome(_, somethingAwesome) { - return somethingAwesome.isAwesome - }) -``` -{% endtab %} - -{% tab title="overmind/actions.ts" %} -```typescript -import { Operator, pipe, action } from 'overmind' -import * as o from './operators' -import { User } from './state' - -export const clickedUser: Operator = pipe( - o.filterAwesome(), - o.handleAwesomeUser() -) +import { Context, mutate, filter, map } from 'overmind' + +// Use the Context type for the first argument +export const changeSomeState = mutate(({ state }: Context) => { + state.foo = 'bar' +}) + +// Type the value as the second argument +export const filterAwesomeUser = filter((_: Context, user: User) => { + return user.isAwesome +}) + +// The output is inferred +export const toNumber = map((_: Context, value: number) => { + return Number(value) +}) ``` {% endtab %} {% endtabs %} -Now the _input_ is actually okay, because `{ isAwesome: boolean }` matches the **User** type, but we are also now saying that the type of _output_ will be `{ isAwesome: boolean }`, which does not match the **User** type required by **handleAwesomeUser**. +When you create a **pipe** that has an input when it is called you only need to type the first operator value. -To fix this we again infer the type, but using **extends** to indicate that we do have a requirement to the type it should pass through: - -{% tabs %} -{% tab title="overmind/operators.ts" %} -```typescript -import { Operator, filter } from 'overmind' - -export const filterAwesome: () => Operator = - () => filter(function filterAwesome(_, somethingAwesome) { - return somethingAwesome.isAwesome - }) -``` -{% endtab %} -{% endtabs %} - -That means this operator can handle any type that matches an **isAwesome** property, though will pass the original type through. - -## Statemachine - -Statemachines exposes a type called **Statemachine** which you will give a single type argument of what states it should manage: - -{% tabs %} -{% tab title="overmind/state.ts" %} ```typescript -import { Statemachine, statemachine } from 'overmind' - -type Mode = - | 'unauthenticated' - | 'authenticating' - | 'authenticated' - | 'unauthenticating' - -type State = { - mode: Statemachine -} - -export const state: State = { - mode: statemachine({ - initial: 'unauthenticated', - states: { - unauthenticated: ['authenticating'], - authenticating: ['unauthenticated', 'authenticated'], - authenticated: ['unauthenticating'], - unauthenticating: ['unauthenticated', 'authenticated'] - } +import { Context, pipe, map, mutate } from 'overmind' + +export const doThis = pipe( + map((context: Context, value: string) => { + // actions.doThis("foo"), requires "string" + return 123 + }), + mutate((context: Context, value) => { + // value is now "number" }) -} ``` -{% endtab %} -{% endtabs %} - -## Statechart - -To type a statechart you use the **Statechart** type: - -{% tabs %} -{% tab title="overmind/someNamespace/index.ts" %} -```typescript -import { Statechart, statechart } from 'overmind/config' -import * as actions from './actions' -import { state } from './state' - -const config = { - state, - actions -} - -const someChart: Statechart = { - initial: 'FOO', - states: { - FOO: {}, - BAR: {} - } -} - -export default statechart(config, someChart) -``` -{% endtab %} -{% endtabs %} - -The **void** type just defines that there are no nested charts. All the states and points of inserting an action name is now typed. Also the **condition** callback is typed. Even the **matches** API is typed correctly. - -### Nested chart - -{% tabs %} -{% tab title="overmind/someNamespace/index.ts" %} -```typescript -import { Statechart, statechart } from 'overmind/config' -import * as actions from './actions' -import { state } from './state' - -const config = { - state, - actions -} - -const someNestedChart: Statechart = { - initial: 'NESTED_FOO', - states: { - NESTED_FOO: {}, - NESTED_BAR: {} - } -} - -const someChart: Statechart = { - initial: 'FOO', - states: { - FOO: { - chart: someNestedChart - }, - BAR: {} - } -} - -export default statechart(config, someChart) -``` -{% endtab %} -{% endtabs %} - -## Linting - -When you are using TSLint it is important that you use the official [MICROSOFT EXTENSION](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) for VS Code. diff --git a/faq.md b/faq.md index 78000ac..9fd47f4 100644 --- a/faq.md +++ b/faq.md @@ -4,6 +4,8 @@ First… try to refresh your app to reconnect. If this does not work make sure that the entry point in your application is actually creating an instance of Overmind. The code **createOvermind\(config\)** has to run to instantiate the devtools. +This is also a common misconception about the ports. You want `createOvermind` to connect to the port defined inside the Devtool, which is `3031` by default. + ## The devtools does not open in VS Code? Restart VS Code @@ -12,5 +14,3 @@ Restart VS Code Operators are identified with a Symbol. If you happen to use Overmind across packages you might be running two versions of Overmind. The same goes for core package and view package installed out of version sync. Make sure you are only running on package of Overmind by looking into your **node\_modules** folder. - - diff --git a/guides-1/using-state-machines.md b/guides-1/using-state-machines.md new file mode 100644 index 0000000..674f1ef --- /dev/null +++ b/guides-1/using-state-machines.md @@ -0,0 +1,315 @@ +# Using state machines + +The Overmind state machines is heavily inspired by [XState](https://xstate.js.org/) and [Davids](https://twitter.com/DavidKPiano) evangelism of bringing this old idea to life in the JavaScript ecosystem. Typically state machines are explained with very specific concepts like street lights, timers or similar "machine like" concepts. For Overmind it was important that this concept could be used to describe the overall state of the application. This was a huge challenge and required several iterations, but we found a concept that holds the idea true and makes it a practical and optional way to manage your state. Use it for your whole application or use it for specific scenarios. + +{% hint style="info" %} +The state machine API is designed for use with **TypeScript**. The reason is that the complexity of transition state matching is best expressed using [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining), which is not yet available in plain JavaScript. +{% endhint %} + +## Creating a state machine + +To understand the benefit of a state machine we have to use a very specific example. One such example that is typical for any application is authentication. Typically in Overmind you would define this as: + +```typescript +type State = { + isAuthenticating: boolean + user: { username: string } | null + signedOutReason: string | null +} + +export const state: State = { + isAuthenticating: true, + user: null, + signedOutReason: null +} +``` + +You would use the existence of the **user** to determine if you are actually authenticated or not. This _works**,**_ but it does not describe the states of your application explicitly. If you rather describe this state as: + +```typescript +type State = { + current: 'AUTHENTICATING' +} | { + current: 'AUTHENTICATED' + user: { username: string } +} | { + current: 'UNAUTHENTICATED' + signedOutReason: string +} + +export const state: State = { + current: 'AUTHENTICATING' +} +``` + +Now we are describing what states our application can actually be in, and what other state is available at that time. + +State machines does not only help us describe explicit states, they act as a translator between the effects of the outside world and the state of your application. It basically ensures that whatever happens "out there" the state machine will ensure that your state is valid. + +You express this by mapping **events** to **state changes**. + +```typescript +type User = { username: string } + +type States = + | { + current: 'AUTHENTICATING' + } + | { + current: 'AUTHENTICATED' + user: User + } + | { + current: 'UNAUTHENTICATED' + signedOutReason: string + } + +type Events = + | { + type: 'SIGNING_IN' + } + | { + type: 'SIGNED_IN' + data: User + } + | { + type: 'SIGNED_OUT' + data: string + } + +export const auth = statemachine({ + SIGNING_IN: (state) => { + if (state.current === 'UNAUTHENTICATED') { + return { current: 'AUTHENTICATING' } + } + }, + SIGNED_IN: (state, user) => { + if (state.current === 'AUTHENTICATING') { + return { current: 'AUTHENTICATED', user } + } + }, + SIGNED_OUT: (state, signedOutReason) => { + if (state.current === 'AUTHENTICATED') { + return { current: 'UNAUTHENTICATED', signedOutReason } + } + } +}) +``` + +In the example above we are are dealing with three events. For each event we check the current state of the machine to see if we want to deal with it at all. When we decide to deal with an event we can change any of the state, for example using **data** from the event. Then we can optionally return a new **current** transition state, with the required state for that transition state to be valid. + +What we have effectively done now is ensure that when these events happens we always deal with them correctly. It is not the event that decides what should happen, it is the machine that decides it based on one of your explicitly set states. + +## Instantiating a machine + +To actually use the machine as part of your state you need to **create** it. + +```javascript +import { auth } from './state' +import * as actions from './actions' + +const config = { + state: auth.create({ + current: 'AUTHENTICATING' + }) +} +``` + +By explicitly instantiating the machine you are allowed to start it in different transition states and also give preset state if necessary. You will see this becomes beneficial later when nesting machines. + +## Sending events + +Instead of explicitly changing the state, you send an **event**. The events is handled by the state machine and it will ensure that it is valid before moving on. That means when you change from **AUTHENTICATING** to **AUTHENTICATED** you would express it something like: + +```javascript +export const authChanged = ({ state }, user) => { + if (user) { + state.send('SIGNED_IN', user) + } else { + state.send('SIGNED_OUT') + } +} +``` + +When sending the **SIGNED\_IN** event we also provide the **user**. The current transition state of the machine is what decides if the user is set or not. + +## Guarding effects + +Now, your state machine is in charge of how it acts on events coming form the outside world, but you might also want the outside world to react to changes in your state machine. So imagine related to transitioning into a state you wanted to change the title of the page. To ensure this logic only runs when your application actually transitions into the **UNAUTHENTICATED** or **AUTHENTICATED** state we can check if the machine actually is in this state after sending it a message. + +```javascript +export const authChanged = ({ state, effects }, user) => { + if (user && state.send('SIGNED_IN', user).matches('AUTHENTICATED')) { + effects.browser.setTitle('Logged in') + } else if (state.send('SIGNED_OUT').matches('UNAUTHENTICATED')) { + effects.browser.setTitle('Logged out') + } +} +``` + +## Base state + +Let us introduce a new machine, a **todos** machine. + +```typescript +import { Statemachine } from 'overmind' + +type Todo = { title: string, completed: boolean } + +type States = + | { + current: 'LOADING' + } + | { + current: 'LIST' + } + + type BaseState { + list: Todo[] + } + +type Events = + | { + type: 'TODOS_LOADED', + data: Todo[] + } + | { + type: 'TODO_ADDED', + data: Todo + } + +export type TodosMachine = Statemachine + +export const todos = statemachine({ + TODOS_LOADED: (state, todos) => { + if (state.current === 'LOADING') { + return { current: 'LIST', todos } + } + }, + TODO_ADDED: (state, todo) => { + if (state.current === 'LIST') { + state.list.push(todo) + } + } +}) +``` + +In this simple example we introduced a todos machine that starts in a **LOADING** state and will at some point transition into a **LIST** state when the initial todos has been loaded. The machine introduces the concept of **base state**. That means state that is available no matter what transition state the machine is in. The purpose of **base state** is that it simplifies typing and the machine will also automatically remove state related to the current transition state, when transitioning to a new state. In the example above the **user** and the **signedOutReason** is deleted when moving out of **AUTHENTICATED** state. + +## Nesting state machines + +One of the goals of the Overmind implementation of state machines is that the machines becomes a natural part of your state tree. You can define them wherever you would normally define a value. That means you can create nested machines. + +```typescript +import { TodosMachine, todos } from './Todos' + +type States = + | { + current: 'AUTHENTICATING' + } + | { + current: 'AUTHENTICATED' + user: User + todos: TodosMachine + } + | { + current: 'UNAUTHENTICATED' + signedOutReason: string + } + +type Events = {...} + +export const auth = statemachine({ + SIGNING_IN: (state) => { + if (state.current === 'UNAUTHENTICATED') { + return { current: 'AUTHENTICATING' } + } + }, + SIGNED_IN: (state, user) => { + if (state.current === 'AUTHENTICATING') { + return { + current: 'AUTHENTICATED', + user, + todos: todos.create({ current: 'LOADING' }, { todos: [] }) + } + } + }, + SIGNED_OUT: (state, signedOutReason) => { + if (state.current === 'AUTHENTICATED') { + return { current: 'UNAUTHENTICATED', signedOutReason } + } + } +}) +``` + +Note that the **base state** of the **todos** is passed as a second argument. + +Now we can go back to our authentication logic and introduce the loading of our todos. + +```javascript +export const authChanged = async ({ state, effects }, user) => { + if (user && state.send('SIGNED_IN', user).matches('AUTHENTICATED')) { + const todos = await effects.api.getTodos() + state.matches('AUTHENTICATED')?.todos.send('TODOS_LOADED', todos) + } else if (state.send('SIGNED_OUT').matches('UNAUTHENTICATED')) { + effects.browser.setTitle('Logged out') + } +} +``` + +You will notice that with nested machines you will be using **matches** and optional chaining quite a bit. The reason simply being that you will always have to ensure that your machines are in the correct transition state before interacting with any of its state and nested machines. + +## Identifying current state in components + +All state machines has a **current** property. This can be used to evaluate what should be rendered, here shown with React: + +```javascript +export const App = () => { + const { state } = useOvermind() + + if (state.current === 'AUTHENTICATING') { + return
Loading...
+ } + + if (state.current === 'AUTHENTICATED') { + return
You are not authenticated
+ } + + return
Hello there!
+} +``` + +When dealing with nested machines you will have to do nested checks. This might seem unnecessary, maybe you loaded the **Todos** component only when the parent is in **AUTHENTICATED** state, but components can be moved and loaded anywhere, so this ensures it behaves exactly like we want it to. + +```typescript +export const Todos = () => { + const { state } = useOvermind() + + if (!state.current === 'AUTHENTICATED') return null + + return ( +
+ {state.todos.current === 'LOADING' ? 'Loading...' : null} +
    {state.todos.list.map(() => ...)}
+
+ ) +} +``` + +## Strict mode + +In strict mode you are not able to change state in actions, you explicitly have to use a state machine transitions through the **send** API to make state changes. + +```javascript +const overmind = createOvermind(config, { + strict: true +}) +``` + +```javascript +export const authChanged = ({ state }, user) => { + // This would throw an error + state.user = user +} +``` + diff --git a/introduction.md b/introduction.md index 256f924..545c178 100644 --- a/introduction.md +++ b/introduction.md @@ -8,7 +8,7 @@ If you rather want to go right ahead and set up a local project, please have a l Before we move on, have a quick look at this sandbox. It is a simple counter application and it gives you some foundation before talking more about Overmind and building applications. -{% embed url="https://codesandbox.io/s/overmind-counter-c4tuh?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" %} +{% embed url="https://codesandbox.io/s/overmind-counter-c4tuh?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" caption="" %} ## Application state VS Component state @@ -29,7 +29,7 @@ Introducing these scenarios we said: **You want**. In reality we rarely know exa 2. **The active tab should certainly be component state?** The active tab might be part of the url query, `/user?tab=count`. That means it should rather be a hyperlink where your application handles the routing and provides state to identify the active tab. 3. **Inputs should certainly be component state?** If the input is part of an application flow, you might want to empty out the content of that input related to other changes, or even change it to something else. -How you want to go about this is totally up to you. We are not telling you exactly how to separate application and component state. What we can tell you though; **“If you lean towards application state your are more flexible to future changes”**. +How you want to go about this is totally up to you. We are not telling you exactly how to separate application and component state. What we can tell you though; **“If you lean towards application state you are more flexible to future changes”**. ## Defining state @@ -93,7 +93,7 @@ And as we will see later you will also be using **effects** from the context. Now we will move to a more complex example. Please have a look: -{% embed url="https://codesandbox.io/s/overmind-todomvc-simple-097zs?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" %} +{% embed url="https://codesandbox.io/s/overmind-todomvc-simple-097zs?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" caption="" %} We have now separated out the Overmind related logic into its own file, **app.js**. This file creates the Overmind instance and also exports how the components will interact with the state and the actions, the hook called **useApp**. Vue and Angular has other mechanisms conventional to those frameworks where application state and actions can be accessed. @@ -139,7 +139,7 @@ state.todos[myReference] delete state.todos[myReference] ``` -Using references also ensures that only one instance of any todo will live in your state tree. The todo itself lives on the **todos** state, while everything else in the state tree references a todo by using its id. For example our **editingTodoId** state uses the id of a todo to reference which todo is currently being edited. +Using references also ensures that only one instance of any todo will live in your state tree. The todo itself lives on the **todos** state, while everything else in the state tree references a todo by using its id. For example our **editingTodoId** state uses the id of a todo to reference which todo is currently being edited. ## Deriving state @@ -149,7 +149,7 @@ Looking through the example you have probably noticed these: createOvermind({ state: { ..., - currentTodos: ({ todos, filter }) => { + currentTodos: derived(({ todos, filter }) => { return Object.values(todos).filter(todo => { switch (filter) { case 'active': @@ -160,16 +160,16 @@ createOvermind({ return true; } }); - }, - activeTodoCount: ({ todos }) => { + }), + activeTodoCount: derived(({ todos }) => { return Object.values(todos).filter(todo => !todo.completed).length; - }, - hasCompletedTodos: ({ todos }) => { + }), + hasCompletedTodos: derived(({ todos }) => { return Object.values(todos).some(todo => todo.completed); - }, - isAllTodosChecked: ({ currentTodos }) => { + }), + isAllTodosChecked: derived(({ currentTodos }) => { return currentTodos.every(todo => todo.completed); - }, + }), }, ... }) @@ -177,13 +177,13 @@ createOvermind({ Our state tree is concerned with state values that you will change using actions. But you can also automatically produce state values based on existing state. An example of this would be to list the **currentTodos**. It uses the todos and filter state to figure out what todos to actually display. Sometimes this is called computed state. We call it **derived** state. -Any function you insert into the state tree is treated as derived state. That means these functions receives a preset first argument which is the immediate state, the state object the derived is attached to. In bigger applications you might also need to use the second argument, which is the root state of the application. The derived will automatically track whatever state you use and then flag itself as dirty whenever it changes. If derived state is used while being dirty, the function will run again. If it is not dirty a cached value is returned. +By using the **derived** function exported from Overmind you can insert a function into the state tree. These functions receives a preset first argument which is the immediate state, the state object the derived is attached to. In bigger applications you might also need to use the second argument, which is the root state of the application. The derived will automatically track whatever state you use and then flag itself as dirty whenever it changes. If derived state is used while being dirty, the function will run again. If it is not dirty, a cached value is returned. ## Effects Now let us move into an even more complex application. Here we have added **effects**. Specifically effects to handle routing, storing todos to local storage and producing unique ids for the todos. We have added an **onInitialize** hook which is a special function Overmind runs when the application starts. -{% embed url="https://codesandbox.io/s/overmind-todomvc-2im6p?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" %} +{% embed url="https://codesandbox.io/s/overmind-todomvc-2im6p?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" caption="" %} You can think of effects as a contract between your application and the outside world. You write an effect API of **what** your application needs and some 3rd party tool or native JavaScript API will implement **how** to provide it. Let us look at the router: @@ -223,7 +223,7 @@ This argument passed is transformed into something Page can understand. What thi Defining all the state, actions and effects on one object would not work very well for a large application. A convention in Overmind is to split these concepts into different files behind folders representing a domain of the application. In this next sandbox you can see how we split up state, actions and effects into different files. They are all exposed through a main file representing that domain, in this case “the root domain”: -{% embed url="https://codesandbox.io/s/overmind-todomvc-split-xdh41?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" %} +{% embed url="https://codesandbox.io/s/overmind-todomvc-split-xdh41?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" caption="" %} Also notice that we have split up the instantiation of Overmind from the definition of the application. What this allows us to do is reuse the same application configuration for testing purposes and/or server side rendering. We separate the definition from the instantiation. @@ -236,10 +236,10 @@ Now that we have insight into the building blocks of Overmind it is time to intr Have a look at this new project where we have typed the application: {% hint style="info" %} -You have to **OPEN IN EDITOR** to get the full Typescript experience. +You have to **OPEN SANDBOX** to get the full Typescript experience. {% endhint %} -{% embed url="https://codesandbox.io/s/overmind-todomvc-typescript-39h7y?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" %} +{% embed url="https://codesandbox.io/s/overmind-todomvc-typescript-39h7y?fontsize=14&hidenavigation=1&theme=dark&view=editor&runonclick=1" caption="" %} As you can see we only have to add an **Action** type to our functions and optionally give it an input type. This is enough for the action to give you all information about the application. Try changing some code and even add some code to see how Typescript helps you to explore the application and ensure that you implement new functionality correctly. @@ -269,7 +269,9 @@ To learn more about Overmind and Typescript read the [TYPESCRIPT](https://www.ov ## Development tool -Overmind also ships with its own development tool. It can be installed as a [VSCODE PLUGIN](https://marketplace.visualstudio.com/items?itemName=christianalfoni.overmind-devtools-vscode) or installed as an NPM package. The development tool knows everything about what is happening inside the application. It shows you all the state, running actions and connected components. By default Overmind connects automatically to the devtool if it is running. Try now by going to the **index.tsx** file and change: +Overmind also ships with its own development tool. It can be installed as a [VSCODE PLUGIN](https://marketplace.visualstudio.com/items?itemName=christianalfoni.overmind-devtools-vscode) or installed as an NPM package. The development tool knows everything about what is happening inside the application. It shows you all the state, running actions and connected components. By default Overmind connects automatically to the devtool if it is running. + +Open the **sandbox** above and try by going to the **index.tsx** file and change: ```typescript export const overmind = createOvermind(config, { @@ -329,9 +331,11 @@ Our todo has been added and we can even see how the derived state was affected b ## Managing complexity -Overmind gives you a basic foundation with its **state**, **actions** and **effects**. As mentioned previously you can split these up into multiple namespaces to organize your code. This manages the complexity of scaling. There is also a complexity of reusability and managing execution over time. The **operators** API allows you to split your logic into many different composable parts. With operators like **debounce**, **waitUntil** etc. you are able to manage execution over time. With the latest addition of **statecharts** you have the possiblity to manage the complexity of state and interaction. What interactions should be allowed in what states. And with state values as **class instances** you are able to co-locate state with logic. +![](.gitbook/assets/image%20%281%29%20%281%29.png) + +Overmind gives you a basic foundation with its **state**, **actions** and **effects**. As mentioned previously you can split these up into multiple namespaces to organize your code. This manages the complexity of scaling. There is also a complexity of reusability and managing execution over time. The **operators** API allows you to split your logic into many different composable parts. With operators like **debounce**, **waitUntil** etc. you are able to manage execution over time. With the latest addition of **statemachines** you have the possiblity to manage the complexity of state and interaction. What interactions should be allowed in what states. And with state values as **class instances** you are able to co-locate state with logic. -The great thing about Overmind is that none of these concepts are forced upon you. If you want to build your entire app in the root namespace, only using actions, that is perfectly fine. You want to bring in operators for a single action to manage time complexity, do that. Or do you have a concept where you want to safely control what actions can run in certain states, use a statechart. Overmind just gives you tools, it is up to you to determine if they are needed or not. +The great thing about Overmind is that none of these concepts are forced upon you. If you want to build your entire app in the root namespace, only using actions, that is perfectly fine. You want to bring in operators for a single action to manage time complexity, do that. Or do you have a concept where you want to safely control what actions can run in certain states, use a statemachines. Overmind just gives you tools, it is up to you to determine if they are needed or not. ## Moving from here diff --git a/views/angular.md b/views/angular.md index fa6f870..6886502 100644 --- a/views/angular.md +++ b/views/angular.md @@ -1,5 +1,13 @@ # Angular +## Install + +```text +npm install overmind overmind-angular +``` + +## Configure + Let us have a look at how you configure your app: {% tabs %} @@ -44,7 +52,6 @@ import { AppComponent } from './app.component'; ] }) export class AppModule { } - ``` {% endtab %} @@ -91,7 +98,6 @@ platformBrowserDynamic() ngZone: "noop" }) .catch(err => console.log(err)); - ``` {% endtab %} {% endtabs %} @@ -124,6 +130,20 @@ export class AppComponent { You can now access the **admin** state and actions directly with **state** and **actions**. +### Polyfill environment + +Angular does not inject the environment, so in your **polyfill.ts** file you have to add the following: + +```typescript +import { environment } from './environments/environment'; + +(window as any).process = { + env: { + NODE_ENV: environment.production ? 'production' : 'development' + }, +}; +``` + ## NgZone The Overmind **\*track** directive knows when your components should update, and so is much more efficient at change detection than Angular's default NgZone. In order to take advantage of the efficiency provided by the \***track** directive, you _must_ set **ngZone** to "noop". Note that other 3rd party libraries may not support this. If for any reason you can't set **ngZone** to "noop", then the \***track** directive is redundant, and you can safely exclude it from your templates. diff --git a/views/react.md b/views/react.md index 6bf7ed3..1b1f00b 100644 --- a/views/react.md +++ b/views/react.md @@ -1,5 +1,11 @@ # React +## Install + +```text +npm install overmind overmind-react +``` + There are two different ways to connect Overmind to React. You can either use a traditional **Higher Order Component** or you can use the new **hooks** api to expose state and actions. When you connect Overmind to a component you ensure that whenever any tracked state changes, only components interested in that state will re-render, and will do so “at their location in the component tree”. That means we remove a lot of unnecessary work from React. There is no reason for the whole React component tree to re-render when only one component is interested in a change. @@ -10,7 +16,12 @@ When you connect Overmind to a component you ensure that whenever any tracked st {% tab title="Javascript" %} ```typescript // overmind/index.js -import { createHook } from 'overmind-react' +import { + createStateHook, + createActionsHook, + createEffectsHook, + createReactionHook +} from 'overmind-react' import { state } from './state' import * as actions from './actions' @@ -19,7 +30,10 @@ export const config = { actions } -export const useOvermind = createHook() +export const useState = createStateHook() +export const useActions = createActionsHook() +export const useEffects = createEffectsHook() +export const useReaction = createReactionHook() // index.js import * as React from 'react' @@ -39,10 +53,17 @@ render(( // components/App.jsx import * as React from 'react' -import { useOvermind } from '../overmind' +import { useState, useActions, useEffects, useReaction } from '../overmind' const App = () => { - const { state, actions, effects, reaction } = useOvermind() + // General + const state = useState() + const actions = useActions() + const effects = useEffects() + const reaction = useReaction() + // Or be specific + const { isLoggedIn } = useState().auth + const { login, logout } = useActions().auth return
} @@ -69,6 +90,10 @@ declare module 'overmind' { } export const useOvermind = createHook() +export const useState = createStateHook() +export const useActions = createActionsHook() +export const useEffects = createEffectsHook() +export const useReaction = createReactionHook() // index.tsx import * as React from 'react' @@ -91,7 +116,11 @@ import * as React from 'react' import { useOvermind } from '../overmind' const App: React.FunctionComponent = () => { + // General const { state, actions, effects, reaction } = useOvermind() + // Or be specific + const { isLoggedIn } = useState().auth + const { login, logout } = useActions().auth return
} @@ -101,24 +130,32 @@ export default App {% endtab %} {% endtabs %} +{% hint style="info" %} +The benefit of using specific hooks is that if you only need actions in a component, you do not add tracking behaviour to the component by using **useActions**. Also it reduces the amount of destructuring needed, as you can point to a namespace on the hook. +{% endhint %} + ### Rendering When you use the Overmind hook it will ensure that the component will render when any tracked state changes. It will not do anything related to the props passed to the component. That means whenever the parent renders, this component renders as well. You will need to wrap your component with [**REACT.MEMO**](https://reactjs.org/docs/react-api.html#reactmemo) to optimize rendering caused by a parent. ### Passing state as props -If you pass a state object or array as a property to a child component you will also in the child component need to use the **useOvermind** hook to ensure that it is tracked within that component, even though you do not access any state or actions. The devtools will help you identify where any components are left “unconnected”. +If you pass a state object or array as a property to a child component you will also in the child component need to use the **useState** hook to ensure that it is tracked within that component, even though you do not access any state or actions. The devtools will help you identify where any components are left “unconnected”. + +{% hint style="info" %} +Note that when you pass an **array** you will not be observing changes to the array itself, for example adding/removing items. You will only observe any items you access in the array. The same goes for **object**. You are not observing adding/removing keys from the object. Consider passing the parent of the values instead. +{% endhint %} {% tabs %} {% tab title="Javascript" %} ```typescript // components/Todos.jsx import * as React from 'react' -import { useOvermind } from '../overmind' +import { useState } from '../overmind' import Todo from './Todo' const Todos = () => { - const { state } = useOvermind() + const state = useState() return (
    @@ -131,10 +168,10 @@ export default Todos // components/Todo.jsx import * as React from 'react' -import { useOvermind } from '../overmind' +import { useState } from '../overmind' const Todo = ({ todo }) => { - useOvermind() + useState() return
  • {todo.title}
  • } @@ -191,10 +228,10 @@ The hook effect of React gives a natural point of running effects related to sta // components/App.jsx import * as React from 'react' import { useEffect } from 'react' -import { useOvermind } from '../overmind' +import { useState } from '../overmind' const App = () => { - const { state } = useOvermind() + const state = useState() useEffect(() => { document.querySelector('#app').scrollTop = 0 @@ -237,10 +274,10 @@ Here you can also use the traditional approach of subscribing to updates. // components/App.jsx import * as React from 'react' import { useEffect } from 'react' -import { useOvermind } from '../overmind' +import { useReaction } from '../overmind' const Todos = () => { - const { reaction } = useOvermind() + const reaction = useReaction() useEffect(() => { return reaction( diff --git a/views/svelte.md b/views/svelte.md new file mode 100644 index 0000000..55f153b --- /dev/null +++ b/views/svelte.md @@ -0,0 +1,94 @@ +# Svelte + +There are two differente ways to connect Overmind to Svelte. You can use the **reactive declarations** or you can use the **reaction**. + +When you connect Overmind to a component you ensure that whenever any tracked state changes, only components interested in that state will re-render. + +## Reactive declarations + +```javascript +import { createOvermind } from 'overmind' +import { createMixin } from 'overmind-svelte' + +const overmind = { + state: { + count: 0 + }, + actions: { + increase({ state }) { + state.count++; + }, + decrease({ state }) { + state.count--; + } + } +} + +const store = createMixin(createOvermind(overmind)) + +export const state = store.state +export const actions = store.actions +``` + +```javascript + + +

    Count: {count}

    + + +``` + +## Reactions + +```javascript +import { createOvermind } from 'overmind' +import { createMixin } from 'overmind-svelte' + +const overmind = { + state: { + count: 0 + }, + actions: { + increase({ state }) { + state.count++; + }, + decrease({ state }) { + state.count--; + } + } +} + +const store = createMixin(createOvermind(overmind)) + +export const state = store.state +export const actions = store.actions +export const reactions = store.reactions +``` + +```javascript + + +

    Count: {count}

    +

    Doubled: {doubled}

    + + +``` + diff --git a/views/vue.md b/views/vue.md index 21ae0e8..525a5e6 100644 --- a/views/vue.md +++ b/views/vue.md @@ -1,34 +1,173 @@ # Vue -There are two approaches to connecting Overmind to Vue. +## Install -## Plugin +```text +npm install overmind overmind-vue +``` -Vue has a plugin system that allows us to expose Overmind to all components. This allows minimum configuration and you just use state etc. from any component. +There are three approaches to connecting Overmind to Vue. + +## Hooks \(experimental\) {% tabs %} {% tab title="overmind/index.js" %} ```typescript -import { createOvermind } from 'overmind' -import { createPlugin } from 'overmind-vue' +import { createHooks } from 'overmind-vue' -const overmind = createOvermind({ +export const config = { state: { foo: 'bar' }, actions: { onClick() {} } -}) +} -export const OvermindPlugin = createPlugin(overmind) +export const hooks = createHooks() +``` +{% endtab %} + +{% tab title="index.js" %} +```javascript +import { createApp } from 'vue' +import { createOvermind } from 'overmind' +import { withOvermind } from 'overmind-vue' +import { config } from './overmind' +import App from './App.vue' + +const overmind = createOvermind(config) + +createApp(withOvermind(overmind, App)).mount('#app') + +... +``` +{% endtab %} + +{% tab title="components/SomeComponent.vue" %} +```javascript + + +``` +{% endtab %} +{% endtabs %} + +The hooks also allows you to point to specific namespaces: + +{% tabs %} +{% tab title="components/SomeComponent.vue" %} +```javascript + + +``` +{% endtab %} +{% endtabs %} + +You also have **effects** and **reaction** available on your hooks: + +{% tabs %} +{% tab title="components/SomeComponent.vue" %} +```javascript +
    + {{ state.foo }} +
    + + +``` +{% endtab %} +{% endtabs %} + +If you prefer using JSX, that is also possible: + +{% tabs %} +{% tab title="components/SomeComponent.vue" %} +```javascript + +``` +{% endtab %} +{% endtabs %} + +## Plugin + +Vue has a plugin system that allows us to expose Overmind to all components. This allows minimum configuration and you just use state etc. from any component. + +{% tabs %} +{% tab title="overmind/index.js" %} +```typescript +export const overmind = { + state: { + foo: 'bar' + }, + actions: { + onClick() {} + } +} ``` {% endtab %} {% tab title="index.js" %} ```typescript import Vue from 'vue/dist/vue' -import { OvermindPlugin } from './overmind' +import { createOvermind } from 'overmind' +import { createPlugin } from 'overmind-vue' +import { config } from './overmind' + +const overmind = createOvermind(config) +const OvermindPlugin = createPlugin(overmind) Vue.use(OvermindPlugin) @@ -53,7 +192,12 @@ If you rather want to expose state, actions and effects differently you can conf {% tab title="index.js" %} ```typescript import Vue from 'vue/dist/vue' -import { OvermindPlugin } from './overmind' +import { createOvermind } from 'overmind' +import { createPlugin } from 'overmind-vue' +import { config } from './overmind' + +const overmind = createOvermind(config) +const OvermindPlugin = createPlugin(overmind) Vue.use(OvermindPlugin, ({ state, actions, effects }) => ({ admin: state.admin,