The question:
What is the most maintainable and recommended best practice for organising containers, components, actions and reducers in a large
React/Redux application?
My opinion:
Current trends seem to organise redux collaterals (actions, reducers, sagas...) around the associated container component. e.g.
/src
/components
/...
/contianers
/BookList
actions.js
constants.js
reducer.js
selectors.js
sagas.js
index.js
/BookSingle
actions.js
constants.js
reducer.js
selectors.js
sagas.js
index.js
app.js
routes.js
This works great! Although there seems to be a couple of issues with this design.
The Issues:
When we need to access actions, selectors or sagas from another container it seems an anti-pattern. Let's say we have a global /App container with a reducer/state that stores information we use over the entire app such as categories and enumerables. Following on from the example above, with a state tree:
{
app: {
taxonomies: {
genres: [genre, genre, genre],
year: [year, year, year],
subject: [subject,subject,subject],
}
}
books: {
entities: {
books: [book, book, book, book],
chapters: [chapter, chapter, chapter],
authors: [author,author,author],
}
},
book: {
entities: {
book: book,
chapters: [chapter, chapter, chapter],
author: author,
}
},
}
If we want to use a selector from the /App container within our /BookList container we need to either recreate it in /BookList/selectors.js (surely wrong?) OR import it from /App/selectors (will it always be the EXACT same selector..? no.). Both these appraoches seem sub-optimal to me.
The prime example of this use case is Authentication (ah... auth we do love to hate you) as it is a VERY common "side-effect" model. We often need to access /Auth sagas, actions and selectors all over the app. We may have the containers /PasswordRecover, /PasswordReset, /Login, /Signup .... Actually in our app our /Auth contianer has no actual component at all!
/src
/contianers
/Auth
actions.js
constants.js
reducer.js
selectors.js
sagas.js
Simply containing all the Redux collaterals for the various and often un-related auth containers mentioned above.
I personally use the ducks-modular-redux proposal.
It's not the "official" recommended way but it works great for me. Each "duck" contains a actionTypes.js, actionCreators.js, reducers.js, sagas.js and selectors.js files. There is no dependency to other ducks in these files to avoid cyclic dependency or duck circle, each "duck" contains only the logic that it have to managed.
Then, at the root I have a components and a containers folders and some root files :
components/ folder contains all the pure components of my app
containers/ folder contains containers created from pure components above. When a container need a specific selector involving many "ducks", I write it in the same file where I wrote the <Container/> component since it is relative to this specific container. If the selector is shared accros multiple containers, I create it in a separate file (or in a HoC that provides these props).
rootReducers.js : simply exposes the root reducers by combining all reducers
rootSelectors.js exposes the root selector for each slice of state, for example in your case you could have something like :
/* let's consider this state shape
state = {
books: {
items: { // id ordered book items
...
}
},
taxonomies: {
items: { // id ordered taxonomy items
...
}
}
}
*/
export const getBooksRoot = (state) => state.books
export const getTaxonomiesRoot = (state) => state.taxonomies
It let us "hide" the state shape inside each ducks selectors.js file. Since each selector receive the whole state inside your ducks you simply have to import the corresponding rootSelector inside your selector.js files.
rootSagas.js compose all the sagas inside your ducks and manage complex flow involving many "ducks".
So in your case, the structure could be :
components/
containers/
ducks/
Books/
actionTypes.js
actionCreators.js
reducers.js
selectors.js
sagas.js
Taxonomies/
actionTypes.js
actionCreators.js
reducers.js
selectors.js
sagas.js
rootSelectors.js
rootReducers.js
rootSagas.js
When my "ducks" are small enough, I often skip the folder creation and directly write a ducks/Books.js or a ducks/Taxonomies.js file with all these 5 files (actionTypes.js, actionCreators.js, reducers.js, selectors.js, sagas.js) merged together.
Related
For example, the recommended way of importing in React Bootstrap is to go this way:
import Button from 'react-bootstrap/Button' instead of import { Button } from 'react-bootstrap';
The reason is "Doing so pulls in only the specific components that you use, which can significantly reduce the amount of code you end up sending to the client."
source: https://react-bootstrap.github.io/getting-started/introduction/
Same for React MUI components:
import Button from '#mui/material/Button';
source: https://mui.com/material-ui/getting-started/usage/
I want to implement something similar in my React components library, to limit the usage of code in the bundle, but I don't know how they implement this specific pattern. I have looked at their code base, but I don't quite understand.
Basically it is all about modules and module files and their organization. You can have a lot of.. lets call them folders, "compoments/*" for example. "components/button", "components/alert", "component/badge", and other things. All of them will have some index.js or .ts file that will export or declare and export all the functionality that needed in order to make this component work, 'react-bootstrap/Button' for example. Ideally all those subfolders or submodules are independend from each other, no references between them but probably each one will have 1 reference to 1 common/shared submodule like "components/common" which will contain some constants, for example, and no references to other files. At the top level of them you will have another index.js or .ts file that is referencing all of those components, so "components/index.js" will import and reexport all the nested components index files. So in order to import a Button, for example, you can either import "components/index.js" file with all the other imports this file is using, either only 1 single "components/button/index.js" file which is obviously much more easy to fetch. Just imagine a tree data structure, you import root of the tree (root index.js) - you get all the tree nodes. You import one specific Node (components/button/index.js) of the tree - just load all the childs (imports) of that node.
Sorry for a long read but asuming you mentioned webpack - there is a technique called tree-shaking which will cut off all the unused things.
Info about modules: https://www.w3schools.com/js/js_modules.asp
Info about Tree-Shaking: https://webpack.js.org/guides/tree-shaking/
It might not be as complicated as you think. Let's say you write the following library:
// your-library.js
const A = 22
const B = 33
export function getA () { return A }
export function getB () { return B }
export function APlusB () { return A + B }
// a lot of other stuff here
If some consumer of your library wants to make use of the APlusB function, they must do the following:
// their-website.js
import { APlusB } from 'your-library'
const C = APlusB()
However, depending on how the code is bundled, they may or may not wind up with the entire your-library file in their web bundle. Modern bundling tools like Webpack may provide tree shaking to eliminate dead code, but this should be considered an additional optimization that the API consumer can opt into rather than a core behavior of the import spec.
To make your library more flexible, you can split up independent functions or chunks of functionality into their own files while still providing a full bundle for users who prefer that option. For example:
// your-library/constants.js
export const A = 22
export const B = 33
// your-library/aplusb.js
import { A, B } from 'constants'
export default function APlusB () { return A + B }
// your-library/index.js
// instead of declaring everything in one file, export it from each module
export * from 'constants'
export { default as APlusB } from 'aplusb'
// more exports here
For distribution purposes you can package your library like so:
your-library
|__aplusb.js
|__constants.js
|__index.js
You mentioned react-bootstrap and you can see this exact pattern in their file structure:
https://github.com/react-bootstrap/react-bootstrap/tree/master/src
and you can see they aggregate and re-export modules in their index file here:
https://github.com/react-bootstrap/react-bootstrap/blob/master/src/index.tsx
Essentially, what you are asking is:
"How to export react components"
OR
"How are react components exported to be able to use it in a different react project ?"
Now coming to your actual question:
import Button from 'react-bootstrap/Button' instead of import { Button } from 'react-bootstrap';
The reason is 'Button' component is the default export of that file react-bootstrap/Button.tsx. So there is no need for destructuring a specific component.
If you export multiple components/ functions out of a file, only 1 of them can be a default export.
If you have only 1 export in a file you can make it the default export.
Consider the file project/elements.js
export default function Button(){
// Implementation of custom button component
}
export function Link(){
// Implementation of custom Link component
}
function Image(){
// Implementation of custom Image component
}
Notice that the Button component has 'default' as a keyword and the Link component doesn't.
The Image component can't even be imported and can only be used by other functions/components in the same file.
Now in project/index.js
import 'Button', {Link} from './elements.js'
As Button component is the default export its possible to import without destructuring and as Link component is a regular export, I have to destructure it for importing.
Structure of the project generally looks like this:
components
- my-component
- - my-component.ts
- - index.ts
Where index.(ts/js) always consists of
import MyComponent from './my-component';
export default MyComponent;
I want to remove index.(js/ts) and still import/require my components using the path
import MyComponent from './components/my-component' // not having the index file!
// Please, do not suggest importing them like this
import MyComponent from './components/my-component/my-component'
I use Webpack 5. I know there was a module for Webpack 4, but it does not work with 5.
I want NodeJS/Webpack to look for a custom filename rather than index.
Some other solutions:
You could move components/my-component/my-component.ts to components/my-component.ts. I assume you have a reason not to do this (presumably, other files in that component directory).
You could use resolve.alias and list all mappings you want:
module.exports = {
//...
resolve: {
alias: {
"./components/my-component": path.resolve(__dirname, 'components/my-component/my-component.js'),
//...
},
},
};
You could set resolve.mainFiles to list all of your .js filenames. This seems pretty ugly though, as it's a global setting.
Are you sure that Webpack 5 doesn't support DirectoryNamedWebpackPlugin? It's explicitly mentioned in the documentation.
I m wondering if there is a performance cost if we make multiple imports, like so:
import { wrapper } from './components/wrapper';
import { error } from './components/error';
import { products } from './components/products';
In each component folder i have an index.js and export it as named, like so:
export { default as wrapper } from '.wrapper';
Compared to:
Import all the files as named imports from the same source, like so:
import {
wrapper,
error,
products,
} from './components';
In components folder i have an index where i gather and export all the files, like so:
export { wrapper } from '...';
export { error } from '...';
export { products } from '...';
According to the ES262 specification, import and export statements just provide information about dependencies between modules to the engine. How the modules are actually loaded in the end is up to the engine (there are a few constraints though). So whether there is actually a difference between importing from the source vs. importing a reexport depends on the environment.
Whatsoever the differences are probably irrelevant. Choose what works best for you.
I'm a fan of that approach. I like to split some components into folder and only expose what I want to the rest of my application. I really don't think that impact the perf on dev. (Obviously, there is absolutely no difference on prod as the whole project is pack in one file)
I am about to have 400+ models for use with js-data in my angular2 (angular-cli) app.
my project's structure is this:
- src/
- app/
- services/
- pipes/
- ui/
- data/
- store.ts
- models/
- model1.ts
- model2.ts
- ...
- model400.ts
In the store, I need to import, and add the mapping to the store.
The model files are actually just mapper configs for js-data 3.
currently, they look something like this:
// src/app/data/models/model1.ts
export default {
schema: {
name: 'model1',
properties: {
id: { type: 'integer' }
}
},
relations: {}
}
and my store currently looks like this:
// src/app/data/store.ts
import {
DataStore,
Mapper,
Record,
Schema,
utils
} from 'js-data'
import {HttpAdapter} from 'js-data-http'
declare var require: any
export const adapter = new HttpAdapter({
// Our API sits behind the /api path
basePath: '/api'
});
export const store = new DataStore({});
store.registerAdapter('http', adapter, { default: true });
import { model1Config} from './models/model1';
import { model2Config } from './models/model2';
import { model3Config } from './models/model3';
// at this point, I give up, cause this is more tedious
// than cutting grass with a finger nail clipper
store.defineMapper('model1', model1Config);
store.defineMapper('model2', model2Config);
store.defineMapper('model3', model3Config);
If there is anyway to iterate over every file in the models folder, that would be great.
angular-cli is supposed to eventually compile all the ts/js to a single js file, so I don't need to worry about anything that couldn't run on the client side.
(so, I have broccoli, and whatever other build tools are bundled with that, I just don't know if any of them would be useful to me for this situation)
You could use an index file, which you can use for your imports. for example in your models folder an index file which just exports every model for you like this:
// ...../models/index.ts
export * from './models/model1';
export * from './models/model2';
then in your other files you can import them like this:
import {model1Config, model2Config, model3Config } from "path/to/models/index";
...
You have to define the exports somewhere. Using a file which functions as a "export collection" saves you at least a lot lines of code (and a lot of time if you're using a good IDE).
Setting up the the index with your x-hundreds of models still is tedious. Maybe a little script with gulp could help.
I'm building an application, where I need to preload people and planet data (it's likely that in the future more preload requirements may be added) on launch of the application. I want to have value in the store that represents the global state of the app as loaded: <boolean>. The value would be true only then when the preload requirements people.loaded: true and planet.loaded: true are true. The store would look something like this:
Store
├── loaded: <Boolean>
├── people:
│ ├── loaded: <Boolean>
│ └── items: []
├── planets:
│ ├── loaded: <Boolean>
│ └── items: []
Separate action creators make the needed async requests and dispatch actions which are handled by the People and Planets reducers. As shown below (uses redux-thunk):
actions/index.js
import * as types from '../constants/action-types';
import {getPeople, getPlanets} from '../util/swapi';
export function loadPeople () {
return (dispatch) => {
return getPeople()
.then((people) => dispatch(addPeople(people)));
};
}
export function loadPlanets () {
return (dispatch) => {
return getPlanets()
.then((planets) => dispatch(addPeople(planets)));
};
}
export function addPeople (people) {
return {type: types.ADD_PEOPLE, people};
}
export function addPlanets (planets) {
return {type: types.ADD_PLANETS, planets};
}
export function initApp () {
return (dispatch) => {
loadPeople()(dispatch);
loadPlanets()(dispatch);
};
}
../util/swapi handles fetching people and planet data either from LocalStorage or making a request.
initApp() action creator calls other action creators within site.js just before rendering to DOM as shown below:
site.js
import React from 'react';
import {render} from 'react-dom';
import Root from './containers/root';
import configureStore from './store/configure-store';
import {initApp} from './actions';
const store = configureStore();
// preload data
store.dispatch(initApp());
render(
<Root store={store} />,
document.querySelector('#root')
);
1. What are the best practices for managing global preload state of the application in Redux?
2. Is having a global loaded state in the store necessary?
3. What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.
Quoting part of Dan's answer from Redux - multiple stores, why not? question.
Using reducer composition makes it easy to implement "dependent updates" a la waitFor in Flux by writing a reducer manually calling other reducers with additional information and in a specific order.
4. Does Dan by calling other reducers mean calling nested reducers?
First, let me correct your example.
Instead of
export function initApp () {
return (dispatch) => {
loadPeople()(dispatch);
loadPlanets()(dispatch);
};
}
you can (and should) write
export function initApp () {
return (dispatch) => {
dispatch(loadPeople());
dispatch(loadPlanets());
};
}
You don’t need to pass dispatch as an argument—thunk middleware takes care of this.
Of course technically your code is valid, but I think my suggestion reads easier.
What are the best practices for managing global preload state of the application in Redux?
What you’re doing seems correct. There are no specific best practices.
Is having a global loaded state in the store necessary?
No. As David notes in his answer, you’re better off storing only necessary state.
What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.
If you’re concerned about duplication, create a “selector” function and place it alongside your reducers:
// Reducer is the default export
export default combineReducers({
planets,
people
});
// Selectors are named exports
export function isAllLoaded(state) {
return state.people.loaded && state.planets.loaded;
}
Now you can import selectors from your components and use them in mapStateToProps function inside any component:
import { isAllLoaded } from '../reducers';
function mapStateToProps(state) {
return {
loaded: isAllLoaded(state),
people: state.people.items,
planets: state.planet.items
};
}
Does Dan by calling other reducers mean calling nested reducers?
Yes, reducer composition usually means calling nested reducers.
Please refer to my free Egghead tutorial videos on this topic:
Reducer Composition with Arrays
Reducer Composition with Objects
Reducer Composition with combineReducers()
Implementing combineReducers() from Scratch
A loaded flag in your store state makes perfect sense.
However you've got too many of them. You've got state.people.loaded, state.planets.loaded as well as state.loaded. The latter is derived from the first two. Your store really shouldn't contain derived state. Either have just the first two or just the latter.
My recommendation would be to keep the first two, i.e. state.people.loaded and state.planets.loaded. Then your connected component can derive an ultimate loaded state. e.g.
function mapStateToProps(state) {
return {
loaded: state.people.loaded && state.planets.loaded,
people: state.people.items,
planets: state.planet.items
};
}