I have a project at the moment that uses nextjs embedded in an express server. I have a page called event.js
and I route /event/:eventId
to /event?eventId={id}
.
My issue now is that if the user goes to /event
without specifying an ID my page component is rendered anyway.
My suggestion is: Allow getInitialProps
function to return null
(or false
or 404
or new Error("")
), which will result in next intercepting this and rendering the 404 page.
expressApp.get('/event/:eventId', (req, res) => {
const mapToUrl = '/event'
const query = {
...req.query,
eventId: req.params.eventId
}
nextApp.render(req, res, mapToUrl, query)
})
expressApp.get('*', nextHandler)
You can do something like this:
import React from 'react'
// Import some custom error page
import Error404 from './Error404'
export default class MyPage extends React.Component {
static async getInitialProps ({ res, query }) {
// Make your validations
const statusCode = !query.eventId ? 404 : 200;
// Change the server response code if needed
if(statusCode !== 200) {
res.statusCode = statusCode;
}
return { statusCode };
}
render () {
const { statusCode } = this.props;
if (statusCode === 404) {
return <Error404 />
}
return (
<div>
Your component code
</div>
)
}
}
@coluccini thanks for the response.
I thought of doing something like this, but it doesn't seem like the most optimal solution. You would need to replicate these same checks on most of your "dynamic" routes and also, by importing Error404
you're bundling more JS code than you need for your page, even if it's not required.
I also thought of using a HOC to achieve a similar behaviour, but I still thought it could be beneficial to bake it directly in Next.
The custom error component is optional. You can just set the res.statusCode to 404 and next will render the default error component (if you wish to customize it, create a _error.js under /pages. There's an example on the docs)
@coluccini solution is the right one. Though instead of Error404 you can just use _error
or next/error
if you don't use a custom 404. Now as to easily including. You can create a hoc similar to this:
import ErrorPage from 'next/error'
export default (Component) => class extends React.Component {
static async getInitialProps(ctx) {
const props = await Component.getInitialProps(ctx)
const {statusCode} = ctx.res
return {statusCode, ...props}
}
render() {
const {statusCode} = this.props
if(statusCode && statusCode !== 200) {
return <ErrorPage statusCode={statusCode} />
}
return <Component {...this.props} />
}
}
Am I right that this does not actually return a 404 status code? Just a 200 that looks like a 404?
@rovansteen if you are following Tim's example, you should set the ctx.res.statusCode
to 404 on the Component
you are passing to the HOC
import ErrorPage from 'next/error'
export withError = (Component) => class extends React.Component {
static async getInitialProps(ctx) {
const props = await Component.getInitialProps(ctx)
const {statusCode} = ctx.res
return {statusCode, ...props}
}
render() {
const {statusCode} = this.props
if(statusCode && statusCode !== 200) {
return <ErrorPage statusCode={statusCode} />
}
return <Component {...this.props} />
}
}
---
import withError from './withError'
class Example extends React.Component {
static async getInitialProps(ctx) {
if (error) { // define your app error logic here
ctx.res.statusCode = 404;
}
return { /* ... */ }
}
render() {
/* ... */
}
}
export default withError(Example);
@timneutkens Can you answer @rovansteen question. I'm facing the problem that your solution render a view of 404 with 200 status in header.
@Everettss is my answer doesn't work for you? you need to set ctx.res.statusCode
if you want to have an different http response code
@coluccini Oh god! Sorry for the mix-up. I've tried your example inaccurately. :sweat:
Your example work like a charm :heart:
@coluccini I have used your answer. When entering the url manually in the address bar, everything works fine, however when navigating using next/link, it shows the following error:
TypeError: Cannot read property 'statusCode' of undefined
Here is my index.js code:
import React, { Component } from "react";
import Link from "next/link";
import PostLink from "../components/PostLink";
import { connect } from "react-redux";
import { itemsFetchData } from "../actions/items";
import withError from "../lib/withError";
class Index extends Component {
static async getInitialProps({ reduxStore, query, res }) {
let pageNum = 1;
var statusCode = 200;
if (query.pageNum) {
if (/^([1-9][0-9]*)$/.test(query.pageNum)) {
pageNum = parseInt(query.pageNum, 10);
} else {
statusCode = 404;
}
}
if (statusCode == 200) {
}
res.statusCode = statusCode;
return {};
}
render() {
return (
<div>
<p>React Component! Index {this.props.items.length}</p>
<div className="row justify-content-center">
<PostLink id="hello-nextjs" title="Hello Next.js" />
<PostLink id="learn-nextjs" title="Learn Next.js is awesome" />
<PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
status: state.tagsReducer.tagsData.status,
items: state.tagsReducer.tagsData.items,
hasError: state.tagsReducer.tagsHasError,
isLoading: state.tagsReducer.tagsIsLoading
};
};
export default withError(connect(mapStateToProps)(Index));
My withError component is exactly the same as Tim's code.
UPDATE: If the page is being rendered on the server, ctx.res is defined, however if it is rendered on the client side, it is undefined! What should we do?
@hi-rad this is the normal behaviour of getInitialProps
.
req
& res
are coming from the http request so from the server.
You can use the shallow routing to route without running getInitialProps
For posterity, here's what I landed on:
import React from 'react';
import PropTypes from 'prop-types';
import ErrorPage from '../../pages/_error';
const withError = Component => {
class WithErrorComponent extends React.Component {
static async getInitialProps(ctx) {
const props = await Component.getInitialProps(ctx);
const { statusCode } = ctx.res || {};
return { statusCode, ...props };
}
render() {
const { statusCode } = this.props;
if (statusCode && statusCode !== 200) {
return <ErrorPage statusCode={statusCode} />;
}
return <Component {...this.props} />;
}
}
WithErrorComponent.propTypes = {
statusCode: PropTypes.number,
};
WithErrorComponent.defaultProps = {
statusCode: null,
};
return WithErrorComponent;
};
export default withError;
Most helpful comment
@rovansteen if you are following Tim's example, you should set the
ctx.res.statusCode
to 404 on theComponent
you are passing to the HOC