├── .babelrc ├── .babelrc.js ├── .flowconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── unstated.js └── unstated.tsx ├── example ├── complex.js ├── index.html ├── shared.js └── simple.js ├── flow-typed └── npm │ └── jest_v22.x.x.js ├── logo.png ├── logo.svg ├── package.json ├── rollup.config.js ├── src ├── unstated.d.ts └── unstated.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["./.babelrc.js"] } 2 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { BABEL_ENV, NODE_ENV } = process.env; 2 | const cjs = BABEL_ENV === 'cjs' || NODE_ENV === 'test'; 3 | 4 | module.exports = { 5 | presets: [ 6 | [ 7 | 'env', 8 | { 9 | modules: false, 10 | loose: true, 11 | targets: { 12 | browsers: ['last 1 version'] 13 | } 14 | } 15 | ], 16 | 'flow', 17 | 'react' 18 | ], 19 | plugins: [ 20 | 'transform-class-properties', 21 | cjs && 'transform-es2015-modules-commonjs' 22 | ].filter(Boolean) 23 | }; 24 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | .cache 5 | dist 6 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 1 3 | sudo: false 4 | language: node_js 5 | node_js: 6 | - '8' 7 | cache: 8 | yarn: true 9 | directories: 10 | - node_modules 11 | script: 12 | - yarn test --coverage && yarn flow 13 | - yarn typescript 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present James Kyle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |




3 | Unstated Logo 4 |







5 |
6 | 7 | # Unstated 8 | 9 | > State so simple, it goes without saying 10 | 11 | ### :wave: [Check out the *-next version of Unstated with an all new React Hooks API →](https://github.com/jamiebuilds/unstated-next) 12 | 13 | ## Installation 14 | 15 | ```sh 16 | yarn add unstated 17 | ``` 18 | 19 | ## Example 20 | 21 | ```jsx 22 | // @flow 23 | import React from 'react'; 24 | import { render } from 'react-dom'; 25 | import { Provider, Subscribe, Container } from 'unstated'; 26 | 27 | type CounterState = { 28 | count: number 29 | }; 30 | 31 | class CounterContainer extends Container { 32 | state = { 33 | count: 0 34 | }; 35 | 36 | increment() { 37 | this.setState({ count: this.state.count + 1 }); 38 | } 39 | 40 | decrement() { 41 | this.setState({ count: this.state.count - 1 }); 42 | } 43 | } 44 | 45 | function Counter() { 46 | return ( 47 | 48 | {counter => ( 49 |
50 | 51 | {counter.state.count} 52 | 53 |
54 | )} 55 |
56 | ); 57 | } 58 | 59 | render( 60 | 61 | 62 | , 63 | document.getElementById('root') 64 | ); 65 | ``` 66 | 67 | For more examples, see the `example/` directory. 68 | 69 | ## Happy Customers 70 | 71 |

72 | "Unstated is a breath of fresh air for state management. I rewrote my whole app to use it yesterday." 73 |

74 | Sindre Sorhus 75 |

76 | 77 |

78 | "When people say you don't need Redux most of the time, they actually mean you do need Unstated.
It's like setState on fucking horse steroids" 79 |

80 | Ken Wheeler (obviously) 81 |

82 | 83 | ## Guide 84 | 85 | If you're like me, you're sick of all the ceremony around state management in 86 | React, you want something that fits in well with the React way of thinking, 87 | but doesn't command some crazy architecture and methodology. 88 | 89 | So first off: Component state is nice! It makes sense and people can pick it 90 | up quickly: 91 | 92 | ```jsx 93 | class Counter extends React.Component { 94 | state = { count: 0 }; 95 | increment = () => { 96 | this.setState({ count: this.state.count + 1 }); 97 | }; 98 | decrement = () => { 99 | this.setState({ count: this.state.count - 1 }); 100 | }; 101 | render() { 102 | return ( 103 |
104 | {this.state.count} 105 | 106 | 107 |
108 | ); 109 | } 110 | } 111 | ``` 112 | 113 | As a new React developer you might not know exactly how everything works, but 114 | you can get a general sense pretty quickly. 115 | 116 | The only problem here is that we can't easily share this state with other 117 | components in our tree. Which is intentional! React components are designed to 118 | be very self-contained. 119 | 120 | What would be great is if we could replicate the nice parts of React's 121 | component state API while sharing it across multiple components. 122 | 123 | But how do we share values between components in React? Through "context". 124 | 125 | > **Note:** The following is part of the new `React.createContext` API 126 | > [described in this RFC](https://github.com/reactjs/rfcs/blob/master/text/0002-new-version-of-context.md). 127 | 128 | ```jsx 129 | const Amount = React.createContext(1); 130 | 131 | class Counter extends React.Component { 132 | state = { count: 0 }; 133 | increment = amount => { this.setState({ count: this.state.count + amount }); }; 134 | decrement = amount => { this.setState({ count: this.state.count - amount }); }; 135 | render() { 136 | return ( 137 | 138 | {amount => ( 139 |
140 | {this.state.count} 141 | 142 | 143 |
144 | )} 145 |
146 | ); 147 | } 148 | } 149 | 150 | class AmountAdjuster extends React.Component { 151 | state = { amount: 0 }; 152 | handleChange = event => { 153 | this.setState({ 154 | amount: parseInt(event.currentTarget.value, 10) 155 | }); 156 | }; 157 | render() { 158 | return ( 159 | 160 |
161 | {this.props.children} 162 | 163 |
164 |
165 | ); 166 | } 167 | } 168 | 169 | render( 170 | 171 | 172 | 173 | ); 174 | ``` 175 | 176 | This is already pretty great. Once you get a little bit used to React's way of 177 | thinking, it makes total sense and it's very predictable. 178 | 179 | But can we build on this pattern to make something even nicer? 180 | 181 | ### Introducing Unstated 182 | 183 | Well this is where Unstated comes in. 184 | 185 | Unstated is designed to build on top of the patterns already set out by React 186 | components and context. 187 | 188 | It has three pieces: 189 | 190 | ##### `Container` 191 | 192 | We're going to want another place to store our state and some of the logic for 193 | updating it. 194 | 195 | `Container` is a very simple class which is meant to look just like 196 | `React.Component` but with only the state-related bits: `this.state` and 197 | `this.setState`. 198 | 199 | ```js 200 | class CounterContainer extends Container { 201 | state = { count: 0 }; 202 | increment = () => { 203 | this.setState({ count: this.state.count + 1 }); 204 | }; 205 | decrement = () => { 206 | this.setState({ count: this.state.count - 1 }); 207 | }; 208 | } 209 | ``` 210 | 211 | Behind the scenes our `Container`s are also event emitters that our app can 212 | subscribe to for updates. When you call `setState` it triggers components to 213 | re-render, be careful not to mutate `this.state` directly or your components 214 | won't re-render. 215 | 216 | ###### `setState()` 217 | 218 | `setState()` in `Container` mimics React's `setState()` method as closely as 219 | possible. 220 | 221 | ```js 222 | class CounterContainer extends Container { 223 | state = { count: 0 }; 224 | increment = () => { 225 | this.setState( 226 | state => { 227 | return { count: state.count + 1 }; 228 | }, 229 | () => { 230 | console.log('Updated!'); 231 | } 232 | ); 233 | }; 234 | } 235 | ``` 236 | 237 | It's also run asynchronously, so you need to follow the same rules as React. 238 | 239 | **Don't read state immediately after setting it** 240 | 241 | ```js 242 | class CounterContainer extends Container { 243 | state = { count: 0 }; 244 | increment = () => { 245 | this.setState({ count: 1 }); 246 | console.log(this.state.count); // 0 247 | }; 248 | } 249 | ``` 250 | 251 | **If you are using previous state to calculate the next state, use the function form** 252 | 253 | ```js 254 | class CounterContainer extends Container { 255 | state = { count: 0 }; 256 | increment = () => { 257 | this.setState(state => { 258 | return { count: state.count + 1 }; 259 | }); 260 | }; 261 | } 262 | ``` 263 | 264 | However, unlike React's `setState()` Unstated's `setState()` returns a promise, 265 | so you can `await` it like this: 266 | 267 | ```js 268 | class CounterContainer extends Container { 269 | state = { count: 0 }; 270 | increment = async () => { 271 | await this.setState({ count: 1 }); 272 | console.log(this.state.count); // 1 273 | }; 274 | } 275 | ``` 276 | 277 | Async functions are now available in [all the major browsers](https://caniuse.com/#feat=async-functions), 278 | but you can also use [Babel](http://babeljs.io) to compile them down to 279 | something that works in every browser. 280 | 281 | ##### `` 282 | 283 | Next we'll need a piece to introduce our state back into the tree so that: 284 | 285 | * When state changes, our components re-render. 286 | * We can depend on our container's state. 287 | * We can call methods on our container. 288 | 289 | For this we have the `` component which allows us to pass our 290 | container classes/instances and receive instances of them in the tree. 291 | 292 | ```jsx 293 | function Counter() { 294 | return ( 295 | 296 | {counter => ( 297 |
298 | {counter.state.count} 299 | 300 | 301 |
302 | )} 303 |
304 | ); 305 | } 306 | ``` 307 | 308 | `` will automatically construct our container and listen for changes. 309 | 310 | ##### `` 311 | 312 | The final piece that we'll need is something to store all of our instances 313 | internally. For this we have ``. 314 | 315 | ```jsx 316 | render( 317 | 318 | 319 | 320 | ); 321 | ``` 322 | 323 | We can do some interesting things with `` as well like dependency 324 | injection: 325 | 326 | ```jsx 327 | let counter = new CounterContainer(); 328 | 329 | render( 330 | 331 | 332 | 333 | ); 334 | ``` 335 | 336 | ### Testing 337 | 338 | Whenever we consider the way that we write the state in our apps we should be 339 | thinking about testing. 340 | 341 | We want to make sure that our state containers have a clean way 342 | 343 | Well because our containers are very simple classes, we can construct them in 344 | tests and assert different things about them very easily. 345 | 346 | ```js 347 | test('counter', async () => { 348 | let counter = new CounterContainer(); 349 | assert(counter.state.count === 0); 350 | 351 | await counter.increment(); 352 | assert(counter.state.count === 1); 353 | 354 | await counter.decrement(); 355 | assert(counter.state.count === 0); 356 | }); 357 | ``` 358 | 359 | If we want to test the relationship between our container and the component 360 | we can again construct our own instance and inject it into the tree. 361 | 362 | ```js 363 | test('counter', async () => { 364 | let counter = new CounterContainer(); 365 | let tree = render( 366 | 367 | 368 | 369 | ); 370 | 371 | await click(tree, '#increment'); 372 | assert(counter.state.count === 1); 373 | 374 | await click(tree, '#decrement'); 375 | assert(counter.state.count === 0); 376 | }); 377 | ``` 378 | 379 | Dependency injection is useful in many ways. Like if we wanted to stub out a 380 | method in our state container we can do that painlessly. 381 | 382 | ```js 383 | test('counter', async () => { 384 | let counter = new CounterContainer(); 385 | let inc = stub(counter, 'increment'); 386 | let dec = stub(counter, 'decrement'); 387 | 388 | let tree = render( 389 | 390 | 391 | 392 | ); 393 | 394 | await click(tree, '#increment'); 395 | assert(inc.calls.length === 1); 396 | assert(dec.calls.length === 0); 397 | }); 398 | ``` 399 | 400 | We don't even have to do anything to clean up after ourselves because we just 401 | throw everything out afterwards. 402 | 403 | ## FAQ 404 | 405 | #### What state should I put into Unstated? 406 | 407 | The React community has focused a lot on trying to put all their state in one 408 | place. You could keep doing that with Unstated, but I wouldn't recommend it. 409 | 410 | I would recommend a multi-part solution. 411 | 412 | First, use local component state as much as you possibly can. That counter 413 | example from above never should have been refactored away from component 414 | state, it was fine before Unstated. 415 | 416 | Second, use libraries to abstract away the bits of state that you'll repeat 417 | over and over. 418 | 419 | Like if form state has you down, you might want to use a library like 420 | [Final Form](https://github.com/final-form/react-final-form). 421 | 422 | If fetching data is getting to be too much, maybe try out [Apollo](https://www.apollographql.com). 423 | Or even something uncool but familiar and reliable like [Backbone models and collections](http://backbonejs.org). 424 | What? Are you too cool to use an old framework? 425 | 426 | Third, a lot of shared state between components is localized to a few 427 | components in the tree. 428 | 429 | ```jsx 430 | 431 | One 432 | Two 433 | Three 434 | 435 | ``` 436 | 437 | For this, I recommend using React's built-in `React.createContext()` API 438 | and being careful in designing the API for the base components you create. 439 | 440 | > **Note:** If you're on an old version of React and want to use the new 441 | > context API, [I've got you](https://github.com/thejameskyle/create-react-context/) 442 | 443 | Finally, (and only after other things are exhausted), if you really need 444 | some global state to be shared throughout your app, you can use Unstated. 445 | 446 | I know all of this might sound somehow more complicated, but it's a 447 | matter of using the right tool for the job and not forcing a single 448 | paradigm on the entire universe. 449 | 450 | Unstated isn't ambitious, use it as you need it, it's nice and small for 451 | that reason. Don't think of it as a "Redux killer". Don't go trying to 452 | build complex tools on top of it. Don't reinvent the wheel. Just try it 453 | out and see how you like it. 454 | 455 | #### Passing your own instances directly to `` 456 | 457 | If you want to use your own instance of a container directly to `` 458 | and you don't care about dependency injection, you can do so: 459 | 460 | 461 | ```jsx 462 | let counter = new CounterContainer(); 463 | 464 | function Counter() { 465 | return ( 466 | 467 | {counter =>
...
} 468 |
469 | ); 470 | } 471 | ``` 472 | 473 | You just need to keep a couple things in mind: 474 | 475 | 1. You are opting out of dependency injection, you won't be able to 476 | `` another instance in your tests. 477 | 2. Your instance will be local to whatever ``'s you pass it to, you 478 | will end up with multiple instances of your container if you don't pass the 479 | same reference in everywhere. 480 | 481 | Also remember that it is _okay_ to use `` in your application 482 | code, you can pass your instance in there. It's probably better to do that in 483 | most scenarios anyways (cause then you get dependency injection and all that 484 | good stuff). 485 | 486 | #### How can I pass in options to my container? 487 | 488 | A good pattern for doing this might be to add a constructor to your container 489 | which accepts `props` sorta like React components. Then create your own 490 | instance of your container and pass it into ``. 491 | 492 | ```jsx 493 | class CounterContainer extends Container { 494 | constructor(props = {}) { 495 | super(); 496 | this.state = { 497 | amount: props.initialAmount || 1, 498 | count: 0 499 | }; 500 | } 501 | 502 | increment = () => { 503 | this.setState({ count: this.state.count + this.state.amount }); 504 | }; 505 | } 506 | 507 | let counter = new CounterContainer({ 508 | initialAmount: 5 509 | }); 510 | 511 | render( 512 | 513 | 514 | 515 | ); 516 | ``` 517 | 518 | ## Related 519 | 520 | - [unstated-debug](https://github.com/sindresorhus/unstated-debug) - Debug your Unstated containers with ease 521 | 522 | -------------------------------------------------------------------------------- /__tests__/unstated.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import renderer from 'react-test-renderer'; 4 | import { Provider, Subscribe, Container } from '../src/unstated'; 5 | 6 | function render(element) { 7 | return renderer.create(element).toJSON(); 8 | } 9 | 10 | async function click({ children = [] }, id) { 11 | const el: any = children.find(({ props = {} }) => props.id === id); 12 | el.props.onClick(); 13 | } 14 | 15 | class CounterContainer extends Container<{ count: number }> { 16 | state = { count: 0 }; 17 | increment(amount = 1) { 18 | this.setState({ count: this.state.count + amount }); 19 | } 20 | decrement(amount = 1) { 21 | this.setState({ count: this.state.count - amount }); 22 | } 23 | } 24 | 25 | function Counter() { 26 | return ( 27 | 28 | {counter => ( 29 |
30 | {counter.state.count} 31 | 34 | 37 |
38 | )} 39 |
40 | ); 41 | } 42 | 43 | test('should incresase/decrease state counter in container', async () => { 44 | let counter = new CounterContainer(); 45 | let tree = render( 46 | 47 | 48 | 49 | ); 50 | 51 | expect(counter.state.count).toBe(0); 52 | 53 | await click(tree, 'increment'); 54 | expect(counter.state.count).toBe(1); 55 | 56 | await click(tree, 'decrement'); 57 | expect(counter.state.count).toBe(0); 58 | }); 59 | 60 | test('should remove subscriber listeners if component is unmounted', () => { 61 | let counter = new CounterContainer(); 62 | let tree = renderer.create( 63 | 64 | 65 | 66 | ); 67 | const testInstance = tree.root.findByType(Subscribe)._fiber.stateNode; 68 | 69 | expect(counter._listeners.length).toBe(1); 70 | expect(testInstance.unmounted).toBe(false); 71 | 72 | tree.unmount(); 73 | 74 | expect(counter._listeners.length).toBe(0); 75 | expect(testInstance.unmounted).toBe(true); 76 | }); 77 | 78 | test('should throw an error if component is not wrapper with ', () => { 79 | spyOn(console, 'error'); 80 | expect(() => render()).toThrowError( 81 | 'You must wrap your components with a ' 82 | ); 83 | }); 84 | -------------------------------------------------------------------------------- /__tests__/unstated.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Provider, Subscribe, Container } from '../src/unstated'; 3 | 4 | class CounterContainer extends Container<{ count: number }> { 5 | state = { count: 0 }; 6 | increment(amount = 1) { 7 | this.setState({ count: this.state.count + amount }); 8 | } 9 | decrement(amount = 1) { 10 | this.setState({ count: this.state.count - amount }); 11 | } 12 | } 13 | 14 | class AmounterContainer extends Container<{ amount: number }> { 15 | state = { amount: 1 }; 16 | setAmount(amount: number) { 17 | this.setState({ amount }); 18 | } 19 | } 20 | 21 | function Counter() { 22 | return ( 23 | 24 | {(counter: CounterContainer) => ( 25 |
26 | {counter.state.count} 27 | 28 | 29 |
30 | )} 31 |
32 | ); 33 | } 34 | 35 | function CounterWithAmount() { 36 | return ( 37 | 38 | {(counter: CounterContainer, amounter: AmounterContainer) => ( 39 |
40 | {counter.state.count} 41 | 44 | 47 |
48 | )} 49 |
50 | ); 51 | } 52 | 53 | function CounterWithAmountApp() { 54 | return ( 55 | 56 | {(amounter: AmounterContainer) => ( 57 |
58 | 59 | { 63 | amounter.setAmount(parseInt(event.currentTarget.value, 10)); 64 | }} 65 | /> 66 |
67 | )} 68 |
69 | ); 70 | } 71 | 72 | const sharedAmountContainer = new AmounterContainer(); 73 | 74 | function CounterWithSharedAmountApp() { 75 | return ( 76 | 77 | {(amounter: AmounterContainer) => ( 78 |
79 | 80 | { 84 | amounter.setAmount(parseInt(event.currentTarget.value, 10)); 85 | }} 86 | /> 87 |
88 | )} 89 |
90 | ); 91 | } 92 | 93 | let counter = new CounterContainer(); 94 | let render = () => ( 95 | 96 | 97 | 98 | ); 99 | -------------------------------------------------------------------------------- /example/complex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Provider, Subscribe, Container } from '../src/unstated'; 5 | 6 | type AppState = { 7 | amount: number 8 | }; 9 | 10 | class AppContainer extends Container { 11 | state = { 12 | amount: 1 13 | }; 14 | 15 | setAmount(amount: number) { 16 | this.setState({ amount }); 17 | } 18 | } 19 | 20 | type CounterState = { 21 | count: number 22 | }; 23 | 24 | class CounterContainer extends Container { 25 | state = { 26 | count: 0 27 | }; 28 | 29 | increment(amount: number) { 30 | this.setState({ count: this.state.count + amount }); 31 | } 32 | 33 | decrement(amount: number) { 34 | this.setState({ count: this.state.count - amount }); 35 | } 36 | } 37 | 38 | function Counter() { 39 | return ( 40 | 41 | {(app, counter) => ( 42 |
43 | Count: {counter.state.count} 44 | 45 | 46 |
47 | )} 48 |
49 | ); 50 | } 51 | 52 | function App() { 53 | return ( 54 | 55 | {app => ( 56 |
57 | 58 | 59 | { 63 | app.setAmount(parseInt(event.currentTarget.value, 10)); 64 | }} 65 | /> 66 |
67 | )} 68 |
69 | ); 70 | } 71 | 72 | render( 73 | 74 | 75 | , 76 | window.complex 77 | ); 78 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unstated - Examples 6 | 7 | 8 |

Simple

9 |
10 | 11 | 12 |

Complex

13 |
14 | 15 | 16 |

Shared

17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/shared.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Provider, Subscribe, Container } from '../src/unstated'; 5 | 6 | type CounterState = { 7 | count: number 8 | }; 9 | 10 | class CounterContainer extends Container { 11 | state = { count: 0 }; 12 | 13 | increment() { 14 | this.setState({ count: this.state.count + 1 }); 15 | } 16 | 17 | decrement() { 18 | this.setState({ count: this.state.count - 1 }); 19 | } 20 | } 21 | 22 | const sharedCounterContainer = new CounterContainer(); 23 | 24 | function Counter() { 25 | return ( 26 | 27 | {counter => ( 28 |
29 | 30 | {counter.state.count} 31 | 32 |
33 | )} 34 |
35 | ); 36 | } 37 | 38 | render( 39 | 40 | 41 | , 42 | window.shared 43 | ); 44 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Provider, Subscribe, Container } from '../src/unstated'; 5 | 6 | type CounterState = { 7 | count: number 8 | }; 9 | 10 | class CounterContainer extends Container { 11 | state = { count: 0 }; 12 | 13 | increment() { 14 | this.setState({ count: this.state.count + 1 }); 15 | } 16 | 17 | decrement() { 18 | this.setState({ count: this.state.count - 1 }); 19 | } 20 | } 21 | 22 | function Counter() { 23 | return ( 24 | 25 | {counter => ( 26 |
27 | 28 | {counter.state.count} 29 | 30 |
31 | )} 32 |
33 | ); 34 | } 35 | 36 | render( 37 | 38 | 39 | , 40 | window.simple 41 | ); 42 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v22.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6e1fc0a644aa956f79029fec0709e597 2 | // flow-typed version: 07ebad4796/jest_v22.x.x/flow_>=v0.39.x 3 | 4 | type JestMockFn, TReturn> = { 5 | (...args: TArguments): TReturn, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: Array 21 | }, 22 | /** 23 | * Resets all information stored in the mockFn.mock.calls and 24 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 25 | * up a mock's usage data between two assertions. 26 | */ 27 | mockClear(): void, 28 | /** 29 | * Resets all information stored in the mock. This is useful when you want to 30 | * completely restore a mock back to its initial state. 31 | */ 32 | mockReset(): void, 33 | /** 34 | * Removes the mock and restores the initial implementation. This is useful 35 | * when you want to mock functions in certain test cases and restore the 36 | * original implementation in others. Beware that mockFn.mockRestore only 37 | * works when mock was created with jest.spyOn. Thus you have to take care of 38 | * restoration yourself when manually assigning jest.fn(). 39 | */ 40 | mockRestore(): void, 41 | /** 42 | * Accepts a function that should be used as the implementation of the mock. 43 | * The mock itself will still record all calls that go into and instances 44 | * that come from itself -- the only difference is that the implementation 45 | * will also be executed when the mock is called. 46 | */ 47 | mockImplementation( 48 | fn: (...args: TArguments) => TReturn 49 | ): JestMockFn, 50 | /** 51 | * Accepts a function that will be used as an implementation of the mock for 52 | * one call to the mocked function. Can be chained so that multiple function 53 | * calls produce different results. 54 | */ 55 | mockImplementationOnce( 56 | fn: (...args: TArguments) => TReturn 57 | ): JestMockFn, 58 | /** 59 | * Just a simple sugar function for returning `this` 60 | */ 61 | mockReturnThis(): void, 62 | /** 63 | * Deprecated: use jest.fn(() => value) instead 64 | */ 65 | mockReturnValue(value: TReturn): JestMockFn, 66 | /** 67 | * Sugar for only returning a value once inside your mock 68 | */ 69 | mockReturnValueOnce(value: TReturn): JestMockFn 70 | }; 71 | 72 | type JestAsymmetricEqualityType = { 73 | /** 74 | * A custom Jasmine equality tester 75 | */ 76 | asymmetricMatch(value: mixed): boolean 77 | }; 78 | 79 | type JestCallsType = { 80 | allArgs(): mixed, 81 | all(): mixed, 82 | any(): boolean, 83 | count(): number, 84 | first(): mixed, 85 | mostRecent(): mixed, 86 | reset(): void 87 | }; 88 | 89 | type JestClockType = { 90 | install(): void, 91 | mockDate(date: Date): void, 92 | tick(milliseconds?: number): void, 93 | uninstall(): void 94 | }; 95 | 96 | type JestMatcherResult = { 97 | message?: string | (() => string), 98 | pass: boolean 99 | }; 100 | 101 | type JestMatcher = (actual: any, expected: any) => JestMatcherResult; 102 | 103 | type JestPromiseType = { 104 | /** 105 | * Use rejects to unwrap the reason of a rejected promise so any other 106 | * matcher can be chained. If the promise is fulfilled the assertion fails. 107 | */ 108 | rejects: JestExpectType, 109 | /** 110 | * Use resolves to unwrap the value of a fulfilled promise so any other 111 | * matcher can be chained. If the promise is rejected the assertion fails. 112 | */ 113 | resolves: JestExpectType 114 | }; 115 | 116 | /** 117 | * Plugin: jest-enzyme 118 | */ 119 | type EnzymeMatchersType = { 120 | toBeChecked(): void, 121 | toBeDisabled(): void, 122 | toBeEmpty(): void, 123 | toBePresent(): void, 124 | toContainReact(element: React$Element): void, 125 | toHaveClassName(className: string): void, 126 | toHaveHTML(html: string): void, 127 | toHaveProp(propKey: string, propValue?: any): void, 128 | toHaveRef(refName: string): void, 129 | toHaveState(stateKey: string, stateValue?: any): void, 130 | toHaveStyle(styleKey: string, styleValue?: any): void, 131 | toHaveTagName(tagName: string): void, 132 | toHaveText(text: string): void, 133 | toIncludeText(text: string): void, 134 | toHaveValue(value: any): void, 135 | toMatchElement(element: React$Element): void, 136 | toMatchSelector(selector: string): void 137 | }; 138 | 139 | type JestExpectType = { 140 | not: JestExpectType & EnzymeMatchersType, 141 | /** 142 | * If you have a mock function, you can use .lastCalledWith to test what 143 | * arguments it was last called with. 144 | */ 145 | lastCalledWith(...args: Array): void, 146 | /** 147 | * toBe just checks that a value is what you expect. It uses === to check 148 | * strict equality. 149 | */ 150 | toBe(value: any): void, 151 | /** 152 | * Use .toHaveBeenCalled to ensure that a mock function got called. 153 | */ 154 | toBeCalled(): void, 155 | /** 156 | * Use .toBeCalledWith to ensure that a mock function was called with 157 | * specific arguments. 158 | */ 159 | toBeCalledWith(...args: Array): void, 160 | /** 161 | * Using exact equality with floating point numbers is a bad idea. Rounding 162 | * means that intuitive things fail. 163 | */ 164 | toBeCloseTo(num: number, delta: any): void, 165 | /** 166 | * Use .toBeDefined to check that a variable is not undefined. 167 | */ 168 | toBeDefined(): void, 169 | /** 170 | * Use .toBeFalsy when you don't care what a value is, you just want to 171 | * ensure a value is false in a boolean context. 172 | */ 173 | toBeFalsy(): void, 174 | /** 175 | * To compare floating point numbers, you can use toBeGreaterThan. 176 | */ 177 | toBeGreaterThan(number: number): void, 178 | /** 179 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 180 | */ 181 | toBeGreaterThanOrEqual(number: number): void, 182 | /** 183 | * To compare floating point numbers, you can use toBeLessThan. 184 | */ 185 | toBeLessThan(number: number): void, 186 | /** 187 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 188 | */ 189 | toBeLessThanOrEqual(number: number): void, 190 | /** 191 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 192 | * class. 193 | */ 194 | toBeInstanceOf(cls: Class<*>): void, 195 | /** 196 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 197 | * nicer. 198 | */ 199 | toBeNull(): void, 200 | /** 201 | * Use .toBeTruthy when you don't care what a value is, you just want to 202 | * ensure a value is true in a boolean context. 203 | */ 204 | toBeTruthy(): void, 205 | /** 206 | * Use .toBeUndefined to check that a variable is undefined. 207 | */ 208 | toBeUndefined(): void, 209 | /** 210 | * Use .toContain when you want to check that an item is in a list. For 211 | * testing the items in the list, this uses ===, a strict equality check. 212 | */ 213 | toContain(item: any): void, 214 | /** 215 | * Use .toContainEqual when you want to check that an item is in a list. For 216 | * 217 | * 218 | * 219 | * ing the items in the list, this matcher recursively checks the 220 | * equality of all fields, rather than checking for object identity. 221 | */ 222 | toContainEqual(item: any): void, 223 | /** 224 | * Use .toEqual when you want to check that two objects have the same value. 225 | * This matcher recursively checks the equality of all fields, rather than 226 | * checking for object identity. 227 | */ 228 | toEqual(value: any): void, 229 | /** 230 | * Use .toHaveBeenCalled to ensure that a mock function got called. 231 | */ 232 | toHaveBeenCalled(): void, 233 | /** 234 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 235 | * number of times. 236 | */ 237 | toHaveBeenCalledTimes(number: number): void, 238 | /** 239 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 240 | * specific arguments. 241 | */ 242 | toHaveBeenCalledWith(...args: Array): void, 243 | /** 244 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 245 | * with specific arguments. 246 | */ 247 | toHaveBeenLastCalledWith(...args: Array): void, 248 | /** 249 | * Check that an object has a .length property and it is set to a certain 250 | * numeric value. 251 | */ 252 | toHaveLength(number: number): void, 253 | /** 254 | * 255 | */ 256 | toHaveProperty(propPath: string, value?: any): void, 257 | /** 258 | * Use .toMatch to check that a string matches a regular expression or string. 259 | */ 260 | toMatch(regexpOrString: RegExp | string): void, 261 | /** 262 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 263 | */ 264 | toMatchObject(object: Object | Array): void, 265 | /** 266 | * This ensures that a React component matches the most recent snapshot. 267 | */ 268 | toMatchSnapshot(name?: string): void, 269 | /** 270 | * Use .toThrow to test that a function throws when it is called. 271 | * If you want to test that a specific error gets thrown, you can provide an 272 | * argument to toThrow. The argument can be a string for the error message, 273 | * a class for the error, or a regex that should match the error. 274 | * 275 | * Alias: .toThrowError 276 | */ 277 | toThrow(message?: string | Error | Class | RegExp): void, 278 | toThrowError(message?: string | Error | Class | RegExp): void, 279 | /** 280 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 281 | * matching the most recent snapshot when it is called. 282 | */ 283 | toThrowErrorMatchingSnapshot(): void 284 | }; 285 | 286 | type JestObjectType = { 287 | /** 288 | * Disables automatic mocking in the module loader. 289 | * 290 | * After this method is called, all `require()`s will return the real 291 | * versions of each module (rather than a mocked version). 292 | */ 293 | disableAutomock(): JestObjectType, 294 | /** 295 | * An un-hoisted version of disableAutomock 296 | */ 297 | autoMockOff(): JestObjectType, 298 | /** 299 | * Enables automatic mocking in the module loader. 300 | */ 301 | enableAutomock(): JestObjectType, 302 | /** 303 | * An un-hoisted version of enableAutomock 304 | */ 305 | autoMockOn(): JestObjectType, 306 | /** 307 | * Clears the mock.calls and mock.instances properties of all mocks. 308 | * Equivalent to calling .mockClear() on every mocked function. 309 | */ 310 | clearAllMocks(): JestObjectType, 311 | /** 312 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 313 | * mocked function. 314 | */ 315 | resetAllMocks(): JestObjectType, 316 | /** 317 | * Restores all mocks back to their original value. 318 | */ 319 | restoreAllMocks(): JestObjectType, 320 | /** 321 | * Removes any pending timers from the timer system. 322 | */ 323 | clearAllTimers(): void, 324 | /** 325 | * The same as `mock` but not moved to the top of the expectation by 326 | * babel-jest. 327 | */ 328 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 329 | /** 330 | * The same as `unmock` but not moved to the top of the expectation by 331 | * babel-jest. 332 | */ 333 | dontMock(moduleName: string): JestObjectType, 334 | /** 335 | * Returns a new, unused mock function. Optionally takes a mock 336 | * implementation. 337 | */ 338 | fn, TReturn>( 339 | implementation?: (...args: TArguments) => TReturn 340 | ): JestMockFn, 341 | /** 342 | * Determines if the given function is a mocked function. 343 | */ 344 | isMockFunction(fn: Function): boolean, 345 | /** 346 | * Given the name of a module, use the automatic mocking system to generate a 347 | * mocked version of the module for you. 348 | */ 349 | genMockFromModule(moduleName: string): any, 350 | /** 351 | * Mocks a module with an auto-mocked version when it is being required. 352 | * 353 | * The second argument can be used to specify an explicit module factory that 354 | * is being run instead of using Jest's automocking feature. 355 | * 356 | * The third argument can be used to create virtual mocks -- mocks of modules 357 | * that don't exist anywhere in the system. 358 | */ 359 | mock( 360 | moduleName: string, 361 | moduleFactory?: any, 362 | options?: Object 363 | ): JestObjectType, 364 | /** 365 | * Returns the actual module instead of a mock, bypassing all checks on 366 | * whether the module should receive a mock implementation or not. 367 | */ 368 | requireActual(moduleName: string): any, 369 | /** 370 | * Returns a mock module instead of the actual module, bypassing all checks 371 | * on whether the module should be required normally or not. 372 | */ 373 | requireMock(moduleName: string): any, 374 | /** 375 | * Resets the module registry - the cache of all required modules. This is 376 | * useful to isolate modules where local state might conflict between tests. 377 | */ 378 | resetModules(): JestObjectType, 379 | /** 380 | * Exhausts the micro-task queue (usually interfaced in node via 381 | * process.nextTick). 382 | */ 383 | runAllTicks(): void, 384 | /** 385 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 386 | * setInterval(), and setImmediate()). 387 | */ 388 | runAllTimers(): void, 389 | /** 390 | * Exhausts all tasks queued by setImmediate(). 391 | */ 392 | runAllImmediates(): void, 393 | /** 394 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 395 | * or setInterval() and setImmediate()). 396 | */ 397 | runTimersToTime(msToRun: number): void, 398 | /** 399 | * Executes only the macro-tasks that are currently pending (i.e., only the 400 | * tasks that have been queued by setTimeout() or setInterval() up to this 401 | * point) 402 | */ 403 | runOnlyPendingTimers(): void, 404 | /** 405 | * Explicitly supplies the mock object that the module system should return 406 | * for the specified module. Note: It is recommended to use jest.mock() 407 | * instead. 408 | */ 409 | setMock(moduleName: string, moduleExports: any): JestObjectType, 410 | /** 411 | * Indicates that the module system should never return a mocked version of 412 | * the specified module from require() (e.g. that it should always return the 413 | * real module). 414 | */ 415 | unmock(moduleName: string): JestObjectType, 416 | /** 417 | * Instructs Jest to use fake versions of the standard timer functions 418 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 419 | * setImmediate and clearImmediate). 420 | */ 421 | useFakeTimers(): JestObjectType, 422 | /** 423 | * Instructs Jest to use the real versions of the standard timer functions. 424 | */ 425 | useRealTimers(): JestObjectType, 426 | /** 427 | * Creates a mock function similar to jest.fn but also tracks calls to 428 | * object[methodName]. 429 | */ 430 | spyOn(object: Object, methodName: string): JestMockFn, 431 | /** 432 | * Set the default timeout interval for tests and before/after hooks in milliseconds. 433 | * Note: The default timeout interval is 5 seconds if this method is not called. 434 | */ 435 | setTimeout(timeout: number): JestObjectType 436 | }; 437 | 438 | type JestSpyType = { 439 | calls: JestCallsType 440 | }; 441 | 442 | /** Runs this function after every test inside this context */ 443 | declare function afterEach( 444 | fn: (done: () => void) => ?Promise, 445 | timeout?: number 446 | ): void; 447 | /** Runs this function before every test inside this context */ 448 | declare function beforeEach( 449 | fn: (done: () => void) => ?Promise, 450 | timeout?: number 451 | ): void; 452 | /** Runs this function after all tests have finished inside this context */ 453 | declare function afterAll( 454 | fn: (done: () => void) => ?Promise, 455 | timeout?: number 456 | ): void; 457 | /** Runs this function before any tests have started inside this context */ 458 | declare function beforeAll( 459 | fn: (done: () => void) => ?Promise, 460 | timeout?: number 461 | ): void; 462 | 463 | /** A context for grouping tests together */ 464 | declare var describe: { 465 | /** 466 | * Creates a block that groups together several related tests in one "test suite" 467 | */ 468 | (name: string, fn: () => void): void, 469 | 470 | /** 471 | * Only run this describe block 472 | */ 473 | only(name: string, fn: () => void): void, 474 | 475 | /** 476 | * Skip running this describe block 477 | */ 478 | skip(name: string, fn: () => void): void 479 | }; 480 | 481 | /** An individual test unit */ 482 | declare var it: { 483 | /** 484 | * An individual test unit 485 | * 486 | * @param {string} Name of Test 487 | * @param {Function} Test 488 | * @param {number} Timeout for the test, in milliseconds. 489 | */ 490 | ( 491 | name: string, 492 | fn?: (done: () => void) => ?Promise, 493 | timeout?: number 494 | ): void, 495 | /** 496 | * Only run this test 497 | * 498 | * @param {string} Name of Test 499 | * @param {Function} Test 500 | * @param {number} Timeout for the test, in milliseconds. 501 | */ 502 | only( 503 | name: string, 504 | fn?: (done: () => void) => ?Promise, 505 | timeout?: number 506 | ): void, 507 | /** 508 | * Skip running this test 509 | * 510 | * @param {string} Name of Test 511 | * @param {Function} Test 512 | * @param {number} Timeout for the test, in milliseconds. 513 | */ 514 | skip( 515 | name: string, 516 | fn?: (done: () => void) => ?Promise, 517 | timeout?: number 518 | ): void, 519 | /** 520 | * Run the test concurrently 521 | * 522 | * @param {string} Name of Test 523 | * @param {Function} Test 524 | * @param {number} Timeout for the test, in milliseconds. 525 | */ 526 | concurrent( 527 | name: string, 528 | fn?: (done: () => void) => ?Promise, 529 | timeout?: number 530 | ): void 531 | }; 532 | declare function fit( 533 | name: string, 534 | fn: (done: () => void) => ?Promise, 535 | timeout?: number 536 | ): void; 537 | /** An individual test unit */ 538 | declare var test: typeof it; 539 | /** A disabled group of tests */ 540 | declare var xdescribe: typeof describe; 541 | /** A focused group of tests */ 542 | declare var fdescribe: typeof describe; 543 | /** A disabled individual test */ 544 | declare var xit: typeof it; 545 | /** A disabled individual test */ 546 | declare var xtest: typeof it; 547 | 548 | /** The expect function is used every time you want to test a value */ 549 | declare var expect: { 550 | /** The object that you want to make assertions against */ 551 | (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType, 552 | /** Add additional Jasmine matchers to Jest's roster */ 553 | extend(matchers: { [name: string]: JestMatcher }): void, 554 | /** Add a module that formats application-specific data structures. */ 555 | addSnapshotSerializer(serializer: (input: Object) => string): void, 556 | assertions(expectedAssertions: number): void, 557 | hasAssertions(): void, 558 | any(value: mixed): JestAsymmetricEqualityType, 559 | anything(): void, 560 | arrayContaining(value: Array): void, 561 | objectContaining(value: Object): void, 562 | /** Matches any received string that contains the exact expected string. */ 563 | stringContaining(value: string): void, 564 | stringMatching(value: string | RegExp): void 565 | }; 566 | 567 | // TODO handle return type 568 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 569 | declare function spyOn(value: mixed, method: string): Object; 570 | 571 | /** Holds all functions related to manipulating test runner */ 572 | declare var jest: JestObjectType; 573 | 574 | /** 575 | * The global Jasmine object, this is generally not exposed as the public API, 576 | * using features inside here could break in later versions of Jest. 577 | */ 578 | declare var jasmine: { 579 | DEFAULT_TIMEOUT_INTERVAL: number, 580 | any(value: mixed): JestAsymmetricEqualityType, 581 | anything(): void, 582 | arrayContaining(value: Array): void, 583 | clock(): JestClockType, 584 | createSpy(name: string): JestSpyType, 585 | createSpyObj( 586 | baseName: string, 587 | methodNames: Array 588 | ): { [methodName: string]: JestSpyType }, 589 | objectContaining(value: Object): void, 590 | stringMatching(value: string): void 591 | }; 592 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiebuilds/unstated/0c8b3102d2ed404672a641d768f888804a2e7d2e/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unstated", 3 | "version": "2.1.1", 4 | "description": "State so simple, it goes without saying", 5 | "main": "lib/unstated.js", 6 | "module": "lib/unstated.es.js", 7 | "types": "lib/unstated.d.ts", 8 | "repository": "https://github.com/thejameskyle/unstated", 9 | "author": "James Kyle ", 10 | "license": "MIT", 11 | "files": ["lib"], 12 | "scripts": { 13 | "clean": "rm -rf lib", 14 | "build": 15 | "rollup -c && flow-copy-source src lib && cp src/unstated.d.ts lib/unstated.d.ts", 16 | "typecheck": "flow", 17 | "test": "jest", 18 | "format": "prettier --write **/*.{js,json,md}", 19 | "prepublish": "yarn clean && yarn build", 20 | "precommit": "lint-staged", 21 | "example": "parcel example/index.html", 22 | "typescript": "tsc -p tsconfig.json" 23 | }, 24 | "dependencies": { 25 | "create-react-context": "^0.2.2" 26 | }, 27 | "peerDependencies": { 28 | "react": "^15.0.0 || ^16.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/react": "^16.0.36", 32 | "babel-core": "^6.26.0", 33 | "babel-plugin-transform-class-properties": "^6.24.1", 34 | "babel-preset-env": "^1.6.1", 35 | "babel-preset-flow": "^6.23.0", 36 | "babel-preset-react": "^6.24.1", 37 | "babel-register": "^6.26.0", 38 | "flow-bin": "^0.64.0", 39 | "flow-copy-source": "^1.2.2", 40 | "husky": "^0.14.3", 41 | "jest": "^22.1.4", 42 | "jsdom": "^11.6.2", 43 | "lint-staged": "^6.1.0", 44 | "parcel-bundler": "^1.5.1", 45 | "prettier": "^1.10.2", 46 | "prop-types": "^15.6.0", 47 | "react": "^16.2.0", 48 | "react-dom": "^16.2.0", 49 | "react-test-renderer": "^16.2.0", 50 | "rollup": "^0.55.3", 51 | "rollup-plugin-babel": "^3.0.3", 52 | "typescript": "^2.7.1" 53 | }, 54 | "lint-staged": { 55 | "*.{js,json,md}": ["prettier --write", "git add"] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/unstated.js', 6 | output: [ 7 | { 8 | file: pkg.main, 9 | format: 'cjs' 10 | }, 11 | { 12 | file: pkg.module, 13 | format: 'es' 14 | } 15 | ], 16 | external: [ 17 | ...Object.keys(pkg.dependencies || {}), 18 | ...Object.keys(pkg.peerDependencies || {}) 19 | ], 20 | plugins: [babel()] 21 | }; 22 | -------------------------------------------------------------------------------- /src/unstated.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export class Container { 4 | state: State; 5 | setState( 6 | state: 7 | | ((prevState: Readonly) => Pick | State | null) 8 | | (Pick | State | null), 9 | callback?: () => void 10 | ): Promise; 11 | subscribe(fn: () => any): void; 12 | unsubscribe(fn: () => any): void; 13 | } 14 | 15 | export interface ContainerType { 16 | new (...args: any[]): Container; 17 | } 18 | 19 | interface SubscribeProps { 20 | to: (ContainerType | Container)[]; 21 | children(...instances: Container[]): React.ReactNode; 22 | } 23 | 24 | export class Subscribe extends React.Component {} 25 | 26 | export interface ProviderProps { 27 | inject?: Container[]; 28 | children: React.ReactNode; 29 | } 30 | 31 | export const Provider: React.SFC; 32 | -------------------------------------------------------------------------------- /src/unstated.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { type Node } from 'react'; 3 | import createReactContext from 'create-react-context'; 4 | 5 | type Listener = () => mixed; 6 | 7 | const StateContext = createReactContext(null); 8 | 9 | export class Container { 10 | state: State; 11 | _listeners: Array = []; 12 | 13 | constructor() { 14 | CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this)); 15 | } 16 | 17 | setState( 18 | updater: $Shape | ((prevState: $Shape) => $Shape), 19 | callback?: () => void 20 | ): Promise { 21 | return Promise.resolve().then(() => { 22 | let nextState; 23 | 24 | if (typeof updater === 'function') { 25 | nextState = updater(this.state); 26 | } else { 27 | nextState = updater; 28 | } 29 | 30 | if (nextState == null) { 31 | if (callback) callback(); 32 | return; 33 | } 34 | 35 | this.state = Object.assign({}, this.state, nextState); 36 | 37 | let promises = this._listeners.map(listener => listener()); 38 | 39 | return Promise.all(promises).then(() => { 40 | if (callback) { 41 | return callback(); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | subscribe(fn: Listener) { 48 | this._listeners.push(fn); 49 | } 50 | 51 | unsubscribe(fn: Listener) { 52 | this._listeners = this._listeners.filter(f => f !== fn); 53 | } 54 | } 55 | 56 | export type ContainerType = Container; 57 | export type ContainersType = Array | ContainerType>; 58 | export type ContainerMapType = Map, ContainerType>; 59 | 60 | export type SubscribeProps = { 61 | to: Containers, 62 | children: ( 63 | ...instances: $TupleMap(Class | C) => C> 64 | ) => Node 65 | }; 66 | 67 | type SubscribeState = {}; 68 | 69 | const DUMMY_STATE = {}; 70 | 71 | export class Subscribe extends React.Component< 72 | SubscribeProps, 73 | SubscribeState 74 | > { 75 | state = {}; 76 | instances: Array = []; 77 | unmounted = false; 78 | 79 | componentWillUnmount() { 80 | this.unmounted = true; 81 | this._unsubscribe(); 82 | } 83 | 84 | _unsubscribe() { 85 | this.instances.forEach(container => { 86 | container.unsubscribe(this.onUpdate); 87 | }); 88 | } 89 | 90 | onUpdate: Listener = () => { 91 | return new Promise(resolve => { 92 | if (!this.unmounted) { 93 | this.setState(DUMMY_STATE, resolve); 94 | } else { 95 | resolve(); 96 | } 97 | }); 98 | }; 99 | 100 | _createInstances( 101 | map: ContainerMapType | null, 102 | containers: ContainersType 103 | ): Array { 104 | this._unsubscribe(); 105 | 106 | if (map === null) { 107 | throw new Error( 108 | 'You must wrap your components with a ' 109 | ); 110 | } 111 | 112 | let safeMap = map; 113 | let instances = containers.map(ContainerItem => { 114 | let instance; 115 | 116 | if ( 117 | typeof ContainerItem === 'object' && 118 | ContainerItem instanceof Container 119 | ) { 120 | instance = ContainerItem; 121 | } else { 122 | instance = safeMap.get(ContainerItem); 123 | 124 | if (!instance) { 125 | instance = new ContainerItem(); 126 | safeMap.set(ContainerItem, instance); 127 | } 128 | } 129 | 130 | instance.unsubscribe(this.onUpdate); 131 | instance.subscribe(this.onUpdate); 132 | 133 | return instance; 134 | }); 135 | 136 | this.instances = instances; 137 | return instances; 138 | } 139 | 140 | render() { 141 | return ( 142 | 143 | {map => 144 | this.props.children.apply( 145 | null, 146 | this._createInstances(map, this.props.to) 147 | ) 148 | } 149 | 150 | ); 151 | } 152 | } 153 | 154 | export type ProviderProps = { 155 | inject?: Array, 156 | children: Node 157 | }; 158 | 159 | export function Provider(props: ProviderProps) { 160 | return ( 161 | 162 | {parentMap => { 163 | let childMap = new Map(parentMap); 164 | 165 | if (props.inject) { 166 | props.inject.forEach(instance => { 167 | childMap.set(instance.constructor, instance); 168 | }); 169 | } 170 | 171 | return ( 172 | 173 | {props.children} 174 | 175 | ); 176 | }} 177 | 178 | ); 179 | } 180 | 181 | let CONTAINER_DEBUG_CALLBACKS = []; 182 | 183 | // If your name isn't Sindre, this is not for you. 184 | // I might ruin your day suddenly if you depend on this without talking to me. 185 | export function __SUPER_SECRET_CONTAINER_DEBUG_HOOK__( 186 | callback: (container: Container) => mixed 187 | ) { 188 | CONTAINER_DEBUG_CALLBACKS.push(callback); 189 | } 190 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6", "dom"], 6 | "sourceMap": true, 7 | "jsx": "react", 8 | "moduleResolution": "node", 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noEmit": true 12 | }, 13 | "include": ["src/**/*", "__test__/*.tsx"] 14 | } 15 | --------------------------------------------------------------------------------