├── .eslintrc
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── README.md
├── demo
└── src
│ ├── App.js
│ ├── ConnectFiltering.js
│ ├── components
│ └── codeHighlight.js
│ ├── index.js
│ ├── main.css
│ ├── reset.css
│ └── utils
│ ├── prism.css
│ └── prism.js
├── nwb.config.js
├── package.json
├── src
├── Connect.js
├── Provider.js
└── index.js
├── tests
├── .eslintrc
└── index-test.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-tools"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | npm-debug.log*
8 | yarn-error.log
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 | node_js:
5 | - 8
6 |
7 | before_install:
8 | - npm install codecov.io coveralls
9 |
10 | after_success:
11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
13 |
14 | branches:
15 | only:
16 | - master
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | [Node.js](http://nodejs.org/) >= v4 must be installed.
4 |
5 | ## Installation
6 |
7 | - Running `npm install` in the component's root directory will install everything you need for development.
8 |
9 | ## Demo Development Server
10 |
11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 |
13 | ## Running Tests
14 |
15 | - `npm test` will run the tests once.
16 |
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 |
19 | - `npm run test:watch` will run the tests on every change.
20 |
21 | ## Building
22 |
23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
24 |
25 | - `npm run clean` will delete built resources.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [react-state](https://github.com/tannerlinsley/react-state)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Superpowers for managing local and reusable state in React
20 |
21 | ## Features
22 |
23 | * **2kb!** (minified)
24 | * No dependencies
25 | * Ditch repetitive props and callbacks throughout deeply nested components
26 | * Improved performance via Provider & Connector components
27 | * Testable and predictable
28 |
29 | ## [Demo](https://react-state.js.org/?selectedKind=2.%20Demos&selectedStory=Kitchen%20Sink&full=0&down=0&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel)
30 |
31 | ## Table of Contents
32 |
33 | * [Installation](#installation)
34 | * [Example](#example)
35 | * [Provider](#provider)
36 | * [Creating a Provider](#creating-a-provider)
37 | * [Initial State](#initial-state)
38 | * [Passing Props as State](#passing-props-as-state)
39 | * [Programatic Control](#programatic-control)
40 | * [Connect](#connect)
41 | * [Memoization And Selectors](#memoization-and-selectors)
42 | * [Using the Dispatcher](#using-the-dispatch)
43 | * [Dispatch Meta](#dispatch-meta)
44 | * [Connect Config](#connect-config)
45 |
46 | ## Installation
47 |
48 | ```bash
49 | $ yarn add react-state
50 | ```
51 |
52 | ## Example
53 |
54 | ```javascript
55 | import React from 'react'
56 | import { Provider, Connect } from 'react-state'
57 |
58 |
59 | const Count = ({ count }) => ({
60 | {count}
61 | }
62 | // Use Connect to subscribe to values from the Provider state
63 | const ConnectedCount = Connect(state => ({
64 | count: state.count
65 | }))(Count)
66 |
67 | // Every Connected component can use the 'dispatch' prop
68 | // to update the Provider's store
69 | const Increment = ({ dispatch }) => ({
70 |
80 | }
81 | const ConnectedIncrement = Connect()(Increment)
82 |
83 | const Demo = () => (
84 |
85 |
86 |
87 |
88 | )
89 |
90 | // A Provider is a new instance of state for all nodes inside it
91 | export default Provider(Demo)
92 | ```
93 |
94 | ## Provider
95 |
96 | The `Provider` higher-order component creates a new state that wraps the component you pass it. You can nest Providers inside each other, and when doing so, `Connect`ed components inside them will connect to the nearest parent Provider. You can also give Providers an initial state in an optional config object.
97 |
98 | ##### Creating a Provider
99 |
100 | ```javascript
101 | const ProviderWrappedComponent = Provider(MyComponent);
102 | ```
103 |
104 | ##### Initial State
105 |
106 | ```javascript
107 | const ProviderWrappedComponent = Provider(MyComponent, {
108 | // the initial state of the provider
109 | initial: {
110 | foo: 1,
111 | bar: "hello"
112 | }
113 | });
114 | ```
115 |
116 | ##### Passing props as state
117 |
118 | Any props you pass to a Provider will be merged with the state and overwrite any same-key values.
119 |
120 | ```javascript
121 |
122 | ```
123 |
124 | ##### Programatic Control
125 |
126 | If you ever need to programmatically dispatch to a provider, you can use a ref!
127 |
128 | ```javascript
129 | {
131 | provider.dispatch;
132 | }}
133 | />
134 | ```
135 |
136 | ## Connect
137 |
138 | The `Connect` higher-order component subscribes a component to any part of the nearest parent Provider, and also provides the component the `dispatch` prop for updating the state.
139 |
140 | ##### Subscribing to state
141 |
142 | To subscribe to a part of the provider state, we use a function that takes the state (and component props) and returns a new object with parts of the state you're interested in. Any time the values of that object change, your component will be updated! (There is no need to return props that already exist on your component. The 'props' argument is simply there as an aid in calculating the state you need to subscribe to)
143 |
144 | ```javascript
145 | class MyComponent extends Component {
146 | render() {
147 | return (
148 |
149 | // This 'foo' prop comes from our Connect function below
150 |
{this.props.foo}
151 |
152 | );
153 | }
154 | }
155 | const MyConnectedComponent = Connect(state => {
156 | return {
157 | foo: state.foo // Any time 'foo' changes, our component will update!
158 | };
159 | });
160 | ```
161 |
162 | ##### Using the dispatcher
163 |
164 | Every connected component receives a 'dispatch' prop. You can use this 'dispatch' function to update the provider state. Just dispatch a function that takes the current state and returns a new version of the state. It's very important to make changes using immutability and also include any unchanged parts of the state. What you return will replace the entire state!
165 |
166 | ```javascript
167 | class MyComponent extends Component {
168 | render() {
169 | return (
170 |
171 |
181 |
182 | );
183 | }
184 | }
185 | const MyConnectedComponent = Connect()(MyComponent);
186 | ```
187 |
188 | ##### Memoization and Selectors
189 |
190 | If you need to subscribe to computed or derived data, you can use a memoized selector. This functions exactly as it does in Redux. For more information, and examples on usage, please refer to [Redux - Computing Derived Data](http://redux.js.org/docs/recipes/ComputingDerivedData.html)
191 |
192 | ```javascript
193 | class MyComponent extends Component {
194 | render() {
195 | return (
196 |
197 |
{this.props.computedValue}
198 |
199 | );
200 | }
201 | }
202 | const MyConnectedComponent = Connect((state, props) => {
203 | return {
204 | computedValue: selectMyComputedValue(state, props)
205 | };
206 | });
207 | ```
208 |
209 | ##### Dispatch Meta
210 |
211 | Any time you dispatch, you have the option to send through a meta object. This is useful for middlewares, hooks, and other optimization options throughout react-state.
212 |
213 | ```javascript
214 | class MyComponent extends Component {
215 | render () {
216 | return (
217 |
218 |
229 |
230 | )
231 | }
232 | }
233 | const MyConnectedComponent = Connect()(MyComponent)
234 | ```
235 |
236 | ##### Connect Config
237 |
238 | `Connect` can be customized for performance and various other enhancments:
239 |
240 | * `pure: Boolean`: Defualts to `true`. When `true` the component will only rerender when the resulting props from the selector change in a shallow comparison.
241 | * `filter(oldState, newState, meta)`: Only run connect if this function returns true. Useful for avoiding high-velocity dispatches or general performance tuning.
242 | * `statics{}`: An object of static properties to add to the connected component's class.
243 |
244 | ```javascript
245 | class MyComponent extends Component { ... }
246 | const MyConnectedComponent = Connect(
247 | state => {...},
248 | {
249 | // Using the following 'filter' function, this Connect will not run if 'meta.mySpecialValue === 'superSpecial'
250 | filter: (oldState, newState, meta) => {
251 | return meta.mySpecialValue ? meta.mySpecialValue !== 'superSpecial' : true
252 | },
253 |
254 | // The Connected component class will also gain these statics
255 | statics: {
256 | defaultProps: {
257 | someProp: 'hello!'
258 | }
259 | }
260 | }
261 | )(MyComponent)
262 | ```
263 |
264 | ## Contributing
265 |
266 | To suggest a feature, create an issue if it does not already exist.
267 | If you would like to help develop a suggested feature follow these steps:
268 |
269 | * Fork this repo
270 | * `$ yarn`
271 | * `$ yarn run storybook`
272 | * Implement your changes to files in the `src/` directory
273 | * View changes as you code via our React Storybook `localhost:8000`
274 | * Make changes to stories in `/stories`, or create a new one if needed
275 | * Submit PR for review
276 |
277 | #### Scripts
278 |
279 | * `$ yarn run storybook` Runs the storybook server
280 | * `$ yarn run test` Runs the test suite
281 | * `$ yarn run prepublish` Builds for NPM distribution
282 | * `$ yarn run docs` Builds the website/docs from the storybook for github pages
283 |
284 |
289 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | //
3 | import { Provider, Connect } from '../../src'
4 |
5 | const CodeHighlight = ({ children }) => (
6 |
40 | )
41 | }
42 | }
43 | // FooComponent needs access to 'foo', so we'll subscribe to it.
44 | const ConnectedFooComponent = Connect(state => ({
45 | foo: state.foo,
46 | }))(FooComponent)
47 |
48 | // ################
49 | // BarComponent
50 | // ################
51 |
52 | // BarComponent is pretty simple. It subscribes to,
53 | // and displays the 'bar' value from the provider.
54 | class BarComponent extends Component {
55 | render () {
56 | const { bar, children } = this.props
57 | return (
58 |
64 |
Bar: {bar}
65 | {children}
66 |
67 | )
68 | }
69 | }
70 | // BarComponent needs access to 'bar', so we'll subscribe to it.
71 | const ConnectedBarComponent = Connect(state => ({
72 | bar: state.bar,
73 | }))(BarComponent)
74 |
75 | // ################
76 | // FooBarComponent
77 | // ################
78 |
79 | // FooBarComponent is very similar.
80 | // It displays both the foo' and 'bar' props
81 | class FooBarComponent extends Component {
82 | render () {
83 | const { foo, bar } = this.props
84 | return (
85 |
91 |
Foo: {foo}
92 |
Bar: {bar}
93 |
94 | )
95 | }
96 | }
97 | // FooBarComponent needs access to 'foo' and 'bar', so we'll subscribe to both of them.
98 | const ConnectedFooBarComponent = Connect(state => ({
99 | foo: state.foo,
100 | bar: state.bar,
101 | }))(FooBarComponent)
102 | // Now, any time the 'foo' or 'bar' values change, FooBarComponent will rerender :)
103 |
104 | // ################
105 | // BazComponent
106 | // ################
107 |
108 | // The BazComponent shows whether the 'baz' value is
109 | // greater than 5 and also displays a message.
110 | class BazComponent extends Component {
111 | render () {
112 | const { bazIsFourth, message } = this.props
113 | return (
114 |
120 |
Baz is a multiple of 4: {bazIsFourth.toString()}
121 |
Message: {message}
122 |
123 | )
124 | }
125 | }
126 | // This time, we are going to return a calculated value.
127 | const ConnectedBazComponent = Connect(state => ({
128 | bazIsFourth: state.baz % 4 === 0,
129 | // Since 'bazIsFourth' will be a boolean, we don't need to use a memoized value,
130 | // But if it was a non-primitive, a selector or memoized value is
131 | // recommended for performance. For an excellent solution, visit https://github.com/reactjs/reselect
132 | }))(BazComponent)
133 | // Now, our BazComponent will only update when the bazIsFourth value changes!
134 |
135 | // ################
136 | // ControlComponent
137 | // ################
138 |
139 | // ControlComponent contains a few buttons that will change
140 | // different parts of our state.
141 | class ControlComponent extends Component {
142 | render () {
143 | const {
144 | dispatch, // this callback is provided to every Connected component
145 | } = this.props
146 | return (
147 |
153 |
154 | Foo:
155 |
169 |
179 |
180 |
181 | Bar:
182 |
192 |
202 |
203 |
204 | Baz:
205 |
215 |
225 |
226 |
227 | )
228 | }
229 | }
230 | // The control component doesn't depend on any state, but
231 | // we still Connect it se we can use the 'dispatch' prop
232 | const ConnectedControlComponent = Connect()(ControlComponent)
233 |
234 | // ################
235 | // Reusable Component
236 | // ################
237 |
238 | // Now let's create our our reusable component.
239 | // We need to keep track of state in our component,
240 | // and your first instinct might be to use local state
241 | // to accomplish this. Interestingly enough though,
242 | // we would probably end up passing many pieces of the state
243 | // down to child components via props and, likewise, would need to
244 | // pass callbacks with them so our child components could
245 | // update the state.
246 |
247 | // We need a better state management system for our component than
248 | // local state, but nothing that will require including a state
249 | // manager like redux or MobX.
250 |
251 | // This is where Provider comes in!
252 |
253 | // Provider is used as a higher order component or decorator
254 | class MyAwesomeReusableComponent extends Component {
255 | render () {
256 | // Components that are wrapped with Provider automatically
257 | // receive the entire provider state as props.
258 | return (
259 |
this.props.dispatch(state => ({
53 | ...state,
54 | position: {
55 | x: e.clientX,
56 | y: e.clientY
57 | }
58 | }), {
59 | type: 'fromCursor'
60 | })}
61 | >
62 | Open your console and move your mouse around in here.
63 |
64 | See how the only connect function that runs is the "Position" component?
65 |
121 | react-state is very good at change detection because of the way it can compare old and new states using Connect functions. But, whenever you dispatch an state change to your provider, EVERY connected component's subscribe function will run.
122 |
123 |
124 | If you are dispatching extremly rapidly, or having thousands of connected components, this can be extremely expensive, especially if you know beforehand that the subscribe function shouldn't even run!
125 |
126 |
127 |
128 |
129 | Now change ExpensiveValue to something random!
130 |
131 | Notice how all of the connect functions ran this time!
132 |
133 |
134 | This is because of a special "filter" option we placed on the "ExpensiveValue" components' connect function:
135 | {() => `
136 | const ConnectedExpensiveValue = Connect(state => {
137 | console.log('Running connect for "ExpensiveValue" component')
138 | // This is to simulate a lot of expensive connect functions
139 | let foo = 0
140 | for (var i = 0; i < 1000000000; i++) {
141 | foo = i
142 | }
143 | foo = state.foo
144 | return {
145 | foo: foo
146 | }
147 | }, {
148 | // This filter function guarantees that it will only if the meta of type does not equal 'fromCursor'.
149 | filter: (oldState, newState, meta) => meta.type !== 'fromCursor'
150 | })(ExpensiveValue)
151 |
152 | // Then, in our cursor move dispatcher we can include 'fromCursor' in the meta:
153 | this.props.dispatch(state => ({
154 | ...state,
155 | position: {
156 | x: e.clientX,
157 | y: e.clientY
158 | }
159 | }), {
160 | // This is the meta object
161 | type: 'fromCursor'
162 | })
163 | `}
164 |
165 | )
166 | }
167 | }
168 | // Just pass Provider a component you would like to wrap and an optional config object
169 | // In the config, we can supply an 'initial' state for the Provider
170 | const ProvidedConnectFiltering = Provider(ConnectFiltering, {
171 | initial: {
172 | foo: 1
173 | }
174 | })
175 |
176 | class Demo extends Component {
177 | render () {
178 | return (
179 | // Let's use our awesome reusable component with some props!
180 |
181 |
182 |
183 |
184 | Here is the full source of the example:
185 | {() => sourceTxt}
186 |