├── comparison.png ├── .gitignore ├── .travis.yml ├── tsconfig.json ├── src ├── react.ts ├── index.ts ├── index.test.ts └── react.test.tsx ├── LICENSE ├── package.json ├── README.md └── yarn.lock /comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gunn/pure-store/HEAD/comparison.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | /coverage 4 | /index.js 5 | /index.d.ts 6 | /react.js 7 | /react.d.ts 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | env: 5 | - NODE_ENV=TEST 6 | cache: 7 | directories: 8 | - "node_modules" 9 | script: yarn test && yarn coveralls 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "noImplicitAny": false, 8 | "sourceMap": false, 9 | "declaration": true, 10 | "rootDir": "src", 11 | "lib": [ 12 | "es2017", 13 | "dom" 14 | ] 15 | }, 16 | "include": [ 17 | "src/*" 18 | ], 19 | "exclude": [ 20 | "src/*.test.*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/react.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { PureStore } from "./index" 3 | 4 | 5 | class PureStoreReact extends PureStore { 6 | usePureStore (): readonly [T, (updater: Partial | ((e: T) => void)) => void] 7 | usePureStore (getter?: (s: T)=>X): readonly [X, (updater: Partial | ((e: X) => void)) => void] 8 | usePureStore(getter?) { 9 | if (getter) { 10 | return new PureStoreReact(this, getter).usePureStore() 11 | } 12 | 13 | const [_, setState] = React.useState(this.getState()) 14 | 15 | React.useEffect(()=> { 16 | return this.subscribe(()=> setState(this.getState())) 17 | }, []) 18 | 19 | return [this.getState(), this.update] as const 20 | } 21 | } 22 | 23 | 24 | export default (state: S)=> ( 25 | new PureStoreReact(null, (s: S)=> s, state) 26 | ) 27 | 28 | export { PureStoreReact as PureStore } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Arthur Gunn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pure-store", 3 | "version": "1.2.0", 4 | "description": "A tiny immutable store with type safety.", 5 | "main": "dist/index.js", 6 | "module": "index.js", 7 | "types": "index.d.ts", 8 | "sideEffects": false, 9 | "repository": "https://github.com/gunn/pure-store", 10 | "author": "Arthur Gunn", 11 | "license": "MIT", 12 | "scripts": { 13 | "build": "tsc --outDir dist && yarn build-esm", 14 | "build-esm": "tsc -m es2015 --outDir .", 15 | "test": "jest", 16 | "prepublish": "yarn test && yarn build", 17 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" 18 | }, 19 | "jest": { 20 | "transform": { 21 | "^.+\\.tsx?$": "ts-jest" 22 | }, 23 | "testRegex": "((\\.|/)test)\\.tsx?$", 24 | "moduleFileExtensions": [ 25 | "ts", 26 | "tsx", 27 | "js" 28 | ] 29 | }, 30 | "files": [ 31 | "src", 32 | "dist", 33 | "index.js", 34 | "index.d.ts", 35 | "react.js", 36 | "react.d.ts" 37 | ], 38 | "devDependencies": { 39 | "@types/jest": "^25.2.1", 40 | "@types/react": "^16.9.41", 41 | "@types/react-dom": "^16.9.8", 42 | "coveralls": "^3.0.13", 43 | "jest": "^25.4.0", 44 | "react": "^16.13.1", 45 | "react-dom": "^16.13.1", 46 | "ts-jest": "^25.4.0", 47 | "typescript": "^3.8.3" 48 | }, 49 | "dependencies": { 50 | "immer": "1.8.0 - 7" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer" 2 | 3 | export class PureStore { 4 | callbacks = [] 5 | rootState: T 6 | getter: (s: S)=>T 7 | root: any 8 | parent: any 9 | 10 | constructor(parent, getter: (s: S)=>T, rootState?: T) { 11 | this.parent = parent 12 | this.root = (parent && parent.root) || this 13 | if (!parent) this.rootState = rootState 14 | this.getter = (s: S)=> getter(parent ? parent.getter(s) : s) 15 | } 16 | 17 | getState = ()=> this.getter(this.root.rootState) 18 | get state() { return this.getState() } 19 | 20 | update = (updater: ((e: T)=> void)|Partial)=> { 21 | const updaterFn = (updater instanceof Function) ? 22 | updater : e=> Object.assign(e, updater) 23 | 24 | const oldState = this.root.rootState 25 | 26 | this.root.rootState = produce(this.root.rootState, s=> { 27 | updaterFn(this.getter(s)) 28 | }) 29 | 30 | if (this.root.rootState !== oldState) { 31 | this.root.callbacks.forEach(callback=> callback()) 32 | } 33 | } 34 | 35 | storeFor = (getter: (s: T)=>X)=> new PureStore(this, getter) 36 | updaterFor = (getter: (s: T)=>X)=> this.storeFor(getter).update 37 | 38 | subscribe = callback=> { 39 | this.root.callbacks.push(callback) 40 | return ()=> this.root.callbacks.splice(this.root.callbacks.indexOf(callback), 1) 41 | } 42 | } 43 | 44 | export default (state: S)=> ( 45 | new PureStore(null, (s: S)=> s, state) 46 | ) 47 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import createStore from "./index" 2 | 3 | 4 | interface State { 5 | numberOfWalks: number 6 | animals: { 7 | name: string 8 | age: number 9 | isBad?: boolean 10 | }[] 11 | } 12 | 13 | const state: State = { 14 | numberOfWalks: 2876, 15 | animals: [ 16 | { 17 | name: "Aiofe", 18 | age: 6 19 | }, 20 | { 21 | name: "Sen", 22 | age: 8 23 | } 24 | ] 25 | } 26 | 27 | 28 | const store = createStore(state) 29 | 30 | 31 | test("Allows access to state", ()=> { 32 | const storeState = store.state 33 | 34 | expect(storeState.numberOfWalks).toBe(2876) 35 | expect(storeState.animals.length).toBe(2) 36 | expect(storeState.animals[1].name).toBe("Sen") 37 | }) 38 | 39 | test("Allow function updates", ()=> { 40 | expect(store.state.numberOfWalks).toBe(2876) 41 | 42 | store.update(s=> s.numberOfWalks++) 43 | 44 | expect(store.state.numberOfWalks).toBe(2877) 45 | }) 46 | 47 | test("Allow object updates", ()=> { 48 | store.update({numberOfWalks: 100}) 49 | 50 | expect(store.state.numberOfWalks).toBe(100) 51 | }) 52 | 53 | test("More Updating", ()=> { 54 | store.update(s=> s.animals.push({name: "Odin", age: 5})) 55 | 56 | expect(store.state.animals.length).toBe(3) 57 | }) 58 | 59 | describe("Sub-stores", ()=> { 60 | test("Basic usage", ()=> { 61 | const odinStore = store.storeFor(s=> s.animals[2]) 62 | 63 | expect(odinStore.state.name).toBe("Odin") 64 | expect(odinStore.state.isBad).toBe(undefined) 65 | 66 | odinStore.update({isBad: true}) 67 | expect(odinStore.state.isBad).toBe(true) 68 | }) 69 | 70 | test("a sub-sub-store", ()=> { 71 | const animalsStore = store.storeFor(s=> s.animals) 72 | const aiofeStore = animalsStore.storeFor(s=> s[0]) 73 | 74 | expect(aiofeStore.state.name).toBe("Aiofe") 75 | }) 76 | }) 77 | 78 | describe("A store for a single property", ()=> { 79 | const walksStore = store.storeFor(s=> s.numberOfWalks) 80 | 81 | test("Can get a single property's state", ()=> { 82 | expect(walksStore.state).toBe(100) 83 | }) 84 | 85 | test("Can't update a store with a single property", ()=> { 86 | walksStore.update(n=> n += 50) 87 | 88 | expect(walksStore.state).not.toBe(150) 89 | }) 90 | }) 91 | 92 | 93 | describe("Immutability", ()=> { 94 | const senStore = store.storeFor(s=> s.animals[1]) 95 | 96 | test("Unmodified objects stay the same", ()=> { 97 | const before = senStore.state 98 | const rootBefore = store.state.animals[1] 99 | expect(before).toBe(rootBefore) 100 | 101 | expect(before).toBe(senStore.state) 102 | expect(rootBefore).toBe(store.state.animals[1]) 103 | }) 104 | 105 | test("Modified objects are different objects", ()=> { 106 | const before = senStore.state 107 | const rootBefore = store.state.animals[1] 108 | expect(before).toBe(rootBefore) 109 | 110 | senStore.update({isBad: false}) 111 | 112 | expect(before).not.toBe(senStore.state) 113 | expect(rootBefore).not.toBe(store.state.animals[1]) 114 | }) 115 | }) 116 | 117 | test("Updaters", ()=> { 118 | const aiofeUpdater = store.updaterFor(s=> s.animals[0]) 119 | 120 | expect(store.state.animals[0].isBad).toBe(undefined) 121 | 122 | aiofeUpdater(aiofe=> aiofe.isBad = false) 123 | expect(store.state.animals[0].isBad).toBe(false) 124 | }) 125 | 126 | describe("Subscriptions", ()=> { 127 | test("subscriptions are called", ()=> { 128 | let callbackCalled = false 129 | store.subscribe(()=> callbackCalled = true) 130 | expect(callbackCalled).toBe(false) 131 | 132 | store.update(s=> s.numberOfWalks++) 133 | expect(callbackCalled).toBe(true) 134 | }) 135 | 136 | test("subscriptions aren't called if data isn't changed", ()=> { 137 | let callbackCalled = false 138 | store.subscribe(()=> callbackCalled = true) 139 | expect(callbackCalled).toBe(false) 140 | 141 | const numberOfWalks = store.state.numberOfWalks 142 | store.update({ numberOfWalks }) 143 | expect(callbackCalled).toBe(false) 144 | }) 145 | 146 | test("updating with a sub-store trigger subscribed callbacks", ()=> { 147 | const senStore = store.storeFor(s=> s.animals[1]) 148 | let callbackCalled = false 149 | store.subscribe(()=> callbackCalled = true) 150 | senStore.update(s=> s.age++) 151 | 152 | expect(callbackCalled).toBe(true) 153 | }) 154 | 155 | test('subscriptions can be cancelled', ()=> { 156 | let count = 0 157 | const cancelSubscription = store.subscribe(()=> count++) 158 | 159 | store.update(s=> s.numberOfWalks++) 160 | store.update(s=> s.numberOfWalks++) 161 | expect(count).toBe(2) 162 | 163 | cancelSubscription() 164 | store.update(s=> s.numberOfWalks++) 165 | expect(count).toBe(2) 166 | }) 167 | 168 | test("sub-stores can be subscribed to", ()=> { 169 | const senStore = store.storeFor(s=> s.animals[1]) 170 | let callbackCalled = false 171 | senStore.subscribe(()=> callbackCalled = true) 172 | senStore.update(s=> s.age++) 173 | 174 | expect(callbackCalled).toBe(true) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /src/react.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import { act } from 'react-dom/test-utils' 4 | 5 | import createStore from "./react" 6 | 7 | 8 | 9 | const counterStore = createStore({ count: 0 }) 10 | const Counter = ()=> { 11 | const [state, update] = counterStore.usePureStore() 12 | 13 | return ( 14 |
15 | {state.count} 16 | 17 | 18 |
19 | ) 20 | } 21 | 22 | 23 | 24 | let container: HTMLDivElement 25 | 26 | beforeEach(()=> { 27 | container = document.createElement('div') 28 | document.body.appendChild(container) 29 | }) 30 | 31 | afterEach(()=> { 32 | document.body.removeChild(container) 33 | container = null 34 | }) 35 | 36 | describe("Using a component with usePureStore()", ()=> { 37 | test("Renders as expected", ()=> { 38 | act(()=> { ReactDOM.render(, container) }) 39 | 40 | const count = container.querySelector("span").textContent 41 | expect(count).toBe("0") 42 | }) 43 | 44 | test("Works with interaction and updates", ()=> { 45 | act(()=> { ReactDOM.render(, container) }) 46 | 47 | const [inc, dec] = container.querySelectorAll("button") as unknown as HTMLButtonElement[] 48 | 49 | act(()=> { 50 | for (let i=0; i<12; i++) { 51 | inc.dispatchEvent(new MouseEvent('click', { bubbles: true })) 52 | } 53 | 54 | dec.dispatchEvent(new MouseEvent('click', { bubbles: true })) 55 | }) 56 | 57 | const count = container.querySelector("span").textContent 58 | expect(count).toBe("11") 59 | }) 60 | }) 61 | 62 | 63 | 64 | const store = createStore({ 65 | lists: [ 66 | { 67 | date: new Date(), 68 | completed: false, 69 | items: [ 70 | { 71 | name: "eggs", 72 | purchased: false 73 | }, 74 | { 75 | name: "bread", 76 | purchased: true 77 | }, 78 | { 79 | name: "eggs", 80 | purchased: false 81 | } 82 | ] 83 | }, 84 | { 85 | date: new Date(+new Date() - 86400000), 86 | completed: true, 87 | items: [ 88 | { 89 | name: "vinegar", 90 | purchased: true 91 | } 92 | ] 93 | } 94 | ] 95 | }) 96 | const ShoppingApp = ()=> { 97 | const [state] = store.usePureStore() 98 | 99 | return ( 100 |
101 |

102 | Shopping 103 | 104 | { state.lists.length } { "lists, " } 105 | { state.lists.filter(d=> !d.completed).length } uncompleted 106 | 107 |

108 | 109 | 110 |
111 | ) 112 | } 113 | 114 | const NextDueList = React.memo(()=> { 115 | const [state, update] = store.usePureStore(s=> s.lists.find(l=> l.completed==false)) 116 | 117 | // If there are no lists due: 118 | if (!state) return null 119 | 120 | const {date, completed, items} = state 121 | 122 | return ( 123 |
124 |

125 | List for {date.toLocaleString()} 126 | 127 | { items.length } items, { items.filter(d=> !d.purchased).length } unpurchased 128 | 129 |

130 | 131 | update({ completed: !completed })} 135 | /> 136 | 137 |
    138 | { 139 | items.map((item, i)=> 140 |
  • 141 | 150 |
  • 151 | ) 152 | } 153 |
154 |
155 | ) 156 | }) 157 | 158 | 159 | describe("Using usePureStore with a getter", ()=> { 160 | test("Renders as expected", ()=> { 161 | act(()=> { ReactDOM.render(, container) }) 162 | 163 | const text = container.querySelector("h1 small").textContent 164 | expect(text).toBe("2 lists, 1 uncompleted") 165 | 166 | const listDescription = container.querySelector("h3 small").textContent 167 | expect(listDescription).toBe("3 items, 2 unpurchased") 168 | }) 169 | 170 | test("Works with interaction and updates", async ()=> { 171 | act(()=> { ReactDOM.render(, container) }) 172 | 173 | act(()=> { 174 | const eggCheckbox = container.querySelector(".list ul input") 175 | 176 | eggCheckbox.dispatchEvent(new MouseEvent('click', { bubbles: true })) 177 | }) 178 | 179 | const listDescription = container.querySelector("h3 small").textContent 180 | expect(listDescription).toBe("3 items, 1 unpurchased") 181 | 182 | 183 | act(()=> { 184 | const checkbox = container.querySelector(".list > input") 185 | checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true })) 186 | }) 187 | 188 | const appDescription = container.querySelector("h1 small").textContent 189 | expect(appDescription).toBe("2 lists, 0 uncompleted") 190 | }) 191 | }) 192 | 193 | 194 | 195 | const renderStore = createStore({ count: 0 }) 196 | const RenderTest = React.memo(()=> { 197 | const [{ count }] = renderStore.usePureStore() 198 | 199 | return { count } - { Math.random() } 200 | }) 201 | 202 | 203 | describe("Using a component with usePureStore()", ()=> { 204 | test("Re-renders with new values", ()=> { 205 | act(()=> { ReactDOM.render(, container) }) 206 | const value1 = container.querySelector("span").textContent 207 | 208 | act(()=> { 209 | renderStore.update(s=> s.count++) 210 | ReactDOM.render(, container) 211 | }) 212 | const value2 = container.querySelector("span").textContent 213 | 214 | expect(value1).not.toBe(value2) 215 | }) 216 | 217 | test("Does not re-render with unchanged values", ()=> { 218 | act(()=> { ReactDOM.render(, container) }) 219 | const value1 = container.querySelector("span").textContent 220 | 221 | act(()=> { 222 | renderStore.update(s=> s.count = s.count) 223 | ReactDOM.render(, container) 224 | }) 225 | const value2 = container.querySelector("span").textContent 226 | 227 | expect(value1).toBe(value2) 228 | }) 229 | }) 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pure-store 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/gunn/pure-store/badge.svg?branch=master)](https://coveralls.io/github/gunn/pure-store?branch=master) 4 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/pure-store?color=%23f60)](https://bundlephobia.com/result?p=pure-store) 5 | [![Build Status](https://travis-ci.org/gunn/pure-store.svg?branch=master)](https://travis-ci.org/gunn/pure-store) 6 | [![npm](https://img.shields.io/npm/v/pure-store.svg)](https://www.npmjs.com/package/pure-store) 7 | [![mit](https://img.shields.io/npm/l/pure-store.svg)](https://opensource.org/licenses/MIT) 8 | [![typescript](https://img.shields.io/badge/TypeScript-%E2%9C%93-007ACC.svg)](https://www.typescriptlang.org/) 9 | 10 | > Just edit your app's state. 11 | 12 | `pure-store` is a fast, simple, immutable store that lets you update state directly (i.e. imperatively). It also works excellently with typescript. 13 | 14 | ## Comparison with redux 15 | 16 | 17 | ## With React Hooks 18 | `pure-store` can be used without react, but if you are using react you can use the `usePureStore` hook. We could create the simple counter from the image above like this: 19 | ```javascript 20 | import createStore from "pure-store/react" 21 | 22 | const store = createStore({ count: 0 }) 23 | 24 | export default ()=> { 25 | const [state, update] = store.usePureStore() 26 | 27 | return ( 28 |
29 | Counter: { state.count } 30 | update({count: count+1})}> + 31 | update({count: count-1})}> - 32 |
33 | ) 34 | } 35 | ``` 36 | If you use react, then congratulations - you know everything you need to to manage state in your app. Because the data is updated immutably, you can pass pieces of the store's state to your `React.memo` components and they will re-render only when the data has changed giving you excellent performance. 37 | 38 | ## Without Hooks 39 | To use `pure-store` without react hooks you need to create a store, and know how to use a couple of methods. 40 | 41 | ### `createStore(initialState)` 42 | Creates a new store with an initial state. You can create multiple independent stores, although usually one is enough. 43 | 44 | ```javascript 45 | import createStore from 'pure-store' 46 | 47 | const store = createStore({ count: 0 }) 48 | ``` 49 | 50 | If you're using typescript, you can get type checking and autocompletion automatically with the rest of your `pure-store` usage: 51 | ```typescript 52 | interface State { 53 | user: User 54 | messages: { 55 | user: User 56 | text: string 57 | starred?: boolean 58 | }[] 59 | lastMessageAt?: Date 60 | messageCount: number 61 | } 62 | 63 | const state: State = { 64 | user: getUser(), 65 | messages: [] 66 | } 67 | 68 | const store = createStore(state) 69 | ``` 70 | 71 | ### `state` / `getState()` 72 | Returns the current state from the store. 73 | 74 | ```jsx 75 | console.log("last message date:", store.getState().lastMessageAt) 76 | 77 | const Messages = ()=> { 78 | const { user, messages, lastMessageAt } = store.state 79 | 80 | return ( 81 |
82 |

Messages for {user.name}

83 |
    84 | { 85 | messages.map(m=> ( 86 | 87 | )) 88 | } 89 |
90 |
91 | ) 92 | } 93 | ``` 94 | 95 | ### `update(updater)` 96 | Use this anytime you want to update store data. The `updater` argument can either be an object in which case it works just like react's `setState`, or it can be a function, in which case it's given a copy of the state which can be modified directly. 97 | 98 | ```javascript 99 | store.update({ lastMessageAt: new Date() }) 100 | 101 | store.update(s=> { 102 | s.messageCount++ 103 | s.messages.push(message) 104 | s.lastMessageAt = new Date() 105 | }) 106 | ``` 107 | 108 | ### `subscribe(callback)` 109 | To re-render components when you update the store, you should subscribe to the store. The `subscribe` method takes a callback that takes no arguments. It returns a method to remove that subscription. You can subscribe many times to one store. 110 | 111 | The recommended way is to re-render your whole app - `pure-store` can make this very efficient because immutable state lets you use `React.PureComponent` classes. 112 | ```javascript 113 | const render = ()=> { 114 | ReactDOM.render(, document.getElementById('root')) 115 | } 116 | 117 | store.subscribe(render) 118 | render() 119 | ``` 120 | 121 | You could also use forceUpdate within a component e.g.: 122 | ```javascript 123 | class App extends React.Component { 124 | constructor() { 125 | store.subscribe(()=> this.forceUpdate()) 126 | } 127 | //... 128 | ``` 129 | 130 | ### bonus: `storeFor(getter)`, `updaterFor(getter)`, and usePureStore(getter) 131 | These methods let you define a subset of the store as a shortcut, so you don't have to reference the whole chain every time. 132 | 133 | ```javascript 134 | console.log(store.user.profile.address.city) 135 | store.update(s=> s.user.profile.address.city = "Wellington") 136 | 137 | // vs 138 | const addressStore = store.storeFor(s=> s.user.profile.address) 139 | const addressUpdater = store.updaterFor(s=> s.user.profile.address) 140 | 141 | // and then: 142 | console.log(addressStore.state.city) 143 | addressUpdater(a=> a.city = "Wellington") 144 | ``` 145 | Which can be useful in larger projects. 146 | 147 | ## Patterns 148 | 149 | ### Actions 150 | Other state management libraries have a concept of using 'actions' and 'action creators' for controlled state updates. You may well find them unnecessary, but if you miss them, you can easily do something similar: 151 | 152 | ```jsx 153 | // actions.js 154 | import store from './store' 155 | export function postMessage(text) { 156 | store.update(s=> { 157 | s.messages.push({ 158 | user: s.user, 159 | text 160 | }) 161 | s.lastMessageAt = new Date() 162 | }) 163 | } 164 | 165 | // component.js 166 | //... 167 |