|
| 1 | +# Organizing apps |
| 2 | + |
| 3 | +This example shows a structured application using two modules. The **compose** function allows you to namespace your modules. In this example we are not taking any shortcuts. We explicitly split up and type all functions, components etc. By default this typing would be inferred when inlining the functions and components, but we are taking full advantage of the functional approach which makes the application highly testable, predictable and composable. The usage of two modules here is artificial, but important to show you how modules has access to each other. |
| 4 | + |
| 5 | +{% code-tabs %} |
| 6 | +{% code-tabs-item title="index.tsx" %} |
| 7 | +```typescript |
| 8 | +import React from 'react' |
| 9 | +import { render } from 'react-dom' |
| 10 | +import App from './components/App' |
| 11 | + |
| 12 | +render(<App />, document.querySelector('#app')) |
| 13 | +``` |
| 14 | +{% endcode-tabs-item %} |
| 15 | + |
| 16 | +{% code-tabs-item title="overmind.ts" %} |
| 17 | +```typescript |
| 18 | +import Overmind, { IContext, IAction, IConnect, compose } from 'overmind/react' |
| 19 | +import * as main from './modules/main' |
| 20 | +import * as items from './modules/items' |
| 21 | + |
| 22 | +export type AppState = { |
| 23 | + main: main.State, |
| 24 | + items: items.State |
| 25 | +} |
| 26 | + |
| 27 | +export type Context = IContext<AppState> |
| 28 | + |
| 29 | +export type Action = IAction<Context> |
| 30 | + |
| 31 | +const app = new Overmind(compose({ |
| 32 | + main, |
| 33 | + items |
| 34 | +})) |
| 35 | + |
| 36 | +export type Connect = IConnect<typeof app.state, typeof app.actions> |
| 37 | + |
| 38 | +export const connect = app.connect |
| 39 | + |
| 40 | +``` |
| 41 | +{% endcode-tabs-item %} |
| 42 | + |
| 43 | +{% code-tabs-item title="modules/main/index.ts" %} |
| 44 | +```typescript |
| 45 | +import { AppState, Action } from '../../overmind' |
| 46 | + |
| 47 | +/* |
| 48 | + STATE |
| 49 | +*/ |
| 50 | +export type State = { |
| 51 | + newItemValue: string |
| 52 | +} |
| 53 | + |
| 54 | +export const state: State = { |
| 55 | + newItemValue: '' |
| 56 | +} |
| 57 | + |
| 58 | +/* |
| 59 | + MUTATIONS |
| 60 | +*/ |
| 61 | +export const setNewItemValue = (state: AppState, value: string) => state.main.newItemValue = value |
| 62 | + |
| 63 | +/* |
| 64 | + HELPERS |
| 65 | +*/ |
| 66 | +export const getEventValue = (event: React.ChangeEvent) => event.target.value |
| 67 | + |
| 68 | +/* |
| 69 | + ACTIONS |
| 70 | +*/ |
| 71 | +export const actions = (action: Action) => ({ |
| 72 | + changeNewItemValue: action<React.ChangeEvent>() |
| 73 | + .map(getEventValue) |
| 74 | + .mutation(setNewItemValue) |
| 75 | +}) |
| 76 | +``` |
| 77 | +{% endcode-tabs-item %} |
| 78 | + |
| 79 | +{% code-tabs-item title="modules/items/index.ts" %} |
| 80 | +```typescript |
| 81 | +import { AppState, Context, Action } from '../../overmind' |
| 82 | + |
| 83 | +/* |
| 84 | + STATE |
| 85 | +*/ |
| 86 | +export type Item = { |
| 87 | + title: string |
| 88 | +} |
| 89 | + |
| 90 | +export type State = { |
| 91 | + items: Item[] |
| 92 | +} |
| 93 | + |
| 94 | +export const state: State = { |
| 95 | + items: [] |
| 96 | +} |
| 97 | + |
| 98 | +/* |
| 99 | + MUTATIONS |
| 100 | +*/ |
| 101 | +export const addNewItem = (state: AppState, item: Item) => state.items.list.push(item) |
| 102 | + |
| 103 | +export const resetNewItemValue = (state: AppState) => state.main.newItemValue = '' |
| 104 | + |
| 105 | +/* |
| 106 | + HELPERS |
| 107 | +*/ |
| 108 | +export const createItem = (_, { state }: Context) => ({ |
| 109 | + title: state.main.newItemValue |
| 110 | +}) |
| 111 | + |
| 112 | +/* |
| 113 | + ACTIONS |
| 114 | +*/ |
| 115 | +export const actions = (action: Action) => ({ |
| 116 | + addNewItem: action() |
| 117 | + .map(createItem) |
| 118 | + .mutation(addNewItem) |
| 119 | + .mutation(resetNewItemValue) |
| 120 | +}) |
| 121 | +``` |
| 122 | +{% endcode-tabs-item %} |
| 123 | + |
| 124 | +{% code-tabs-item title="components/App/index.tsx" %} |
| 125 | +```typescript |
| 126 | +import React from 'react' |
| 127 | +import { connect, Connect } from '../../overmind' |
| 128 | + |
| 129 | +const App: React.SFC<Connect> = ({ appState, actions }) => ( |
| 130 | + <div> |
| 131 | + <form onSubmit={event => { |
| 132 | + event.preventDefault() |
| 133 | + actions.items.addNewItem() |
| 134 | + }}> |
| 135 | + <input |
| 136 | + onChange={actions.changeNewItemValue} |
| 137 | + value={appState.main.newItemValue} |
| 138 | + /> |
| 139 | + </form> |
| 140 | + <ul> |
| 141 | + {appState.items.list.map(item => ( |
| 142 | + <li>{item.title}</li> |
| 143 | + ))} |
| 144 | + </ul> |
| 145 | + </div> |
| 146 | +) |
| 147 | + |
| 148 | +export default connect(App) |
| 149 | +``` |
| 150 | +{% endcode-tabs-item %} |
| 151 | +{% endcode-tabs %} |
| 152 | + |
| 153 | +What to take notice of: |
| 154 | + |
| 155 | +* We are not interfering in any way with the initial rendering of the app, meaning you just connect state where you need it |
| 156 | +* By default Overmind takes a single module with state, actions etc., but **compose** allows us to merge multiple modules together, giving them a namespace \("main" and "items" in this example\) |
| 157 | +* We separate mutations and other side effects. This makes it absolutely clear where mutations are performed and only the **mutation** operator is allowed to perform these mutations. |
| 158 | +* The actions are now just plain functions taking any payload. When the action is typed it requires a value, when it is not typed, it does not require a value \(this is actually very difficult to do in TypeScript\) |
| 159 | + |
| 160 | + |
| 161 | + |
0 commit comments