Find me on medium.
When you're building out a react application, small projects can often be a little more flexible than large projects when it comes to code architecture. While there's nothing really wrong with building a small application with best practices intended for larger applications, it may be unnecessary to apply all the big decisions. The smaller the application is, the more it becomes "okay" to be lazy.
However, some of the best practices in this article are recommended to be applied with any sized react applications.
If you've never had experience building an application in production, then this article can help you prepare for the next large scaled application you build. The worst thing that could happen to you is building an application at your job and realizing that you have to refactor a lot of the code architecture to be more scalable and maintainable---especially if you're missing unit tests!
Trust me. I've been there. I was given several tasks to complete ____ by ____. At first, I thought everything was going smooth and perfect. I thought that just because my web application worked and still stayed fast that I was doing an excellent job in developing and maintaining my code. I knew how to use redux and make the UI components interact normally. Reducers and actions were an easy concept to me. I felt invincible.
Until The Future Creeped Up.
A couple months and 15+ features later, things were becoming out of control. My code utilizing redux was no longer easy to maintain.
"Why?" you may ask.
"Weren't you invincible?"
Well, I thought so too. It was a ticking time bomb waiting for a disaster to happen. Redux has the amazing ability to keep things maintainable if used correctly in a larger sized project.
Read along to find out what not to do if you're planning on building scalable react web applications.
1. Placing Actions and Constants Into One Place
You might see some redux tutorials out there placing constants and all of the actions into one place. However, it can quickly become a hassle as the app gets bigger. Constants should be in a separate location like ./src/constants
so that there is one place to search and not in multiple locations.
In addition, its definitely okay to create a separate actions file representing what or how it is going to be used with, encapsulating directly related actions. If you were building a new arcade/RPG game introducing a warrior, sorceress and archer class, it will be a lot more maintainable if you placed your actions like this:
src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js
Rather than something like:
src/actions/classes.js
If the app gets really big, its probably a better approach to go with something like this:
src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js
A bigger picture that includes other actions using that approach would then look like this if we were to separate them as demonstrated:
src/actions/warrior/skills.js
src/actions/warrior/quests.js
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js
src/actions/archer/equipping.js
An example of how the sorceress actions would look like:
src/actions/sorceress/skills
import { CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT } from '../constants/sorceress'
export const castFireTornado = (target) => ({
type: CAST_FIRE_TORNADO,
target,
})
export const castLightningBolt = (target) => ({
type: CAST_LIGHTNING_BOLT,
target,
})
src/actions/sorceress/equipping
import * as consts from '../constants/sorceress'
export const equipStaff = (staff, enhancements) => {...}
export const removeStaff = (staff) => {...}
export const upgradeStaff = (slot, enhancements) => {
return (dispatch, getState, { api }) => {
// Grab the slot in our equipment screen to grab the staff reference
const state = getState()
const currentEquipment = state.classes.sorceress.equipment.current
const staff = currentEquipment[slot]
const isMax = staff.level >= 9
if (isMax) {
return
}
dispatch({ type: consts.UPGRADING_STAFF, slot })
api.upgradeEquipment({
type: 'staff',
id: currentEquipment.id,
enhancements,
})
.then((newStaff) => {
dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff })
})
.catch((error) => {
dispatch({ type: consts.UPGRADE_STAFF_FAILED, error })
})
}
}
The reason why we do this is because there will always be new features to add, and you must prepare for them as your files become more bloated!
It may feel redundant in the beginning but these approaches will begin to shine the more the project gets larger.
2. Placing Reducers Into One Place
When my reducers start looking like this:
const equipmentReducers = (state, action) => {
switch (action.type) {
case consts.UPGRADING_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: action.slot,
},
},
},
}
case consts.UPGRADED_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
current: {
...state.classes.sorceress.equipment.current,
[action.slot]: action.staff,
},
},
},
},
}
case consts.UPGRADE_STAFF_FAILED:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
},
},
},
}
default:
return state
}
}
This can obviously create a big mess very quickly, so its best to keep your state structure simple and flattened as possible or try to compose all of your reducers instead.
A neat trick is to create a higher order reducer that generates reducers, mapping each wrapped reducer to an object mapping from action types to handlers.
3. Naming Your Variables Poorly
Naming your variables sounds like a simple no-brainer, but it can actually be one of the most difficult things to be good at when writing code.
It's essentially a clean coding practice... and the reason this term even exists is because its so important to apply in practice. Poorly naming your variables is a good way to let your team members and your future self suffer!.
Have you ever tried to edit someones code and end up having a hard time trying to understand what the code is trying to do? Have you ever run someone's code and it turned out to function differently than what you had expected?
I'm willing to bet that the author of the code was applying dirty code practices.
The worst situation to be in in this scenario is having to go through this in a large application where it's commonly happening in multiple areas.
Let me give you a real life experience of a situation that I was in:
I was editing an existing react hook from the app code when I received a task to add and show additional information about each doctor when a patient clicks on them. When they choose (click) on a doctor, the doctors information gets picked up from the table row so that they can attach the information onto the next request to the backend.
Everything was going fine, except I was unnecessarily spending more time than I should have when I was searching for where that part was in the code.
At this point in my head I was looking for words like info, dataToSend, dataObject, or anything relating to the data that was just gathered. 5-10 minutes later I found the part that implemented this flow, and the object it was placed in was named paymentObject
. When I think of payment objects, I think of CVV, last 4 digits, zip code, etc. Out of the 11 properties, only three were related to paying: charge method, payment profile id, and coupons.
And it didn't help that it was way too awkward trying to blend in my changes afterwards.
In short, try to refrain from naming your functions or variables like this:
import React from 'react'
class App extends React.Component {
state = { data: null }
// Notify what?
notify = () => {
if (this.props.user.loaded) {
if (this.props.user.profileIsReady) {
toast.alert(
'You are not approved. Please come back in 15 minutes or you will be deleted.',
{
position: 'bottom-right',
timeout: 15000,
},
)
}
}
}
render() {
return this.props.render({
...this.state,
notify: this.notify,
})
}
}
export default App
4. Changing The Data/Type Structure Mid-Way
One of the biggest mistakes I've ever made was to change the data/type structure of something during an already-established flow of the app. The new data structure would have been a huge boost in performance as it utilized object lookups to snatch data in memory instead of mapping over arrays. But it was too late.
Please don't do this unless you really know all of the areas that are going to be affected.
What are some of the consequences?
If something changes from an array to an object, multiple areas of the app are at risk of being unfunctional. I made the biggest mistake to think that I had every part of the app planned out in mind that would be affected by a structured data change, but there will always be that one spot left behind that was missed.
6. Developing Without Using Snippets
I used to be an Atom fan, but I switched to VScode because of how quick it was compared to Atom--while still supporting tons and tons of features without a noticeable loss of speed.
If you're using VSCode, I highly recommend you to download an extension called Project Snippets. This extension lets you declare custom snippets for each workspace for you to use for that project. It works exactly like the built in User Snippets feature that comes in vscode by default, except you create a .vscode/snippets/
folder inside your project like so:
7. Ignoring Unit/E2E/Integration Tests
As the app grows larger it becomes scarier to edit existing code without any sort of tests in place. You might end up editing a file located at src/x/y/z/ and decide to push the changes to production, however if the change affects another part of the app and you didn't notice, the bug will stay there until a real user catches it while they are browsing through your pages since you won't have any tests to alert you beforehand.
8. Skipping the Brainstorming Phase
Developers often skip the brainstorming phase because they aren't coding, especially when they're given a week to develop a feature. However, from experience this is the most important step and will save you and your team a lot of time in the future.
Why Bother Brainstorming?
The more complex an application, the more developers have to manage certain parts of the app. Brainstorming helps eliminate the amount of times you refactor code, because you already planned out what could go wrong. Often times developers are hardly given the time to sit back and apply all the neat practices to enhancing the app further.
This is why brainstorming is important. You think of all the code design in architecture and the enhancements you'd need so you can tackle them all from the start with a strategical approach. Don't fall into the habit of being overly confident and planning it all in your head. If you do, you won't be able to remember everything. Once you do something wrong, more things will go wrong like a domino effect.
Brainstorming will make it a little easier for your team as well. If one of them ever gets stuck on a task, they can refer to the brainstorming they had from the beginning and it's possibly already there.
The notes you take in brainstorming ideas can also serve you and your team as an agenda and help in easily providing a consistent sense of your current progress when developing the application.
9. Not Determining the UI Components Beforehand
If you're going to start building out your app, you should decide on how you want your app to look and feel. Several tools are available to help you with creating your own mockups.
A mockup tool I hear about often is Moqups. It's fast, does not require any plugins and is built in HTML5 and JavaScript.
Doing this step is very helpful in giving you both the information and data that is going to be on the pages you create. Developing your app will be much more of a breeze.
10. Not Planning The Data Flow
Almost every component of your application will be associated with some kind of data. Some will use its own source of data, but most of them will be provided from a location higher up in the tree. For parts of your application where data is shared with more than one component, it's a good idea to make that data available higher up in the tree where it will act as a centralized state tree. This is where the power of redux comes to the rescue :)
I advise to make a list of how the data is going to flow throughout your application. This will help you create a more firm mental and written models of your app. Based on these values, your reducer should easily be established from it.
11. Not Utilizing Accessor Functions
When the app gets bigger, so does the amount of components. And when the number of components increase, so does the number of times you use selectors (react-redux ^v7.1) or mapStateToProps. If you find your components or hooks often selecting state slices like useSelector((state) => state.app.user.profile.demographics.languages.main) in several parts of your application, it's time to start thinking about creating accessor functions in a shared location where the components/hooks can import and use from. These accessor functions can be filterers, parsers, or any other data transformation functions.
Here are some examples:
src/accessors
export const getMainLanguages = (state) =>
state.app.user.profile.demographics.languages.main
connect version
src/components/ViewUserLanguages
import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
export default connect((state) => ({
mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)
useSelector version
src/components/ViewUserLanguages
import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => {
const mainLanguages = useSelector(getMainLanguages)
return (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
}
export default ViewUserLanguages
It's also very important to keep these functions immutable--free of side effects. To find out why, click here.
12. Not Controlling The Flow In Props With Destructuring And Spread Attributes
What are the benefits in using props.something
versus something
?
Without destructuring
const Display = (props) => <div>{props.something}</div>
With destructuring
const Display = ({ something }) => <div>{something}</div>
With destructuring, you're not only making your code more readable for yourself and others but you're also making straight forward decisions on what goes in and what goes out. When other developers edit your code in the future, they don't have to scan through each line of code in your render method to find all of the props that the component is using.
You also benefit from the ability to declare a default props right from the beginning without having to add any more lines of code:
const Display = ({ something = 'apple' }) => <div>{something}</div>
You might have seen something like this before:
const Display = (props) => (
<Agenda {...props}>
{' '}
// forward other props to Agenda
<h2>Today is {props.date}</h2>
<hr />
<div>
<h3>Here your list of todos:</h3>
{props.children}
</div>
</Agenda>
)
This is not only a little bit harder to read, but there is also an unintentional bug occurring in this component. If App
also renders children, you have props.children being rendered twice. This causes duplicates. When working with a team of developers other than yourself there are chances that these mistakes can happen by accident especially if they aren't careful enough.
By destructuring props instead, the component can get straight to the point and decrease chances of unwanted bugs:
const Display = ({ children, date, ...props }) => (
<Agenda {...props}>
{' '}
// forward other props to Agenda
<h2>Today is {date}</h2>
<hr />
<div>
<h3>Here your list of todos:</h3>
{children}
</div>
</Agenda>
)
Conclusion
That is all, folks! I hope these tips helped you and shoot me a comment/message for any questions and/or concerns! See you next time!
Top comments (10)
In addition to tests I'd also recommend TrackJS, because it catches errors encountered in the wild.
Another thing that I've noticed people to do is to rename a thing, super minimal example:
I think the worst I've had to refactor had three or four different names, while not being the only thing changing it's name as data was passed on.
A third point I've noticed in larger apps is that people seem to develop their own solution each time they can't find something using keywords that pop into their mind. This is one of the reasons naming is important to spend time on. However this is also a communication issue.
Those points are right on! Miscommunication should have made it into this list. Miscommunication causes duplicate component implementations because it wasn't visible to the developer at the time. After the issue occurs sometimes we won't have enough time to merge them together because we're being pressured to finish another thing within the end of the day. This causes two separate implementations that practically do the same thing except a tiny change somewhere, like a different named prop for example.
Those of you reading this please make a mental note that communication with your teammates saves time and progress.
IMO another thing that's easy to do "wrong" is to abandon a core idea of Redux / Elm -- that your app has a single set of named behaviors drawn from a "menu" of action constants. In Elm, you're basically forced to stick to that architecture, but in Redux, the side effect libraries offer various escape hatches.
Once you drop in
redux-thunk
, you get another place to stick app logic. That logic can be composed of multiple actions firing, with time-dependent (can't think of a better way to say it) data being passed between them.With
redux-saga
, since the sagas are long-running, values living in the generators can act as implicit app state.The closest thing to Elm's ideal that enables side effects is
redux-loop
. While it may not be the best choice for every app, IMO it's generally good to be cautious around things like: logic moving out of named actions, unnamed "meta-actions," and app state that lives outside of the store.I'd argue that things like which files code lives in, or what variables get named, while good to get right, are less costly to fix than runtime behavior that works but is hard to reason about. Which is something that the combo of discrete, named actions & immutable state is meant to avoid.
Not sure why they're called accessor functions, the common name for them is selectors. The benefit of using selectors is that you can make selecting slices of state a lot easier. They are also composable for cases where you want to use multiple selectors to select multiple slices of state and combine, transform or filter them (e.g. for relational data).
Another benefit is that selectors can be memoized. Since they are (and should be) pure functions they'll always produce the same output from the same input. For example, if you have a selector that does a lot of work (mapping, filtering, reducing, etc.) and it is called often, memoization can be useful to avoid performing the slow operations each time for the same inputs.
These practices and benefits doesn't only apply to selectors but also React components themselves!
I highly recommend reading about this within the reduxjs/reselect GitHub repository.
Your welcome!
I think there are couple of reasons for choosing mapStateToProps over the useSelector in some conditions. Yes, Hooks like useSelector is powerful and it does have some benefits like memorization ,and using reselector can also avoid ‘accessory function’; however, if the component needs to touch base with lots of values in the state or more nested structure by the components. mapStateToProps in a separated file is much more organized and easier to maintain, because it will isolate the JSX components as UI as possible and also let test easier. It’s also depended on how the project structure is desgined at the first. If the components have a few state values should talk to or values are bond to components, which means they are cohesive, using useSelector is a good idea. It will drop some unneeded code. Nevertheless, we shouldn’t overuse the useSelector, for example adding tons of selectors in the component and make file gigantic or using same selector in multiple components.
Thanks for the article. I'm wondering what is the third argument inner function of Redux Thunk receives that you are destructing as
{ api }
on src/actions/sorceress/equipping example?yep, useSelector hook is much better than obsolete mapStateToProps
Can you elaborate more why ducks-modular-redux pattern is bad?