Lessons Learned Building in Next.js
Part 1: Introduction and Initial Gotchas
Last Updated: 5/9/2017
- React: v15.4
- next.js: v2.3
next.js is a server-side rendering framework for React apps (and it’s pretty fast, too)! It also offers a slightly more opinionated approach to building your React apps by asking you to stick to SOME slight conventions regarding your directory structure and how you structure your components. While this may sound scary at first, this actually ends up being a big help to get your next.js application structured a little bit more soundly.
So how does it differ from create-react-app? In my mind, it is best described via a bit of a Venn diagram. There’s some overlap and some areas where one surpasses (or at least focuses more) the other. create-react-app feels like it has a slightly nicer developer experience: a lot of the testing setup is already ready to go and doesn’t require much tinkering. In addition, you can add redux to it or do dynamic routes with react-router with no extra effort, which is definitely a pretty nice bonus. However, if you want to do any neat stuff with server-side rendering, you’re on your own, and changing the pipelines to include SASS or other post-css-style plugins get a lot rougher. next.js is more on the spectrum of giving you a good framework, a good foundation, and getting your app a little more love on the server side of things.
Before I go any further, however, there is one thing I’d like to note: this is a deviation from my usual style of posts that focus deeply on tutorial-style lessons. This is instead a little bit more geared towards the things I learned in the process of converting a side project from create-react-app into next.js. Depending on the various levels of interest I may venture I a little more into a tutorial covering more bits of this approach (as well as the API side of things via Elixir/Phoenix).
As of a few days ago, I shipped my first approach to a next.js application, and one that I feel will be a precursor for others as well, so the tl;dr for this post is that next.js is pretty good, and I’m pretty happy with it overall! That being said, I ran into some issues during the process of converting my full create-react-app over, and I thought it might be nice to document them out should someone else run into similar scenarios!
Also, before I proceed any further, I want to give a huge shout-out to the create-react-app team and Zeit/the next.js team. These are some fantastic tools that significantly increase the quality of life for all of us! Thanks for all the hard work you’ve all poured into these products, and please know that we appreciate and respect you and all that you’ve done! You all rock!
So one thing that is worth mentioning before proceeding at all is that you can’t just straight up copy and paste your create-react-app code into next.js and hope for the best. There aren’t a ton of differences between the two but there enough that you can’t do a straight-up 1:1 translation.
The first little gotcha (or at least, the first thing I needed to acclimate to) was next.js’ intended project structure. At a very minimum level, you should be splitting your app apart into two separate directories: pages and components.
pages are what you’d think of as the actual pages that your users can navigate to, and should be composed from components inside of your components directory. This also informs next.js about the URL and routing structure of your application through the names of files. So, if under pages, you had a few files: index.js, store.js, and about.js, next.js would assume your route structure was, by default, [yoursite] (the index.js acts as the index or base route for your app), [yoursite]/store, and [yoursite]/about. These are really the only major decisions that next.js will inform about your project structure; beyond this it is dealer’s choice.
Uh, this thing is sort of a screen, so I’ll create a screens/ directory, and this thing is a container, so I’ll create containers/, and this is stuff related to the Redux store, so I’ll create store/…
This gets very unsustainable very quickly. Trust me. If you’re looking for an alternative structure, you can also check out Redux ducks!
I personally structured my application into these directories:
- pages — The top-level components and routes for my app
- components — The smaller components used to build the larger pages
- services — Any of the library/utility JS functions that provided the layer to communicate with my API
- __tests__ — This is more Jest-specific, but this is where my tests went for my app. From there, the directory structure mirrored the root of my next.js project, so I had: __tests__/pages, __tests__/components, etc.
This is more in-depth than what you’ll get or have suggested as part of building an app in create-react-app, so it took a little bit of restructuring of my original app to make it all work. That being said, I like it; it forced me to rethink my structure a bit and focus more composability.
Redux was a slightly more interesting case and one that originally caused me to drop my excursions into converting my app from create-react-app to next.js. Specifically, I had a number of container components that embedded Redux via react-redux’s connect function.
next.js has a slightly different idea. Instead of using connect(mapStateToProps, mapDispatchToProps)(Component), you can instead use a library (next-redux-wrapper) that provides a withRedux(store, mapStateToProps, mapDispatchToProps)(Component) function instead. Instead of picking up the store through context and higher-order components like Provider, you’re using a function and hooking the store up to each page individually. The idea is not to bundle the redux store and functionality for your components, as this again encourages bad design patterns and bad habits.
This component needs something from the store, but it’s a lot of work to pass that property down from the parent here, so screw it, I’ll just hook it up to the store with connect!
Yeah, don’t do this. You can get into some wacky scenarios and you’re making your tests harder to write (and your life more difficult in the process)!
My original concern with this approach was two-fold.
I didn’t want to get locked into framework-specific knowledge that wouldn’t be applicable elsewhere. This is still a concern of mine, for what it’s worth, but I decided that the things I had to learn to make my app work weren’t significant enough departures from standard React knowledge that I’d be “specializing” in a single framework.
I’d rather be a React developer than a next.js developer!
My app was built in a pretty inefficient way that originally had me attempting to jump through hoops to still be able to use withRedux with smaller components (not full pages). Again, I was clinging to the original way I was attempting to architect my app, which was not necessarily the best option. I’ve instead modified my components and pages to pass state down when appropriate, and my tests for my components are about a million times easier. Yes, it gets annoying passing props down three, four, even five levels, but it gives a level of predictability that I appreciate.
We’ve already covered a lot of ground, and this is only about 1/3rd of the outline I put together for this series, so we’ll wrap up here and keep this short and sweet. Ultimately, next.js has some really cool aspects of it, and I hope that this series of posts can help you tackle adopting next.js with a little more certainty and information! Stay tuned for the next couple of posts where we’ll discuss AJAX, Custom Routing, and Deployment (plus more)!