├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config
├── env.js
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── paths.js
├── polyfills.js
├── webpack.config.dev.js
├── webpack.config.prod.js
└── webpackDevServer.config.js
├── diagram.png
├── diagram.xml
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── scripts
├── build.js
├── start.js
└── test.js
├── src
├── App.css
├── App.js
├── App.test.js
├── UserCreator
│ ├── actions.js
│ ├── index.js
│ └── reducer.js
├── index.css
├── index.js
├── state
│ ├── app.js
│ └── users.js
└── store.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | .vscode
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8.4"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Riku Rouvila
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scaling a Redux app - reusable containers
2 |
3 |
4 |
5 | The idea here is to experiment with building Redux applications by reusing Redux applications. So basically a classic Yo Dawg situation. I'm doing this in the name of science and in the hope that future generations would have a more structured way of building user interfaces.
6 |
7 | This repository functions as a sandbox for testing different methods of cluing these Redux apps together. The way I see it, this could potentially lead into more scalable Redux codebases as you would now have a concept even greater than components or containers, that both are quite low-level. Also a really practical thing you could use this for would be a bit more complex reusable components like date pickers that have a state of their own, but have to use `this.setState`, component state & other funny stuff for managing the state.
8 |
9 | ### Structure of this repository
10 |
11 | Currently on the master branch, I have a component called [``](https://github.com/rikukissa/redux-isolated-apps/tree/master/src/UserCreator) that has the purpose of being a component/widget/fragment that the user would use for creating new users. Basically it's just an text input and a submit button.
12 |
13 | From the developer's point of view, I would want to be able to include this component anywhere in my app, without always having to manually connect to a store. So in the best case scenario, the only thing you would need is a `` tag.
14 |
15 | **Few design principles for ``:**
16 | - It should not know anything about being isolated
17 | - Its implementation should look exactly like normal container's
18 | - It communicates to outer world only via props
19 | - It can only access its own state + state given to it as props
20 | - It can be attached anywhere on the app without any additional setup
21 | - Middleware / enhancers used in its reducer / action creator implementation should be completely independent from app's
22 | - e.g. Using redux-saga as a part of UserCreator does not mean it should be also added to `src/store.js`
23 |
24 | I would also want it to be isolated in a way that it wouldn't share any state with other UserCreators in my app. In the future it will include a lot of logic (both async and sync), and for that reason I want to be able to leverage the action/action creator/reducer pattern Redux provides.
25 |
26 | I've written a few [tests](https://github.com/rikukissa/redux-isolated-apps/blob/master/src/App.test.js) to make these requirements a bit clearer, but unfortunately in the master branch some of them are failing. You can run them by running `yarn test` or `npm test`. All the relevant code for this endeavour lives under the `src` directory.
27 |
28 | **Leave an issue / a pull request if you know other ways of making Redux apps scalable or if you have other problems that you would like to solve.**
29 |
30 | ## Methods & libraries I've tried so far
31 |
32 | ### Nested stores
33 | 
34 | [Pull request](https://github.com/rikukissa/redux-isolated-apps/pull/2)
35 |
36 | I got this idea from [Redux's documentation](http://redux.js.org/docs/recipes/IsolatingSubapps.html). It's fairly simple to see what's going on just by looking at the code and I actually managed to get everything working with this method as well. At first my main concern was whether the tooling will play well with multiple stores, but it turns out that at least Redux DevTools lets you choose which store you want to observe.
37 |
38 | #### Pros
39 | - Minimalistic approach, easy to understand
40 | - No additional dependencies needed
41 | - Minimal code changes required when isolating pieces of UI
42 | - Easy integration with redux-loop, redux-forms etc
43 | - You can reuse the component dynamically anywhere without needing to explicitely define it in reducer
44 | - App store state remains a bit cleaner
45 |
46 | #### Cons/Challenges
47 | ##### Case 1.
48 | **Example case**
49 | > I want to notify the parent component about created user **after** it has been succesfully stored to our API
50 |
51 | ```js
52 |
53 | ```
54 |
55 | **The problem**
56 | > How do I call **this.props.onUserCreated** as a consequence of a dispatched **MY_NESTED/USER_SAVED_SUCCESS**?
57 |
58 |
59 | **Possible solutions**
60 | Since we are creating a nested store for our component, we might as well apply middlewares to that store. We could, for instance create a "bridge" between internal actions and prop callback calls. The underlying middleware would take care of calling these defined methods when ever a suitable action is dispatched.
61 |
62 | ```js
63 | // like mapStateToProps & mapDispatchToProps
64 | function actionsToProps(props) {
65 | return {
66 | /* dispatched action */ /* callback prop call */
67 | [CREATE_USER]: (state, action) => props.onUserCreated({ name: state.name })
68 | };
69 | }
70 | ```
71 |
72 | The thing that I don't like about this approach is, that you now have to expose action types to components / containers. It seems like a small thing that I would be willing to live with, but I have a feeling it might be a telltale sign of an architectural problem.
73 |
74 |
75 |
76 | ##### Case 2.
77 |
78 | **Example case**
79 | > I want to create a button for clearing all UserCreator inputs
80 |
81 | 🙃 This is where it gets even more complicated.
82 |
83 | **The problem**
84 | > How do I trigger **MY_NESTED/CLEAR** as a consequence of **TOP_LEVEL/CLEAR_ALL**?
85 |
86 |
87 | The only semi-clean way of doing it that I could come up with, would be to create a `top-level action -> nested store action` bridge. Something similar to what you saw above, but with
88 |
89 | ```js
90 | // like mapStateToProps & mapDispatchToProps & actionsToProps
91 | function actionsToActions() {
92 | return {
93 | /* top-level */ /* nested */
94 | [CLEAR_ALL]: CLEAR_INPUT
95 | };
96 | }
97 | ```
98 |
99 | However, it would still have the same problem with components knowing about actions. On top of that, as far as I know this impossible to achieve with a vanilla Redux store instance. This is because a Redux store doesn't provide a way of listening actions it receives. This is why we used a middleware to solve the previous problem. Now we can't really do that, because we are not no longer in control of the store which exists outside the scope of our UserCreator component 😔
100 |
101 | **Possible solutions**
102 | I wish I had one.
103 |
104 | ---
105 |
106 | ### [redux-subspace](https://github.com/ioof-holdings/redux-subspace)
107 | 
108 | [Pull request](https://github.com/rikukissa/redux-isolated-apps/pull/3)
109 |
110 | >After using this method in production for couple of months now, I already feel like some parts of the codebase become overly complicated. Most of it is because it's quite difficult to see just by looking at the code to which "subspace" the component / actions belong to. This can potentially be remedied by avoiding the usage of `globalAction` and coming up with some way of achieving your goal just by using component props. Other thing I would advice against is the usage of [wormholes](https://github.com/ioof-holdings/redux-subspace/blob/master/docs/advanced/GlobalState.md#wormholes). It's most likely better to pass the required data down as props.
111 | **Would still recommend this library and I'm keen to see how it evolves in the future**
112 |
113 | I bumped into this by accident while googling this subject. At first the documentation was a bit off-putting, but once I got desperate enough, I decided to give it a go. I definitely recommend checking it out, since it has been easily the best solution I've found so far. Once you start using it, you will find the documentation actually quite nicely structured.
114 |
115 | Besides just offering a solution to the problem I described above, I wanted to make sure it also works with other libraries we're often using in our apps. There are currently 2 different branches I made for this:
116 | -  [redux-loop](https://github.com/rikukissa/redux-isolated-apps/pull/4)
117 | -  [redux-form](https://github.com/rikukissa/redux-isolated-apps/pull/6)
118 |
119 | **Pros**:
120 | - Easy to use, well tested & documented
121 | - Integrates well with middlewares and store enhancers like redux-saga, redux-observable & redux-loop
122 |
123 | **Cons:**
124 | - Might hurt the readability of your code in the long run
125 | - Subspaced component's state becomes a part of your app's store state, which by default means, that you have to define a location for that explicitly in your reducer. This makes it a bit tricky to dynamically add new subspaced components. However, there is an additional library [redux-dynamic-reducer](https://github.com/ioof-holdings/redux-dynamic-reducer) for addressing this problem.
126 | - The reducer & action creators are dependant on the main app's middlewares / store enhancers. Using redux-thunk / redux-loop in the subspaced component's logic forces the app to also have them installed. This is not a problem with substores.
127 | - It didn't support redux-loop when I first started using it, but I managed to fix this by writing a wrapper function, that now can be found as the [`redux-subspace-loop`](https://github.com/ioof-holdings/redux-subspace/tree/master/packages/redux-subspace-loop) package on npm.
128 |
129 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
130 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 |
7 | // Make sure that including paths.js after env.js will read .env variables.
8 | delete require.cache[require.resolve('./paths')];
9 |
10 | const NODE_ENV = process.env.NODE_ENV;
11 | if (!NODE_ENV) {
12 | throw new Error(
13 | 'The NODE_ENV environment variable is required but was not specified.'
14 | );
15 | }
16 |
17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
18 | var dotenvFiles = [
19 | `${paths.dotenv}.${NODE_ENV}.local`,
20 | `${paths.dotenv}.${NODE_ENV}`,
21 | // Don't include `.env.local` for `test` environment
22 | // since normally you expect tests to produce the same
23 | // results for everyone
24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`,
25 | paths.dotenv,
26 | ].filter(Boolean);
27 |
28 | // Load environment variables from .env* files. Suppress warnings using silent
29 | // if this file is missing. dotenv will never modify any environment variables
30 | // that have already been set.
31 | // https://github.com/motdotla/dotenv
32 | dotenvFiles.forEach(dotenvFile => {
33 | if (fs.existsSync(dotenvFile)) {
34 | require('dotenv').config({
35 | path: dotenvFile,
36 | });
37 | }
38 | });
39 |
40 | // We support resolving modules according to `NODE_PATH`.
41 | // This lets you use absolute paths in imports inside large monorepos:
42 | // https://github.com/facebookincubator/create-react-app/issues/253.
43 | // It works similar to `NODE_PATH` in Node itself:
44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
48 | // We also resolve them to make sure all tools using them work consistently.
49 | const appDirectory = fs.realpathSync(process.cwd());
50 | process.env.NODE_PATH = (process.env.NODE_PATH || '')
51 | .split(path.delimiter)
52 | .filter(folder => folder && !path.isAbsolute(folder))
53 | .map(folder => path.resolve(appDirectory, folder))
54 | .join(path.delimiter);
55 |
56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
57 | // injected into the application via DefinePlugin in Webpack configuration.
58 | const REACT_APP = /^REACT_APP_/i;
59 |
60 | function getClientEnvironment(publicUrl) {
61 | const raw = Object.keys(process.env)
62 | .filter(key => REACT_APP.test(key))
63 | .reduce(
64 | (env, key) => {
65 | env[key] = process.env[key];
66 | return env;
67 | },
68 | {
69 | // Useful for determining whether we’re running in production mode.
70 | // Most importantly, it switches React into the correct mode.
71 | NODE_ENV: process.env.NODE_ENV || 'development',
72 | // Useful for resolving the correct path to static assets in `public`.
73 | // For example,
.
74 | // This should only be used as an escape hatch. Normally you would put
75 | // images into the `src` and `import` them in code to get their paths.
76 | PUBLIC_URL: publicUrl,
77 | }
78 | );
79 | // Stringify all values so we can feed into Webpack DefinePlugin
80 | const stringified = {
81 | 'process.env': Object.keys(raw).reduce((env, key) => {
82 | env[key] = JSON.stringify(raw[key]);
83 | return env;
84 | }, {}),
85 | };
86 |
87 | return { raw, stringified };
88 | }
89 |
90 | module.exports = getClientEnvironment;
91 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | // This is a custom Jest transformer turning file imports into filenames.
6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
7 |
8 | module.exports = {
9 | process(src, filename) {
10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`;
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(path, needsSlash) {
15 | const hasSlash = path.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return path.substr(path, path.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${path}/`;
20 | } else {
21 | return path;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right