├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── actions.js
├── index.js
├── local.js
├── localReducer.js
└── utils.js
└── tests
├── helpers
├── configureStore.js
├── sagas.js
└── setupEnv.js
├── localReducerSpec.js
├── localSpec.js
└── utilsSpec.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "rules": {
5 | "import/no-unresolved": 0
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | npm-debug.log
4 | coverage
5 | .nyc_output
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # dev-oly folders
2 | tests
3 | npm-debug.log
4 | coverage
5 | .nyc_output
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | branches:
5 | only:
6 | - master
7 | - /^greenkeeper-.*$/
8 | after_success:
9 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.7.1
2 | - Prevent state from being destroying when components with the same keys
3 | enter/exit the view(due to destroying being done in a timeout)
4 |
5 | ## 1.5.0
6 | - Add support for mergeProps ( thanks @farism )
7 | - Cleanup filterGlobalActionscallback upon component unmount
8 | - Linting
9 |
10 | ## 1.4.1
11 | - Fixed a bug preventing composition of `local` HOC with react-redux `connect`
12 | - Add `react-redux` as a peer dependency
13 |
14 | ## 1.4
15 | - Hoist the wrapped component contextTypes into the `local` HOC( thanks @kuon )
16 | - Hoist all non react statics into the `local` HOC
17 | - Provide a display name for components generated by `local`
18 |
19 | ## 1.3
20 | - Stores are now shareable among all components that have the same `key`
21 | - Pass in the component context to all of callback style functions defined on `local`.
22 | Configuration now can be defined as
23 | ```js
24 | local({
25 | key: (props, context) => ...,
26 | createStore: (props, existingState, context) => ...
27 | persist: (props, context) => ...
28 | })
29 | ```
30 | - Add the `mergeReducers` utility in 'redux-fractal/utils'
31 | - Renamed 'triggerComponentKey' and 'currentComponentKey' set on actions meta by
32 | `redux-fractal` to reduxFractalTriggerComponent and reduxFractalCurrentComponent to prevent
33 | name collisions with user code.
34 | - Documentation improvements
35 |
36 | ## 1.2
37 |
38 | - Made the `persist` flag configurable by being able to define it also as a function of props
39 |
40 | ## 1.1
41 |
42 | - Add ability to persist state when component unmounts by configuring `local` HOC with a `persist` boolean flag
43 |
44 | ## 1.0.0
45 |
46 | - Initial release
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Cazaciuc Gabriel
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-fractal
2 | 
3 | [](https://coveralls.io/github/gcazaciuc/redux-fractal)
4 |
5 | Local component state & actions in Redux.
6 |
7 | Provides the means to hold up local component state in Redux state, to dispatch locally scoped actions and to react to global ones.
8 |
9 | What Redux fractal offers is a Redux private store for each component with the notable difference that the component state is actually held up in your
10 | app's state atom, so all global and components ui state live together.
11 |
12 | The unique and powerful approach consists in the fact that it allows you to
13 | use the built-in Redux createStore, combineReducers and others for defining the shape and managing the UI ,state with great benefits:
14 | - All state ( either application state or UI state) is help up in your app single state atom
15 | - Component state updates are managed via reducers just like your global app state which means predictability and easy testability
16 | - Reducers used for local state aren't aware that they are used for managing state for a component instance. As such they can be easily re-used across components or even used for managing global state.
17 | - You can have per component middleware. This opens up interesting possibilities like locally scoped sagas(eg redux-saga), intercepting and handling of all actions generated by a component before they reach the component's UI store.
18 | - By default a component intercepts in reducer only actions generated by itself but it;s easy to enable intercepting global actions or actions generated by other components.
19 |
20 | It's easy to get started using redux-fractal
21 | ## Installation
22 |
23 | ```console
24 | $ npm install --save redux-fractal
25 | ```
26 | ## Usage
27 | ### Importing the local reducer into global store
28 | Add the local reducer to the redux store under the
29 | 'local' reducer key;
30 | ```js
31 | import { localReducer } from 'redux-fractal';
32 | const store = createStore(combineReducers({
33 | local: localReducer,
34 | myotherReducer: myotherReducer
35 | }))
36 | ```
37 | ### Adding the `local` HOC to components maintaining UI state
38 | Decorate the components that hold ui state( transient state, scoped to that very specific component ) with the 'local' higher order component and provide a mandatory, globally unique key for your component and a `createStore` method.
39 |
40 | The key can be generated based on props or a static string but it must be stable between re-renders. Basically it should follow exactly
41 | the same rules as the React component 'key'. In general it should be unique among components, unless you
42 | want multiple components to use the same store.
43 | ```js
44 | import { local } from 'redux-fractal';
45 | import { createStore } from 'redux';
46 |
47 | const CompToRender = local({
48 | key: 'myDumbComp',
49 | createStore: (props) => {
50 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
51 | }
52 | })(Table);
53 | ```
54 | ### Defining root reducer for the private store
55 | Define a root reducer that will intercept own dispatched functions( by default only actions dispatched from the wrapped component, but the 'local' higher order component can be easily configured to intercept actions from global Redux instance or generated by other components):
56 | ```js
57 | const rootReducer = (state = { filter: null, sort: null, trigger: '', current: '' }, action) => {
58 | switch(action.type) {
59 | case 'SET_FILTER':
60 | return Object.assign({}, state, { filter: action.payload });
61 | case 'SET_SORT':
62 | return Object.assign({}, state,
63 | { sort: action.payload });
64 | case 'GLOBAL_ACTION':
65 | return Object.assign({}, state, { filter: 'globalFilter' });
66 | case 'RESET_DEFAULT':
67 | return Object.assign({}, state, { sort: state.sort+'_globalSort' });
68 | default:
69 | return state;
70 | }
71 | };
72 | ```
73 | Note that the reducer is like any other ordinary reducer used in a Redux app. The difference is that it manages and controls the state transitions for a certain component state.
74 |
75 | In fact, you can use whatever method of combining reducers you use for your app with no exceptions, also for the individual components:
76 | ```js
77 | import { combineReducers, createStore } from 'redux';
78 | const rootReducer = combineReducers({
79 | filter: filterReducer,
80 | sort: sortReducer
81 | });
82 | local({
83 | key: (props) => props.tableID,
84 | createStore: (props) => {
85 | return createStore(rootReducer, componentInitialState);
86 | }
87 | })
88 | ```
89 | ## Accessing local state and dispatching local actions
90 | The well know `mapStateToProps`, `mapDispatchToProps`, and `mergeProps` familiar from react-redux `connect` are available having the very same signatures.
91 | In fact , internally, redux-fractal uses the connect function from 'react-redux' to connect the component to it's private store.
92 |
93 | The difference is, that you get only the component's state in `mapStateToProps` as opposed to the entire app state and the `dispatch`
94 | function in `mapDispatchToProps` dispatches an action tagged with the component key as specified in the HOC config. `mergeProps`, on the other hand, is no different.
95 | `mapStateToProps`, `mapDispatchToProps`, and `mergeProps` are completely optional, define them if you need them.
96 | ### Mapping component state to props
97 | Beware that the components wrapped in 'local' HOC do not update when global state changes but only when their own state changes. These components are effectively connected to their own private store.
98 | Of course, you can also connect them to the global store using standard 'connect' function.
99 | ```js
100 | local({
101 | key: 'mycomp',
102 | createStore: (props) => createStore(rootReducer, initialState),
103 | mapStateToProps: (componentState, ownProps) => {
104 | // Get component state from it's private store and
105 | // component own props.
106 | // You must return an object containing the keys that will become props to the component just like in react redux 'connect'
107 | return {
108 | filter: getFilter(componentState)
109 | }
110 | },
111 |
112 | })
113 | ```
114 | By default, if no `mapStateToProps` is defined, then all keys from the component's state become
115 | individual props in the wrapped component:
116 | ```js
117 | local({
118 | key: 'mycomp',
119 | createStore: (props) => createStore(rootReducer, { filter: 'all', sort: 'asc' }),
120 | mapStateToProps: (componentState, ownProps) => {
121 | // Get component state from it's private store and
122 | // component own props.
123 | // You must return an object containing the keys that will become props to the component just like in react redux 'connect'
124 | return {
125 | filter: getFilter(componentState)
126 | }
127 | },
128 |
129 | })(Table)
130 | ```
131 | In the Table you now have access to the 'filter' state via props.filter.
132 | ### Dispatching local actions
133 | Local actions can be dispatched by defining a 'mapDispatchToProps' function.
134 | Note that local dispatches can be caught by the component's own reducer AND by global, application wide reducers.
135 | ```js
136 | import { updateSearchTerm } from ''
137 | local({
138 | key: 'mycomp',
139 | createStore: (props) => createStore(rootReducer, initialState),
140 | mapDispatchToProps: (dispatch) => {
141 | // ALL actions dispatched via 'dispatch' function above have the component key tagged to the action.
142 | // You can see that by inspecting action.meta.reduxFractalTriggerComponent in redux dev tools.
143 | // All local actions are dispatched also on the global store
144 | // You must return an object containing the keys that will become props to the component just like in react redux 'connect'
145 | return {
146 | onFilter: (term) => dispatch(updateSearchTerm(term))
147 | }
148 | },
149 |
150 | })
151 | ```
152 |
153 | These actions can be caught in any global reducer and by default only in the originating component reducer.
154 | One can inspect the originating component by looking at `action.meta.reduxFractalTriggerComponent` to get the component's key that dispatched the action.
155 |
156 | ```js
157 | import { combineReducers, createStore } from 'redux';
158 | const rootReducer = combineReducers({
159 | filter: filterReducer,
160 | sort: sortReducer
161 | });
162 | local({
163 | id: "mygreattable",
164 | createStore: (props) => {
165 | return createStore(rootReducer, componentInitialState);
166 | },
167 | mapStateToProps: (componentState, ownProps) => ({
168 | filter: getTableFilter(componentState)
169 | }),
170 | mapDispatchToProps: (localDispatch) =>({
171 | onFilter: (term) => localDispatch(updateSearchTerm(term))
172 | })
173 | })
174 | ```
175 | ### Reacting to globally dispatched actions or actions dispatched from other components
176 | By default your component will not react to when other components dispatch
177 | local actions or when something is being dispatched in the global store.
178 | You can change that using `filterGlobalActions` which must return either true if the action
179 | can be forwarded to the component's store or false otherwise.
180 | Locally dispatched actions are ALWAYS forwarded to the corresponding component reducer.
181 | You need to define only if you care in updating the component state based on actions happening globally or in other
182 | components.
183 |
184 | ```js
185 | local({
186 | id: (props) => props.itemID,
187 | creat
188 | filterGlobalActions: (action) => {
189 | // Any logic to determine if the actions should be forwarded
190 | // to the component's reducer. By default none is except those
191 | // originated by component itself
192 | const allowedActions = ['RESET_FILTERS', 'CLEAR_SORTING'];
193 | return allowedActions.indexOf(action.type) !== -1;
194 | }
195 | })
196 | ```
197 | Now any `RESET_FILTERS` or `CLEAR_SORTING` global actions or originated by other components will be allowed.
198 | You have lots of flexibility with this method to react when a component updates it's UI state.
199 | Crazy example: when the sorting from one component changes all dropdowns from another component should close:
200 | ```js
201 | local({
202 | id: 'dropdownsContainer',
203 | createStore: (props) => {
204 | return createStore((state = {isClosed: false}, action) => {
205 | switch(action.type) {
206 | case SET_SORT:
207 | return Object.assign({}, state, { isClosed: true });
208 | break;
209 | default:
210 | return state;
211 | }
212 | });
213 | },
214 | filterGlobalActions: (action) => {
215 | // This component is interested in updating it's state
216 | // when things happen in the 'mygreattable' component, in this
217 | // case when sorting changes
218 | const allowedActions = ['SET_SORT'];
219 | return allowedActions.indexOf(action.type) !== -1 && actions.meta.reduxFractalTriggerComponent === 'mygreattable';
220 | }
221 | })
222 | ```
223 | ## Merging local state, local dispatch, and own props
224 | Just like in `connect`, you have the opportunity to transform the final props that are passed into your component using `mergeProps`:
225 | ```
226 | local({
227 | key: 'mycomp',
228 | createStore: (props) => createStore(rootReducer, initialState),
229 | mapStateToProps: (componentState, ownProps) => {
230 | ...
231 | },
232 | mapDispatchToProps: (localDispatch) =>({
233 | onFilter: (term) => localDispatch(updateSearchTerm(term))
234 | }),
235 | mergeProps: (state, localDispatch, ownProps) =>({
236 | {
237 | ...ownProps,
238 | ...state,
239 | ...localDispatch
240 | }
241 | })
242 | })
243 | ```
244 | ## Local middleware
245 | Since Redux-fractal relies on redux for manging the component state and offers a private store for the component
246 | you can define middleware for the private component store using the exact same approach as you do when setting up
247 | the application's store.
248 | This opens up some interesting possibilities:
249 | - Use locally scoped sagas that get destroyed on component unmount(eg using [Redux saga](https://github.com/yelouafi/redux-saga) ). These sagas will have access only to the component's private store( so all yield select() would actually return the component's state) and also be able to react to all local actions and actions allowed by `filterGlobalActions`.
250 | - Apply per component throttling/debouncing of actions using middleware's such as redux-batch-subscribers
251 | - Transform the actions originating in components of a certain type
252 | - Track at all times the component that initiated for eg a server request, validations etc by using locally
253 | scoped redux-thunk middleware and dispatching thunks( in which case the 'dispatch' and `getState` functions will be the ones from the component local store)
254 | - Re-use middleware that you use on the global store on the component's private store
255 |
256 | ### Local Redux saga example
257 | First install redux-saga as describe here [Redux saga](https://github.com/yelouafi/redux-saga).
258 | Define a saga somewhere( eg in sagas.js):
259 | ```js
260 | function* fetchUser(action) {
261 | const compState = yield select();
262 | try {
263 | const user = yield call(Api.fetchUser, action.payload.userId);
264 | yield put({type: "USER_FETCH_SUCCEEDED", user: user});
265 | } catch (e) {
266 | yield put({type: "USER_FETCH_FAILED", message: e.message});
267 | }
268 | }
269 | export default function* mySaga() {
270 | yield* takeLatest("USER_FETCH_REQUESTED", fetchUser);
271 | }
272 | ```
273 | Then wrap a component in the 'local' HOC:
274 | ```js
275 | import { createStore, applyMiddleware } from 'redux'
276 | import createSagaMiddleware from 'redux-saga'
277 |
278 | import reducer from './reducers'
279 | import mySaga from './sagas'
280 | const componentRootReducer = (state = { user: {} }, action) => {
281 | switch(action.type) {
282 | case USER_FETCH_SUCCEEDED:
283 | return Object.assign({}, state, { user: action.payload });
284 | default:
285 | return state;
286 | }
287 | };
288 | local({
289 | key: 'formContainer',
290 | createStore: (props) => {
291 | // create the saga middleware
292 | const sagaMiddleware = createSagaMiddleware();
293 | const store = createStore(
294 | componentRootReducer,
295 | applyMiddleware(sagaMiddleware)
296 | );
297 | sagaMiddleware.run(mySaga)
298 | return { store: store, cleanup: () => sagaMiddleware.cancel() };
299 | },
300 | mapDispatchToProps: (dispatch) => {
301 | onFetchUser: (userId) => dispatch({type: "USER_FETCH_REQUESTED", payload: userId })
302 | }
303 | })
304 | ```
305 | All of the `put()` effects dispatch a local action.
306 | All of the `select()` effects return parts of the component's state.
307 | All of the `take()` effects react to the very same action's that local reducers are allowed to react:
308 | - Locally dispatched actions
309 | - Global or other component actions allowed by `filterGlobalActions` function.
310 |
311 | It's important to note that by default component stores do not contain middleware, just as
312 | the global Redux store doesn't contain it by default and middleware needs to be added to it.
313 | This means for example that implictly you can only dispatch plain objects. To dispatch functions, promises etc
314 | configure the private component state with the needed middleware( eg redux-thunk etc )
315 | ## Recipes
316 | ### I need access to global application state in mapStateToProps of the `local()` component
317 |
318 | Access to only the component's internal state in `mapStateToProps` is a conscious design decision.
319 | If you need data from the global state and need to update the component when that data changes you can
320 | wrap the component returned by `local` in `connect()`.
321 | That way you will be able to pass data from the global store into the component returned by `local` via props.
322 | ```js
323 | import { connect } from 'react-redux';
324 | import { local } from 'redux-fractal';
325 | import { createStore, compose } from 'redux';
326 | const wrapper = compose(
327 | connect((state) => ({
328 | userSettings: state.UserSettingsReducer.settings
329 | })),
330 | local({
331 | key: 'comp',
332 | createStore: (props) => {
333 | const filterVal = props.userSettings.defaultFilter;
334 | return createStore(componentRootReducer, { filter: filterVal });
335 | }
336 | });
337 | export default wrapper(MyComponent);
338 | ```
339 | ### I need to keep the local state even if the component gets unmounted and restore it when it gets mounted again
340 | In that case you can use the `persist` flag on the `local` HOC. When the flag is set to `true`
341 | you will get the existing component state, when the component re-mounts, as the second parameter
342 | to the `createStore` function.
343 | ```js
344 | import { local } from 'redux-fractal';
345 | import { createStore } from 'redux';
346 | const wrapper = local({
347 | key: 'comp',
348 | createStore: (props, existingState) => {
349 | const filterVal = 'search';
350 | return createStore(componentRootReducer, existingState || { filter: filterVal });
351 | },
352 | persist: true
353 | });
354 | export default wrapper(MyComponent);
355 | ```
356 | If there are multiple instance of MyComponent, using `persist` as shown above will persist the state of ALL instances.
357 | You can have fine grained control over which component instances get persisted and which do not
358 | by defining `persist` as a function receiving the component props.
359 | ```js
360 | const HOC = local({
361 | key: (props) => props.itemId,
362 | createStore: (props, existingState) => {
363 | return createStore(
364 | rootReducer,
365 | existingState || { filter: true }
366 | );
367 | },
368 | // Any logic depending on props here to decide if the component state should be persisted
369 | persist: (props) => props.keepState
370 | });
371 | const ConnectedItem = HOC(Item);
372 |
373 | const App = (props) => {
374 | return (
375 |
376 |
377 |
378 |
379 | );
380 | }
381 | ```
382 | In the above example only the state of component with itemId 'a' will be kept in the global
383 | state after the component unmounts.
384 |
385 | ### I need to read a component's state somewhere else: eg in thunk action creators, sagas, in `mapStateToProps`
386 | All the local components state is available at `state.local[key]` where `key` is the key for the component
387 | as return by the `key` property of the `local` HOC.
388 |
389 | ### Sharing the same stores across multiple components
390 |
391 | Starting with version 1.3 it's possible to share the same store across multiple components. All of the components
392 | having the same `key` will have access to the store's state and be able to dispatch actions on it.
393 | To do this in a sane manner there are a few rules to be followed:
394 | - Components having the same `key` value defined for `local` HOC use the same store
395 | - The component sharing the store state must be a parent of the components consuming it
396 | - Child components accessing the shared store should NOT define a `createStore` method. Only the parent component should do this.
397 | - The store lifetime is controlled by the parent defining it.
398 |
399 | It sounds more complicated than it actually is. Let's see an example:
400 |
401 | ```js
402 | // in containers/ParentComp.js
403 | const ParentComp = local({
404 | key: 'parentKey',
405 | createStore: (props) => createStore(rootReducer, {sort: 1})
406 | });
407 | ```
408 |
409 | ```js
410 | // in containers/ChildComp.js
411 | const ChildComp = local({
412 | key: 'parentKey',
413 | mapDispatchToProps: (dispatch) => {
414 | onSort: (val) => dispatch(onSort(val)) // I'm dispatching actions on ParentComp store
415 | },
416 | mapStateToProps: (state, ownProps) => {
417 | // I got the whole ParentComp state in here. Take the state slices you need and inject
418 | // them in the child component
419 | }
420 | });
421 | ```
422 |
423 | ### I would like to use React `context` on the components returned by `local`
424 |
425 | There is only 1 thing to be aware: `local` returns a component that already has
426 | `contextTypes` defined in order to be able to access the global Redux store.
427 | As such take care to extend the `contextTypes` and not over-write them.
428 |
429 | Also note that `key`, `createStore` and `persist` receive the component context as
430 | the last parameter so you can make use of everything on the `context` besides props to
431 | create a component's key, store state or control it's persistence settings.
432 |
433 | ```js
434 | // in containers/ParentComp.js
435 | const WrappedComp = local({
436 | key: (props, context) => context.parentKey,
437 | createStore: (props, existingState, context) => ...
438 | persist: (props, context) => ....
439 | })(MyComp);
440 | WrappedComp.contextTypes = Object.assign({}, WrappedComp.contextTypes, {
441 | parentKey: React.PropTypes.string
442 | });
443 | ```
444 |
445 | ### I need to do some store cleanup(eg cancel middleware etc) upon component unmount
446 |
447 | `createStore` can return either a Redux store object or an object having 2 keys:
448 |
449 | ```js
450 |
451 | createStore: (props, existingState) => ({
452 | store: createStore( .... ), // this is the result of Redux createStore
453 | cleanup: () => .... // cleanup is optional, you don't have to return it
454 | })
455 | ```
456 | When the component unmounts, if a cleanup method has been defined it will be automatically invoked.
457 |
458 | ### I need to manually cleanup the state of components created with `persist: true`( a single one or all of them)
459 |
460 | In some situations you might want to blow up a component state manually:
461 | - The component is unmounted and `persist: true` was set for that component
462 | - You navigate away from the page and want to cleanup the state of all components having `persist: true`
463 |
464 | For these cases there are 2 actions that you can import and dispatch:
465 | ```js
466 | import { destroyComponentState, destroyAllComponentsState } from 'redux-fractal';
467 | Store.dispatch(destroyComponentState(componentKey)); // Destroy a specific component state
468 | Store.dispatch(destroyAllComponentsState()); // Destroy all components state
469 | ```
470 |
471 | Please note that if there are still components mounted that listen to the destroyed stores
472 | the components will not update anymore.
473 |
474 | ### I want to re-use reducers in multiple components
475 |
476 | You can use any strategy you want for creating the root reducer of a certain component.
477 | Eg you can use `combineReducers` from Redux or apply other strategies.
478 |
479 | One strategy that we found usefull was the `mergeReducers` and it's shipped as part of redux-fractal.
480 | Let's suppose you have the following reducers:
481 | ```js
482 | const EditableReducer = (state = { editState: false }, action) => {
483 | case EDIT_STARTED:
484 | return ..... // Determine new state somehow
485 | case EDIT_STOPPED:
486 | return .... // Determine new state somehow
487 | }
488 |
489 | const FiltersReducer = (state = { filtersList: [] }, action) => {
490 | case SET_FILTER:
491 | return ..... // Determine new state somehow
492 | case REMOVE_FILTER:
493 | return .... // Determine new state somehow
494 | }
495 | ```
496 | What you would like is to take these 2 reducers and apply them to any components
497 | that have editable and filter behavior , basically which have the same way
498 | of updating their filters and edit state.
499 | Besides that it would be good if the component would also have it's own, component specific data.
500 |
501 | ```js
502 | import { mergeReducers } from 'redux-fractal/utils';
503 | const ComponentSpecificReducer = (state = { data: {} }, action) => {
504 | case COMPONENT_ACTION:
505 | return .....
506 | }
507 | const componentRootReducer = mergeReducers(ComponentSpecificReducer, EditableReducer, FiltersReducer)
508 | ```
509 |
510 | Some interesting points:
511 |
512 | - The final component state would have the shape { data: {}, filtersList: [], editState: false }.
513 | Of course the initial values can be supplied via `createStore` as usual.
514 | - When an action comes in it will be passed throught all reducers from right to left: FiltersReducer then EditableReducer then ComponentSpecificReducer.
515 | - Each reducer will receive the full component state not only it's piece, but it doesn't need to be aware of that is concerned with only updating
516 | it's piece of data( so each one will receive { data: {}, filtersList: [], editState: false } )
517 | - The merged reducers must all return objects so that they can be merged together into a final state.
518 |
519 |
520 |
521 | ## TODO (Help wanted)
522 | - Write additional tests
523 | - Verify server side rendering
524 | - Improve and better organize docs, add examples, add a contributing guide
525 | - Development warnings similar to React in dev mode for common mistakes
526 |
527 | Feel free to add anything else I may have missed by opening an issue.
528 | We welcome every contribution!
529 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-fractal",
3 | "version": "1.8.1",
4 | "description": "A local component state management library using Redux",
5 | "main": "dist/index.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "npmName": "redux-fractal",
10 | "scripts": {
11 | "build": "rimraf dist && cross-env BABEL_ENV=production && $(npm bin)/babel -d dist src",
12 | "prepublish": "npm run build",
13 | "test-dev": "cross-env BABEL_ENV=development && cross-env NODE_ENV=development && ava --watch --serial --no-cache tests/*Spec.js",
14 | "test-dev-coverage": "cross-env BABEL_ENV=development && nyc ava --serial --watch tests/*Spec.js",
15 | "test": "cross-env BABEL_ENV=development && nyc ava --serial tests/*Spec.js",
16 | "pretty-coverage": "./node_modules/.bin/nyc report --reporter=html"
17 | },
18 | "ava": {
19 | "require": [
20 | "babel-core/register",
21 | "./tests/helpers/setupEnv.js"
22 | ],
23 | "babel": "inherit"
24 | },
25 | "babel": {
26 | "presets": [
27 | "es2015",
28 | "react"
29 | ],
30 | "plugins": [
31 | "transform-runtime"
32 | ],
33 | "ignore": [
34 | "tests/**/*Spec.js"
35 | ],
36 | "env": {
37 | "development": {
38 | "sourceMaps": "inline"
39 | }
40 | }
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/gcazaciuc/redux-fractal.git"
45 | },
46 | "keywords": [
47 | "javascript",
48 | "redux",
49 | "react",
50 | "local state",
51 | "fractal architecture",
52 | "component state",
53 | "ui state",
54 | "React state"
55 | ],
56 | "author": "Cazaciuc Gabriel",
57 | "license": "MIT",
58 | "bugs": {
59 | "url": "https://github.com/gcazaciuc/redux-fractal/issues"
60 | },
61 | "homepage": "https://github.com/gcazaciuc/redux-fractal#readme",
62 | "devDependencies": {
63 | "ava": "^0.16.0",
64 | "babel": "^6.5.2",
65 | "babel-cli": "^6.14.0",
66 | "babel-core": "^6.14.0",
67 | "babel-eslint": "^6.1.2",
68 | "babel-plugin-transform-runtime": "^6.15.0",
69 | "babel-polyfill": "^6.13.0",
70 | "babel-preset-es2015": "^6.14.0",
71 | "babel-preset-react": "^6.11.1",
72 | "babel-register": "^6.14.0",
73 | "coveralls": "^2.11.12",
74 | "cross-env": "^3.0.0",
75 | "enzyme": "^2.4.1",
76 | "eslint": "^3.4.0",
77 | "eslint-config-airbnb": "^10.0.1",
78 | "eslint-plugin-import": "^1.14.0",
79 | "eslint-plugin-jsx-a11y": "^2.2.1",
80 | "eslint-plugin-react": "^6.2.0",
81 | "jsdom": "^9.4.2",
82 | "nyc": "^8.1.0",
83 | "prop-types": "^15.6.0",
84 | "react": "^15.3.1",
85 | "react-addons-test-utils": "^15.3.1",
86 | "react-dom": "^15.3.1",
87 | "react-redux": "^4.4.5",
88 | "redux": "^3.5.2",
89 | "redux-saga": "^0.12.0",
90 | "rimraf": "^2.5.4",
91 | "sinon": "^1.17.6"
92 | },
93 | "peerDependencies": {
94 | "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0",
95 | "prop-types": "^15.6.0",
96 | "redux": "^2.0.0 || ^3.0.0",
97 | "react-redux": "*"
98 | },
99 | "dependencies": {
100 | "babel-runtime": "^6.11.6",
101 | "hoist-non-react-statics": "^1.2.0",
102 | "invariant": "^2.2.1"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | export const CREATE_COMPONENT_STATE = '@@ui/CREATE_COMPONENT_STATE';
2 | export const DESTROY_COMPONENT_STATE = '@@ui/DESTROY_COMPONENT_STATE';
3 | export const DESTROY_ALL_COMPONENTS_STATE = '@@ui/DESTROY_ALL_COMPONENTS_STATE';
4 |
5 | export const destroyComponentState = (componentKey) => ({
6 | type: DESTROY_COMPONENT_STATE,
7 | payload: { persist: false, hasStore: true },
8 | meta: { reduxFractalTriggerComponent: componentKey },
9 | });
10 | export const destroyAllComponentsState = () => ({
11 | type: DESTROY_ALL_COMPONENTS_STATE,
12 | payload: null,
13 | });
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import local from './local.js';
2 | import localReducer from './localReducer.js';
3 | import {
4 | destroyComponentState, destroyAllComponentsState,
5 | } from './actions.js';
6 |
7 | export { local };
8 | export { localReducer };
9 | export { destroyComponentState, destroyAllComponentsState };
10 |
--------------------------------------------------------------------------------
/src/local.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import invariant from 'invariant';
4 | import hoistNonReactStatics from 'hoist-non-react-statics';
5 | import { connect } from 'react-redux';
6 | import * as UIActions from './actions.js';
7 | import { createStore } from './localReducer.js';
8 |
9 | export default (Config) => (Component) => {
10 | const defaultMapStateToProps = (state) => state;
11 | const ConnectComp = connect(
12 | Config.mapStateToProps || defaultMapStateToProps,
13 | Config.mapDispatchToProps,
14 | Config.mergeProps)((props) => {
15 | const newProps = Object.assign({}, props);
16 | delete newProps.store;
17 | // eslint-disable-next-line
18 | return ();
19 | });
20 | class UI extends React.Component {
21 | constructor(props, context) {
22 | super(props, context);
23 | const compKey = typeof Config.key === 'function' ?
24 | Config.key(props, context) : Config.key;
25 | this.store = null;
26 | invariant(Config.key,
27 | '[redux-fractal] - You must supply a key to the component either as a function or string');
28 | this.compKey = compKey;
29 | this.unsubscribe = null;
30 | }
31 | componentWillMount() {
32 | const existingState = this.context.store.getState().local[this.compKey];
33 | const storeResult = createStore(
34 | Config.createStore, this.props,
35 | this.compKey, existingState, this.context);
36 | this.store = storeResult.store;
37 | this.storeCleanup = storeResult.cleanup;
38 | this.context.store.dispatch({
39 | type: UIActions.CREATE_COMPONENT_STATE,
40 | payload: { config: Config, props: this.props, store: this.store, hasStore: !!Config.createStore },
41 | meta: { reduxFractalTriggerComponent: this.compKey },
42 | });
43 | }
44 | componentWillUnmount() {
45 | const persist = typeof Config.persist === 'function' ?
46 | Config.persist(this.props, this.context) : Config.persist;
47 | setTimeout(() => {
48 | this.context.store.dispatch({
49 | type: UIActions.DESTROY_COMPONENT_STATE,
50 | payload: { persist, hasStore: !!Config.createStore },
51 | meta: { reduxFractalTriggerComponent: this.compKey }
52 | });
53 | if (this.storeCleanup) {
54 | this.storeCleanup();
55 | }
56 | this.store = null;
57 | }, 0);
58 | }
59 | render() {
60 | if (this.props.store) {
61 | // eslint-disable-next-line
62 | console.warn(`Props named 'store' cannot be passed to redux-fractal 'local'
63 | HOC with key ${this.compKey} since it's a reserved prop`);
64 | }
65 | return (
66 | this.store &&
70 | );
71 | }
72 | }
73 |
74 | UI.contextTypes = Object.assign({}, Component.contextTypes, {
75 | store: PropTypes.shape({
76 | subscribe: PropTypes.func.isRequired,
77 | dispatch: PropTypes.func.isRequired,
78 | getState: PropTypes.func.isRequired,
79 | }),
80 | });
81 | UI.propTypes = Object.assign({}, {
82 | store: PropTypes.shape({
83 | subscribe: PropTypes.func.isRequired,
84 | dispatch: PropTypes.func.isRequired,
85 | getState: PropTypes.func.isRequired,
86 | }),
87 | });
88 | const displayName = Component.displayName || Component.name || 'Component';
89 | UI.displayName = `local(${displayName})`;
90 | return hoistNonReactStatics(UI, Component);
91 | };
92 |
--------------------------------------------------------------------------------
/src/localReducer.js:
--------------------------------------------------------------------------------
1 | import * as UIActions from './actions.js';
2 |
3 | const stores = {};
4 | const globalActions = {};
5 | const refCounter = {};
6 | const defaultGlobalFilter = () => false;
7 |
8 | const initialiseComponentState = (state, payload, componentKey) => {
9 | const { config, store } = payload;
10 | stores[componentKey] = store;
11 | refCounter[componentKey] = refCounter[componentKey] || 0;
12 | refCounter[componentKey]++;
13 | globalActions[componentKey] = config.filterGlobalActions || defaultGlobalFilter;
14 | const initialState = stores[componentKey].getState();
15 | const newComponentsState = Object.assign({}, state, { [componentKey]: initialState });
16 | return newComponentsState;
17 | };
18 | const destroyComponentState = (state, payload, componentKey) => {
19 | refCounter[componentKey] = refCounter[componentKey] || 0;
20 | if (refCounter[componentKey] > 0) {
21 | refCounter[componentKey]--;
22 | }
23 | if (refCounter[componentKey]) {
24 | return state;
25 | }
26 | const newState = Object.assign({}, state);
27 | delete newState[componentKey];
28 | delete refCounter[componentKey];
29 | delete stores[componentKey];
30 | delete globalActions[componentKey];
31 | return newState;
32 | };
33 | const updateSingleComponent = (oldComponentState, action, componentKey) => {
34 | const store = stores[componentKey];
35 | if (store) {
36 | // eslint-disable-next-line
37 | action.meta = Object.assign({}, action.meta, { reduxFractalCurrentComponent: componentKey });
38 | store.originalDispatch(action);
39 | return store.getState();
40 | }
41 | return oldComponentState;
42 | };
43 |
44 | const updateComponentState = (state, action, componentKey) => {
45 | const newState = Object.keys(state).reduce((stateAcc, k) => {
46 | const shouldUpdate = componentKey === k ||
47 | (typeof globalActions[k] === 'function' && globalActions[k](action));
48 | let updatedState = state[k];
49 | if (shouldUpdate) {
50 | updatedState = updateSingleComponent(state[k], action, k);
51 | return Object.assign({}, stateAcc, { [k]: updatedState });
52 | }
53 | return stateAcc;
54 | }, {});
55 | return Object.assign({}, state, newState);
56 | };
57 |
58 | export default (state = {}, action) => {
59 | const componentKey = action.meta && action.meta.reduxFractalTriggerComponent;
60 | let nextState = null;
61 | switch (action.type) {
62 | case UIActions.CREATE_COMPONENT_STATE:
63 | return initialiseComponentState(
64 | state,
65 | action.payload,
66 | componentKey);
67 | case UIActions.DESTROY_COMPONENT_STATE:
68 | if (!action.payload.persist && stores[componentKey]) {
69 | return destroyComponentState(state, action.payload, componentKey);
70 | }
71 | return state;
72 | case UIActions.DESTROY_ALL_COMPONENTS_STATE:
73 | nextState = state;
74 | Object.keys(state).forEach((k) => {
75 | nextState = destroyComponentState(nextState, {}, k);
76 | });
77 | return nextState;
78 | default:
79 | return updateComponentState(state, action, componentKey);
80 | }
81 | };
82 |
83 |
84 | export const createStore = (createStoreFn, props, componentKey, existingState, context) => {
85 | if (!stores[componentKey]) {
86 | const getWrappedAction = (action) => {
87 | let wrappedAction = action;
88 | if (typeof action === 'object') {
89 | const actionMeta = Object.assign({},
90 | action.meta,
91 | { reduxFractalTriggerComponent: componentKey });
92 | wrappedAction = Object.assign({}, action, { meta: actionMeta });
93 | }
94 | return wrappedAction;
95 | };
96 | const localDispatch = (action) => {
97 | const wrappedAction = getWrappedAction(action);
98 | return context.store.dispatch(wrappedAction);
99 | };
100 | const storeResult = createStoreFn(props, existingState, context);
101 | let storeCleanup = () => true;
102 | if (storeResult.store) {
103 | stores[componentKey] = storeResult.store;
104 | }
105 | if (storeResult.cleanup) {
106 | storeCleanup = storeResult.cleanup;
107 | }
108 | if (storeResult.dispatch && storeResult.getState) {
109 | stores[componentKey] = storeResult;
110 | }
111 | stores[componentKey].originalDispatch = stores[componentKey].dispatch;
112 | stores[componentKey].dispatch = localDispatch;
113 | return { store: stores[componentKey], cleanup: storeCleanup };
114 | }
115 | return { store: stores[componentKey] };
116 | };
117 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | export const mergeReducers = (...reducers) => {
3 | const reversedReducers = reducers.slice(0).reverse();
4 | return (state, action) => {
5 | let nextState = state || {};
6 | reversedReducers.forEach((reducer) => {
7 | if (typeof state === 'undefined') {
8 | // The reducers are being initilized by Redux. Give them a chance to return their
9 | // initial default value and merge all the values together for form the final state
10 | nextState = Object.assign({}, nextState, reducer(undefined, action));
11 | } else {
12 | nextState = reducer(nextState, action);
13 | }
14 | });
15 | return nextState;
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/tests/helpers/configureStore.js:
--------------------------------------------------------------------------------
1 | import localReducer from '../../src/localReducer.js';
2 | import { createStore, combineReducers } from 'redux';
3 | export function configureStore() {
4 | const store = createStore(
5 | combineReducers({
6 | local: localReducer,
7 | isVisible: (state = true, action) => {
8 | switch (action.type) {
9 | case 'CLOSE':
10 | return false;
11 | default:
12 | return state;
13 | }
14 | },
15 | someGlobalState: (state = { isGlobal: true }, action) => {
16 | switch (action.type) {
17 | case 'SET_GLOBAL':
18 | return Object.assign({}, state, { isGlobal: action.payload });
19 | default:
20 | return state;
21 | }
22 | },
23 | })
24 | );
25 | return store;
26 | }
27 |
28 | export const Store = configureStore();
29 |
--------------------------------------------------------------------------------
/tests/helpers/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeEvery, takeLatest } from 'redux-saga'
2 | import { call, put, select } from 'redux-saga/effects'
3 | const getUser = (userId, sort) => ({ username: 'test', id: userId, sort: sort });
4 |
5 | function* fetchUser(action) {
6 | const compState = yield select();
7 | try {
8 | const user = yield call(getUser, action.payload, compState.sort);
9 | yield put({type: "USER_FETCH_SUCCEEDED", payload: user});
10 | } catch (e) {
11 | yield put({type: "USER_FETCH_FAILED", payload: e.message});
12 | }
13 | }
14 |
15 | function* mySaga() {
16 | yield* takeEvery("USER_FETCH_REQUESTED", fetchUser);
17 | }
18 |
19 | export default mySaga;
20 |
--------------------------------------------------------------------------------
/tests/helpers/setupEnv.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is used to set up the environment that's needed for most
3 | * of the unit tests for the project which includes babel transpilation
4 | * with babel-register, polyfilling, and initializing the DOM with jsdom
5 | */
6 | require('babel-register')
7 | require('babel-polyfill')
8 |
9 | global.document = require('jsdom').jsdom('')
10 | global.window = document.defaultView
11 | global.navigator = window.navigator
12 |
--------------------------------------------------------------------------------
/tests/localReducerSpec.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import localReducer from '../src/localReducer.js';
3 |
4 | test('Should return the correct initial state for the component', t => {
5 | t.deepEqual(localReducer(undefined, {type: undefined, meta: {}}), {});
6 | });
7 |
--------------------------------------------------------------------------------
/tests/localSpec.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { mount } from 'enzyme';
3 | import { spy, useFakeTimers } from 'sinon';
4 | import PropTypes from 'prop-types';
5 | import React from 'react';
6 | import local from '../src/local.js';
7 | import { Provider, connect } from 'react-redux';
8 | import { createStore } from 'redux';
9 | import mySaga from './helpers/sagas.js';
10 | import { configureStore } from './helpers/configureStore.js';
11 | import createSagaMiddleware from 'redux-saga';
12 | import { applyMiddleware } from 'redux';
13 | import { destroyAllComponentsState, destroyComponentState } from '../src/index.js';
14 |
15 | class DummyComp extends React.Component {
16 | constructor(props, context) {
17 | super(props, context);
18 | }
19 | render() {
20 | return ();
21 | }
22 | }
23 | DummyComp.displayName = 'DummyComp';
24 | DummyComp.childContextTypes = {
25 | color: PropTypes.string
26 | };
27 | DummyComp.staticFn = () => 'query';
28 | DummyComp.staticProp = 'staticProp';
29 |
30 | class ContextProviderComp extends React.Component {
31 | constructor(props) {
32 | super(props);
33 | }
34 | getChildContext() {
35 | return { sortOrder: "asc", "keepState": true, "id": 'abc' };
36 | }
37 | render() {
38 | return React.Children.only(this.props.children);
39 | }
40 | }
41 | ContextProviderComp.childContextTypes = {
42 | sortOrder: PropTypes.string,
43 | keepState: PropTypes.bool,
44 | id: PropTypes.string
45 | };
46 | const rootReducer = (state = { filter: null, sort: null, trigger: '', current: '' }, action) => {
47 | switch(action.type) {
48 | case 'SET_FILTER':
49 | return Object.assign({}, state, { filter: action.payload });
50 | case 'SET_SORT':
51 | // console.log(action.meta && JSON.stringify(action.meta));
52 | return Object.assign({}, state,
53 | { sort: action.payload,
54 | trigger: action.meta && action.meta.reduxFractalTriggerComponent,
55 | current: action.meta && action.meta.reduxFractalCurrentComponent
56 | });
57 | case 'GLOBAL_ACTION':
58 | return Object.assign({}, state, { filter: 'globalFilter' });
59 | case 'RESET_DEFAULT':
60 | return Object.assign({}, state, { sort: state.sort+'_globalSort' });
61 | default:
62 | return state;
63 | }
64 | };
65 |
66 | test('Should return the correct initial state for the component', t => {
67 | const CompToRender = local({
68 | key: 'myDumbComp',
69 | filterGlobalActions: (action) => {
70 | return false;
71 | },
72 | createStore: (props) => {
73 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
74 | },
75 | mapDispatchToProps:(dispatch) => ({
76 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
77 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
78 | })
79 | })(DummyComp);
80 | const wrapper = mount();
81 | const filterVal = wrapper.find('DummyComp').props().filter;
82 | const sortVal = wrapper.find('DummyComp').props().sort;
83 | t.deepEqual(filterVal, true);
84 | t.deepEqual(sortVal, 'desc');
85 | wrapper.unmount();
86 | return new Promise((resolve) => setTimeout(() => resolve(), 10));
87 | });
88 |
89 | test(`Should dispatch local actions that update component state. The local actions
90 | should also hit the global app reducers`, t => {
91 | const Store = configureStore();
92 | const CompToRender = local({
93 | key: 'myDumbComp',
94 | filterGlobalActions: (action) => {
95 | return false;
96 | },
97 | createStore: (props) => {
98 | return createStore(rootReducer, { filter: true, sort: props.sortOrder });
99 | },
100 | mapDispatchToProps:(dispatch) => ({
101 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
102 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
103 | })
104 | })(DummyComp);
105 | const wrapper = mount(
106 |
107 |
108 | );
109 | let dumbComp = wrapper.find('DummyComp');
110 | dumbComp.props().onFilter('my term');
111 | let filterVal = wrapper.find('DummyComp').props().filter;
112 | let sortVal = wrapper.find('DummyComp').props().sort;
113 | t.deepEqual(filterVal, 'my term');
114 | t.deepEqual(sortVal, 'desc');
115 | dumbComp = wrapper.find('DummyComp');
116 | dumbComp.props().onSort('asc');
117 | filterVal = wrapper.find('DummyComp').props().filter;
118 | sortVal = wrapper.find('DummyComp').props().sort;
119 | t.deepEqual(filterVal, 'my term');
120 | t.deepEqual(sortVal, 'asc');
121 | // Check that global state is also updated
122 | t.deepEqual(Store.getState().local,
123 | {"myDumbComp":{"filter":"my term","sort":"asc", trigger:"myDumbComp", current:"myDumbComp"}});
124 | wrapper.unmount();
125 | return new Promise((resolve) => setTimeout(() => resolve(), 10));
126 | });
127 |
128 | test(`Should forward global actions to the component as long as they pass
129 | the global actions filter`, t => {
130 | const Store = configureStore();
131 | const CompToRender = local({
132 | key: 'myDumbComp',
133 | filterGlobalActions: (action) => {
134 | return true;
135 | },
136 | createStore: (props) => {
137 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
138 | },
139 | mapDispatchToProps:(dispatch) => ({
140 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
141 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
142 | })
143 | })(DummyComp);
144 | const wrapper = mount();
145 | let filterVal = wrapper.find('DummyComp').props().filter;
146 | t.deepEqual(filterVal, true);
147 | Store.dispatch({ type: 'GLOBAL_ACTION' });
148 | t.deepEqual(wrapper.find('DummyComp').props().filter, 'globalFilter');
149 | wrapper.unmount();
150 | return new Promise((resolve) => setTimeout(() => resolve(), 10));
151 | });
152 |
153 | test(`Should NOT forward any global actions if 'filterGlobalActions' function is not defined`, t => {
154 | const Store = configureStore();
155 | const CompToRender = local({
156 | key: 'myDumbComp',
157 | createStore: (props) => {
158 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
159 | },
160 | mapDispatchToProps:(dispatch) => ({
161 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
162 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
163 | })
164 | })(DummyComp);
165 | const wrapper = mount(
166 |
167 |
168 |
169 | );
170 | let filterVal = wrapper.find('DummyComp').props().filter;
171 | t.deepEqual(filterVal, true);
172 | Store.dispatch({ type: 'GLOBAL_ACTION' });
173 | // State remains unchanged as the action is not forwarded
174 | t.deepEqual(wrapper.find('DummyComp').props().filter, true);
175 | wrapper.unmount();
176 | return new Promise((resolve) => setTimeout(() => resolve(), 10));
177 | });
178 |
179 | test(`Should not forward other actions besides those the component is tagged
180 | on to the component is filterGlobalActions returns false for the action`, t => {
181 | const Store = configureStore();
182 | const HOC = local({
183 | key: (props) => props.id,
184 | filterGlobalActions: (action) => {
185 | const allowedGlobalActions = ['SET_SORT'];
186 | return allowedGlobalActions.indexOf(action.type) !== -1;
187 | },
188 | createStore: (props) => {
189 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
190 | },
191 | mapDispatchToProps:(dispatch) => ({
192 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
193 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
194 | })
195 | });
196 | const CompToRender = HOC(DummyComp);
197 | const App = (props) => {
198 | return(
199 |
200 |
201 |
202 |
203 | );
204 | };
205 | const wrapper = mount(
206 |
207 |
208 | );
209 | let sortVal = wrapper.find('DummyComp').at(1).props().sort;
210 | t.deepEqual(sortVal, 'desc');
211 | wrapper.find('DummyComp').at(0).props().onSort('asc');
212 | // Intercepts all SET_SORT actions no matter where are originated
213 | const props = wrapper.find('DummyComp').at(1).props();
214 | sortVal = props.sort;
215 | t.deepEqual(sortVal, 'asc');
216 | t.deepEqual(props.trigger, 'comp1');
217 | t.deepEqual(props.current, 'comp2');
218 | wrapper.unmount();
219 | return new Promise((resolve) => setTimeout(() => resolve(), 10));
220 | });
221 |
222 | test(`Should be able to render multiple components of the same type
223 | and each should get it's own slice of state and react to it's own
224 | internal actions`, t => {
225 | const Store = configureStore();
226 | const HOC = local({
227 | key: (props) => props.id,
228 | filterGlobalActions: (action) => {
229 | const allowedGlobalActions = ['GLOBAL_ACTION', 'RESET_DEFAULT'];
230 | return allowedGlobalActions.indexOf(action.type) !== -1;
231 | },
232 | createStore: (props) => {
233 | return createStore(rootReducer, { filter: true, sort: props.sortOrder })
234 | },
235 | mapDispatchToProps:(dispatch) => ({
236 | onFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
237 | onSort: (sort) => dispatch({ type: 'SET_SORT', payload: sort }),
238 | })
239 | });
240 | const CompToRender = HOC(DummyComp);
241 | const App = (props) => {
242 | return(
243 |