Lessons Learned Building in Next.js

Part 2: Ajax, Styling, and Routing

Last Updated: 5/9/2017

…uh, hey everyone! How’s it going? I’m a crab, just hanging out, doing my crab thing.

Previous Post in this Series

Current Versions:

  • React: v15.4
  • next.js: v2.3

Where We Left Off

In the last post, I talked a little bit about some of the initial hiccups and snags I hit while working on converting my create-react-app over to a next.js app, as well as documented out some of the ways to either clear the hurdles or understand the differences to make that initial bump as small and easy-to-clear as we possibly can!

AJAX calls

This was something I was a little nervous about, because dealing with ajax requests in Redux already feels a little clunky, and I wasn’t sure what adding another layer to all of this was going to do to my sanity. I’ve toyed with redux-sagas, redux-thunk, rxjs and observables and…well, some of it is probably just me and my own personal hangups with how some of this stuff works. That being said: I was pleasantly surprised with how pleasant a lot of this was to implement! To be fair, I still pretty much stayed away from Redux when implementing any of my ajax calls, but almost none of them outside of logins were affecting global state, so nothing needed to go through the deep transformation levels of generators and thunks and whatnot.

getInitialProps and State

One thing right out of the gate that next.js provides for you is a special static function you can declare on either class components or functional components: getInitialProps. So, if you were to declare this on a class, for example, you’d do so like this:

class Foo extends React.Component {
static getInitialProps() {
return { data: "Some Data" };
}
render() {
return <div>{this.props.data}</div>;
}
}

Or, if you were to declare it on a functional component, you can do something like this:

const Foo = ({ data }) => <div>{data}</div>;
Foo.getInitialProps = () => ({ data: "Some Data" });

What this tells React is that there are some default props that should get populated as part of the initialization of this page. This might be data that needs to get loaded in from some external source; maybe an API server or something to that effect. Maybe there’s some sort of calculation that needs to happen, or…there’s a lot of reasons why you may want to start off with populating some data. This is particularly important with next.js and how it deals with server-side rendering: this tells next.js what sort of data to try to prefetch or populate initially to prepare the app and make it fast. It also allows for some neat tricks with link prefetching!

One thing I ran into is that some of my endpoints require authentication, and that comes from a token populated via Redux. Trouble is, getInitialProps will be called BEFORE the token is loaded if the page is refreshed, which means it will be sending out unauthenticated requests. This is something you’ll need to consider when deciding whether to use getInitialProps or not to prepopulate data.

When I ran into this particular issue, I instead opted to go the React-standard route and have data populate via componentDidMount instead. Also, note that getInitialProps can only be used on pages, not on components!

Dealing with async/await

Another thing that next.js provides out of the box is support for async/await, which is really nice! If you’re used to doing promises for your ajax calls you can instead declare your functions as async and have a few await statements. Here’s an example with a promise that returns an object that has a function that returns another promise:

function foo() {
SomeService
.makeRequest('url')
.then(res => {
res
.json()
.then(body => {
console.log('Data received:', body);
});
});
}

Can instead be written as:

async function foo() {
const res = await SomeService.makeRequest('url');
const body = await res.json();
console.log(body);
};

It works very well with both axios and fetch, so you won’t have to do any special magic to make this work, and you can enjoy the benefits of simpler code. The example above really hammers home how much simpler being able to just do async/await out of the box is! Being able to avoid the confusion of nested promises is totally worth it!

In terms of which library is better for getting data out of an API, again, I’ll use the phrase “dealer’s choice”. I’ve had the most experience with axios so it is what I tend to favor, but I know a decent portion of the React community uses fetch for simple things, so hey, you can do either and they’ll both work great!

CSS

With the previous incarnation of the app I was building, it was being built out primarily in create-react-app and I was using external CSS files to handle styling the components. It was…okay, but it certainly wasn’t ideal. It was clunky to modify and the styles relied on class names that were global. So, I’d have a directory structure like this:

- login/
- Login.js
- Login.css
- store/
- Store.js
- Store.css
- StoreForm.js
- StoreForm.css
- StoreItem.js
- StoreItem.css

Etc. You can likely see where this might start to get very confusing, especially since anything applied in StoreForm.css could actually affect Store.css as well! Instead, next.js relies on styled jsx, which allows you to embed the CSS for a component IN that component (in the JSX as well), and it enforces that the embedded CSS affects ONLY that component!

So if login.js was a simple component:

const Login = () => (
<div className="Login">
Login Time
</div>
);

And login.css contains something like this:

.Login {
border: 2px solid red;
}

You could instead do this inside of login.js and be sure that it will only affect the component there:

const Login = () => (
<div className="Login">
<style jsx>{`
.Login { border: 2px solid red; }
`}</style>
Login Time
</div>
);

Routing

Routing is a much trickier beast in next.js, admittedly. It’s much simpler to be able to use something like react-router and specify your routes and nested routes and routing rules/dynamic routes/etc, but here you’ll have to do some things a little differently. First of all, you’ll need to name your js files inside of your pages/ directory to be the routes you want to see. So as mentioned before, if your app has an index.jsand about.js inside of pages/, your app would have [domain.com] and [domain.com]/about URLs available. Similarly, if you want to use underscores or dashes in your path, that works too! pages/some-page.js and pages/some_page.js will map to [domain.com]/some-page and [domain.com]/some_page, respectively!

But what if you want to do something more tricky? Let’s say you want something like [domain.com]/posts/[slug]? You can’t do this out of the box with no configuration, but thankfully next.js provides the ability to use your own custom server.js for overriding the default behavior of next.js!

Switching to Express.js for the server

I started off trying to follow the example server.js from the README to very mixed results. I could sort of get things working but it was a significant struggle, and I ran into a lot of issues where the route would work until I refreshed the page, or wouldn’t work on refresh but would only work via a link, which was very frustrating to try to solve. What I instead ended up doing after finding a few Github issues and StackOverflow posts was adopting express as my custom server, which made dynamic routing about a million times easier to deal with and parse! Here is an example of a custom server.js file that uses express and implements some dynamic routing. Suffice to say that for this to work, you will have to add express to your project via NPM or Yarn, and then create server.js:

const express = require('express');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
// CUSTOM ROUTES GO HERE
server.get('/blog/:slug', (req, res) => {
const mergedQuery = Object.assign({}, req.query, req.params);
return app.render(req, res, '/blog', mergedQuery);
});
// THIS IS THE DEFAULT ROUTE, DON'T EDIT THIS
server.get('*', (req, res) => {
return handle(req, res);
});
const port = process.env.PORT || 3000;
server.listen(port, err => {
if (err) throw err;
console.log(`> Ready on port ${port}...`);
});

This will give you a param that gets sent to your blog.js component inside of your pages/ directory and give you the custom routing that you want! It also makes it very simple and easy to define custom routes. Where it gets a little weird, however, is when you need to deal with the client-side and linking to these dynamic routes on your pages. So, assuming we have the route setup above /blog/:slug, your links to specific slugs would need to be structure using next/link’s Link component via the following:

<Link href={`/blog?slug=${slug}`} as={`/blog/${slug}`} prefetch>
...
</Link>

as is what the user will see in their browser, but href is what next.js will interpret to figure out how things need to get routed. Both of these steps are required to make the link behavior and routing behavior behave the same no matter where the page is rendered from! Hopefully this will save you much frantic googling!

I know there are also specifically next.js packages that do this sort of thing but I did not get a chance to mess around with them (such as next-routes). Finally, you’ll need to modify your package.json file to include everything so that next.js knows how to run the server.js file:

"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js",
}

Now you can continue to run your dev server with npm run dev or yarn dev and you can build/start your production server as well!

Conclusion

Now we have a pretty solid foundation for our next.js app. We’ve covered a lot of the bare necessities to getting our development environment up and running and configured the way we like, but next we’ll need to start testing this and preparing it for deployment. In the next and final post in this series (possibly final? Definitely not “Final Fantasy” final but maybe “Final Destination” final), we’ll talk about testing, deployment, and finally what to do to get your app running (and keep it running, too).

Next Post in this Series