A bunch of ideas for developers working on large production apps.
Average App Anatomy
To reach the widest possible audience I will use a fairly common setup for the demonstration. Our average app ...
- has a static landing page with some marketing pitch.
- has some public pages, at least a login and a register.
- has a handful of private pages.
- uses JWT token for authentication.
- is written in React with redux, react-router and axios.
- is bootstrapped with create-react-app.
I work at a consulting company and this is what comes around most often. Hopefully you can apply the below ideas to your preferred stack too.
Tip #1: Have a solid API layer
The api should handle everything networking related.
Avoid duplicating URLs and headers, use a base API instance instead.
Handle authentication here. Make sure to add the auth token to both
localStorage
and the base API instance.Use API interceptors for generic fallback behaviors - like global loading indicators and error notifications.
import axios from 'axios'
import store from '../store'
import { startLoading, stopLoading, notify } from '../actions'
const JWT_TOKEN = 'JWT_TOKEN'
// have a base api instance to avoid repeating common config - like the base URL
// https://github.com/axios/axios#custom-instance-defaults
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: process.env.REACT_APP_API_TIMEOUT
})
// add the Auth header to the base API instance once here to avoid repeated code
if (localStorage.getItem(JWT_TOKEN)) {
const token = localStorage.getItem(JWT_TOKEN)
api.defaults.headers.Authorization = `Bearer ${token}`
}
// keep networking logic - like handling headers and tokens - in the network layer
export function login (token) {
api.defaults.headers.Authorization = `Bearer ${token}`
localStorage.setItem(JWT_TOKEN, token)
}
export function logout () {
delete api.defaults.headers.Authorization
localStorage.removeItem(JWT_TOKEN)
}
// handle generic events - like loading and 500 type errors - in API interceptors
api.interceptors.request.use(config => {
// display a single subtle loader on the top of the page when there is networking in progress
// avoid multiple loaders, use placeholders or consistent updates instead
store.dispatch(startLoading())
return config
})
api.interceptors.response.use(
resp => {
store.dispatch(stopLoading())
return resp
},
err => {
store.dispatch(stopLoading())
// if you have no specific plan B for errors, let them be handled here with a notification
const { data, status } = err.response
if (500 < status) {
const message = data.message || 'Ooops, something bad happened.'
store.dispatch(notify({ message, color: 'danger' }))
}
throw err
}
)
export default api
Tip #2: Keep the State Simple
Since loading and generic error handling is already covered by the API, you won't need to use full-blown async actions. In most cases, it is enough to cover the success event.
action.js
import articlesApi from '../api/articles'
const LIST_ARTICLES = 'LIST_ARTICLES'
export function listArticles () {
return async dispatch => {
// no need to handle LIST_ARTICLES_INIT and LIST_ARTICLES_ERROR here
const articles = await articlesApi.list()
dispatch({ type: LIST_ARTICLES, articles })
}
}
reducer.js
import { LIST_ARTICLES } from '../actions/articles'
export function articles (state = [], { type, articles }) {
switch (type) {
case LIST_ARTICLES:
return articles
default:
return state
}
}
You should only handle init and error events when you have a specific plan B.
Tip #3: Keep Routing Simple
Implementing a correct ProtectedRoute
component is tricky. Keep two separate router trees for public and protected pages instead. Login and logout events will automatically switch between the trees and redirect to the correct page when necessary.
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
// isLoggedIn is coming from the redux store
export default App ({ isLoggedIn }) {
// render the private routes when the user is logged in
if (isLoggedIn) {
return (
<Switch>
<Route exact path="/home" component={HomePage} />
<Route exact path="/article/:id" component={ArticlePage} />
<Route exact path="/error" component={ErrorPage} />
<Redirect exact from="/" to="/home" />
<Route component={NotFoundPage} />
</Switch>
)
}
// render the public router when the user is not logged in
return (
<Switch>
<Route exact path="/login" component={LoginPage} />
<Route exact path="/register" component={RegisterPage} />
<Redirect to="/login" />
</Switch>
)
}
The above pattern has a well-behaved UX. It is not adding history entries on login and logout, which is what the user expects.
Tip #4: Init the App Properly
Do not render anything until you know if the user is logged in or out. Making a bold guess could result in a short flicker of public/private pages before redirecting to the correct page.
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './store'
// do not render the app until we know if there is a logged in user or not
store.dispatch(getMe()).then(renderApp)
function renderApp () {
ReactDOM.render(<App />, document.getElementById('root'))
}
getMe()
should call the /me
endpoint which returns the logged in user or a 401 (Unauthorized) error code. Checking for a JWT token in the localStorage is not enough, the token might be expired which could result in an infinite redirect loop for the user.
export function getMe (data) {
return async dispatch => {
try {
const user = await userApi.getMe(data)
dispatch({ type: LOGIN, user })
} catch (err) {
userApi.logout()
}
}
}
Tip 5: Use the Landing Page
Returning users will already have some interest in your product and a cached app in their browser. Newcomers won't, and they will judge quickly.
Server Side Rendering your whole app can give a nice first impression, but it is one of the toughest techs out there. Don't jump on that train just yet. Most of the time you can rely on a simple heuristic instead: newcomers will most likely start on your landing page.
Just keep your landing page simple, static and separate from your app. Then use preload or HTTP/2 push to load your main app while the user is reading the landing page. The choice between the two is use-case specific: go for prefetch if you have a single big bundle and go for HTTP/2 push in case of multiple small dynamically named chunks.
I hope I could teach a few new tricks! If you got so far, please help by sharing the article. I may create a second one about creating reusable components if this one gets enough love.
Thanks!
Top comments (21)
To this point, I'd actually advocate for going frameworkless and static on a landing page if possible. It's usually not a page with complex state/logic and performance and needling over pixels is what is important.
Server side rendering is actually simple if you strip away modern JS and write HTML.
Here is a great article on the tradeoffs:
developers.google.com/web/updates/...
Static Rendering is probably what you are referring to, server-side rendering implies some munging of data to render the final HTML and send it to the browser.
You are right and I wanted to suggest exactly what you commented. "Keep your landing simple and static (Server Side Rendered)". I was just carried away with my chosen stack and I incorrectly meant "Server Side Rendering your whole ReactJS app" by "Server Side Rendering". I will make a slight correction, thanks for this comment.
It's hard to maintain (react + react router + redux ) in the front and an APi in the backend.
For me, coding smart is use the old mvc pattern for the big shape and only react-like for some specialized components where we need a snapy UI
You know something, I’ve been thinking the same thing. After doing a few PWAs and realizing that it is indeed much more complicated, more parts, more potential problems, the old fashioned MVC is looking quite simple.
But it does depend on the situation though.
Yeah !, also the team's size should drive our stack, if we are small team, we should make it simple (simple to code, simple to test, simple to maintain).
A very nice video from Stephenson explaining very well this point youtube.com/watch?v=SWEts0rlezA
I am sorry to @Miklos Bertalan, all my comments are more related to the title of your port than the content ˆ, btw great tips :)
Don't think I agree, the traditional web programming model (server side generated pages, no separation between backend and frontend) had a lot of drawbacks (look at the mess that you can easily get when you start to use AJAX, jQuery and so on).
The new "API + SPA" model (which is quickly becoming the norm or default when developing apps) has a much more simple and logical mental model, and a more sound architecture (by separating backend and frontend).
I do agree that with the API/SPA model you sometimes have more "boilerplate", but it's a programming model which 'scales', and which allows you to build more advanced apps/UIs, while keeping things structured and manageable.
What tools do you use for the MVC pattern? Just plain DOM and JS?
I use rails for the regular parts like loging and forms, react for isolated components when I need real-time or a intensive Ui.
To test everything, I use system test, to test my react components and it's interactions with rails.
I think it's a nice spot, This make me a lot more productive.
Fantastic article, some of the best and most practical advice I've read in a while, this is stuff that removes a lot of complexity and makes the simple things easy. Difficult stuff can be hard, simple things should be (and can be) easy but often we're wasting a lot of time by complicating the simple things.
I'm struggling with the security aspect of keeping a JWT in localStorage. that's potentially the same as keeping the whole user session in a non-http cookie, for any js script to read.
I see most apps keep their JWT there, yet it makes no sense 🤭 (At the same time I do see the benefits and sexiness of a static SPA)
This is a good point and it keeps coming back. There are some heated discussions about this and most articles are pretty one sided with a few slight half-truths against the other side.
I suggest you to read this article against local storage first.
I can't find a similarly good article for the pro-localstorage side but here are a few points to consider:
While LocalStorage is vulnerable against XSS, cookies are vulnerable aganst CSRF. Both are equally bad security issues and both can be mitigated (to some extent at least).
Server set httpOnly cookies are good for simple one server -> one client apps. When you need to open up your API you should not keep the session in cookies anymore. In these cases you should issues tokens to the clients and let them store them however they want to (DB on the server side or localStorage on client side). This is what oauth does as an example.
I am personally okay with both approaches and I try to secure things either way but I am not a security expert. It would be nice to hear the opinion of someone more experienced.
Keeping a JWT in localStorage doesn't just open you up to XSS, it also opens you up to session hijacking (a much more severe vulnerability imo). If someone/-thing grabs that token, then it has free access to your API for X amount of time. And since detecting that a 3rd party's or extension's script is reading something from your localStorage is impossible, then they can simply wait for the user to renew their session and attack again.
Way way way too complicated. Life is much simpler on the Web.
I would say that if you want to code smart, code with the real web-languages. I'm sorry but what you proposed is more coding hard than anything else, but this is only my honest opinion. I do understand the popularity of using such complex frameworks.
Still, great article!
What are "real web languages", PHP, Python, "anything but" Javascript?
In my experience, once you have a good base setup (see the tips in this article) building an app with the API + SPA model (e.g. with React) can be smooth and simple.
As soon as you require a bit more interactivity on the frontend and a more advanced UI then the 'old' model with server side generated pages, AJAX and jQuery quickly becomes cumbersome.
Great article, good advices. Bookmarked.
I am realllly advocating hard for "Tip #4: Init the App Properly"
Very nicely written, straight to the point, and solid advises. These advises hold regardless of framework
totally true. I'm going to check this boxes now on our vue app :)
Kudos! Really great artice
Nice infos, it also carries over to the other frameworks of course. 👍