How do I render a 404 page in NextJS in development and production? - http-status-code-404

I have a situation where I am using nextjs for client-side and the '404 - page not found error' only appears in local development, but I would like to see it on dev and on production.
How can I archive this?
Found some examples on the net that worked well locally but didn't work again on dev and prod since I could get the statusCode locally from ctx.res.statusCode from the getInitialProps method in _app.js and display the default nextjs Error component if the status code is 404.
I also created a custom _errors.js page, but this also works in local dev only
Reusing the built-in error page :
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';
import Error from 'next/error';
import Page from '../components/Page';
import withData from '../lib/withData';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
const { res } = ctx;
const errorCode = res.statusCode > 200 ? res.statusCode : false;
pageProps.query = ctx.query;
return {
pageProps,
errorCode,
};
}
render() {
const { Component, apollo, pageProps, errorCode } = this.props;
if (errorCode) {
return <Error statusCode={errorCode} />;
}
return (
<Container>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
</Container>
);
}
}
export default withData(MyApp);
Custom error handling solution :
import React from 'react';
import PropTypes from 'prop-types';
class Error extends React.Component {
static getInitialProps({ res, err }) {
let errCode = null;
if (res.statusCode) {
errCode = res.statusCode;
} else if (err.statusCode) {
errCode = err.statusCode;
}
return { errCode };
}
render() {
const { errCode } = this.props;
return <p>{errCode ? `An error ${errCode} occurred on server` : 'An error occurred on client'}</p>;
}
}
Error.propTypes = {
errCode: PropTypes.number,
};
export default Error;

Related

NextJS undefined appContext.ctx.req.headers on Cookies

I want to store my portfolio password in browser cookies. I added the following code to my _app.js now when visiting the not normal pages I'm getting an error. I don't understand what I have done wrong.
import App from 'next/app'
import Cookies from 'universal-cookie'
import consts from '../consts'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext)
const cookies = new Cookies(appContext.ctx.req.headers.cookie)
const password = cookies.get(consts.SiteReadCookie) ?? ''
if (password === 'letmein') {
appProps.pageProps.hasReadPermission = true
}
return { ...appProps }
}
export default MyApp
Error

NextJS use client-side error in custom error page

NextJS renders the custom error page (_error.jsx) when there is client-side UI error.
Is it possible to get information on the error inside the custom error page?
I'm using a custom App:
// pages/_app.jsx
import App from 'next/app'
function CustomApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
CustomApp.getInitialProps = async (props) => {
{ Component, ctx } = props
const initProps = await Component.getInitialProps(ctx);
return { pageProps: initProps }
}
export default CustomApp
and I have a page which triggers an error
// pages/throw_error.jsx
function ThrowErrorPage() {
throw new Error("client-side error")
...
}
export default ThrowError
and a custom error page
// pages/_error.jsx
function Error(props) {
// Expecting error from the UI
const { err } = props;
return (
<div>{err.message}</div>
)
}
Error.getInitialProps = async (props) => {
const { err } = props;
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { err, statusCode };
}
export default Error
I was expecting err to be passed from the UI to _error.jsx. Am I missing something?
So it turns out that NextJS does return client-side error to the custom error component. When a client-side error occurs, NextJS fetches the custom _error.jsx [1], and passes it as the value of the Component to the custom App (_app.jsx), along with a ctx object which contains the error object err [2]. In the custom App, we can then pass the error object to the error component as a prop, and extract it in the component for display/logging:
First pass the error object in the custom app to the component where the error is going to be rendered:
// pages/_app.jsx
import App from 'next/app'
function CustomApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
CustomApp.getInitialProps = async (props) => {
{ Component, ctx : { err } } = props;
// Return early if there is an error
// pass the error to the component
if (err) return { pageProps : { err } };
const componentProps = await Component.getInitialProps(ctx);
return { pageProps: { ...componentProps } };
}
export default CustomApp;
Then extract the error object from the component's prop
// pages/_error.jsx
function Error({ err }) {
logToService(err)
return err.message;
}
export default Error;

nextjs 9.4 error on HOC private route with cookies

Trying to implement private ( authenticated ) routes in Nextjs using HOC and cookies but running into error below:
TypeError: Object(...) is not a function
at export default withPrivateRoute(Private);
I have checked elsewhere in the app that cookies are available and also sent with the request. They seem to be available server side.
The HOC at `/components/withPrivateRoute
import { withRouter } from 'next/router';
import { withCookies } from 'react-cookie';
const withPrivateRoute = (authComponent) => {
return class Private extends React.Component {
componentDidMount() {
console.log('PRIVATE ROUTE', this.props);
const { router, cookies } = this.props;
const intendedRoute = router.pathname;
const isAdmin = !!cookies.get('isAdmin');
const isAuthenticated = !!cookies.get('username');
if (!isAuthenticated) {
router.push({
pathname: '/login',
query: { from: intendedRoute },
});
}
if (
isAuthenticated &&
router.pathname.includes('admin') &&
!isAdmin
) {
router.push('/');
}
}
render() {
// eslint-disable-next-line react/jsx-props-no-spreading
return <authComponent {...this.props} />;
}
}
}
export default withCookies(withRouter(withPrivateRoute));
The private route example:
import withPrivateRoute from '../components/withPrivateRoute';
import getCategories from '../lib/getCategories';
const Private = (props) => {
console.log('props', props);
return <div>Private route </div>;
}
export default withPrivateRoute(Private);
export async function getStaticProps() {
let categories = await getCategories();
categories = categories.data.categories;
return {
props: {
categories,
},
};
}
I have since found a better way to handle private routes in Nextjs from this discussion:
Everything is handled inside getServerSideProps, no HOC required.
class Private extends React.Component{
render() {
console.log('props', this.props);
return <p>Private route</p>;
}
}
export default Private;
export async function getServerSideProps(context) {
const { req: { headers, url }, res } = context;
const cookies = {};
if (headers && headers.cookie) {
headers.cookie.split(';').forEach((cookie) => {
const parts = cookie.match(/(.*?)=(.*)$/);
cookies[parts[1].trim()] = (parts[2] || '').trim();
});
}
const isAuthenticated = !!cookies.username;
const isAdmin = !!cookies.isAdmin;
if (!isAuthenticated) {
res.setHeader('Location', `/login?from=${url}`);
res.statusCode = 307;
}
if (
isAuthenticated &&
url.includes('admin') &&
!isAdmin
) {
res.setHeader('Location', '/');
res.statusCode = 307;
}
return {
props: {
},
};
}

NextJS: How to add screen loading for production build?

I want add screen loading in next js project. And I tried to do that with the Router component in next/router.
This is my _app.js in next.js project:
import {CookiesProvider} from 'react-cookie';
import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import withRedux from 'next-redux-wrapper'
import withReduxSaga from 'next-redux-saga'
import createStore from '../src/redux/store'
import Router from "next/router";
import {Loaded, Loading} from "../src/util/Utils";
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ctx})
}
return {pageProps}
}
render() {
Router.onRouteChangeStart = () => {
Loading()
};
Router.onRouteChangeComplete = () => {
Loaded()
};
Router.onRouteChangeError = () => {
Loaded()
};
const {Component, pageProps, store} = this.props;
return (
<CookiesProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</CookiesProvider>
)
}
}
export default withRedux(createStore)(withReduxSaga(MyApp))
This is Loaded() and Loading() functions:
export const Loaded = () => {
setTimeout(() => {
let loading = 'has-loading';
document.body.classList.remove(loading);
}, 100);
};
export const Loading = () => {
let loading = 'has-loading';
document.body.classList.add(loading);
};
The code works well when the project is under development mode. But when the project is built, the loading won't disappear.
Do you know solution of this issue or are you suggesting another solution?
Using apollo client and react hooks you could do as follow.
Example:
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { withApollo } from '../lib/apollo';
import UserCard from '../components/UserCard';
export const USER_INFO_QUERY = gql`
query getUser ($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
}
}
`;
const Index = () => {
const { query } = useRouter();
const { login = 'default' } = query;
const { loading, error, data } = useQuery(USER_INFO_QUERY, {
variables: { login },
});
if (loading) return 'Loading...'; // Loading component
if (error) return `Error! ${error.message}`; // Error component
const { user } = data;
return (
<UserCard
float
href={user.url}
headerImg="example.jpg"
avatarImg={user.avatarUrl}
name={user.name}
bio={user.bio}
/>
);
};
export default withApollo({ ssr: true })(Index);
More info here: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I added the following codes to a wrapper component and the problem was resolved.
componentDidMount() {
Loaded();
}
componentWillUnmount() {
Loading();
}

Cookie token access in _app.js in next.js

Here is my _app.js page:
import { Provider } from 'react-redux';
import App from 'next/app';
import withRedux from 'next-redux-wrapper';
import { Layout } from '../components';
import makeStore from '../store';
import { api } from '../utils/api';
class MyApp extends App {
render() {
const { Component, pageProps, store } = this.props;
console.log(this.props); // Here I cannot see cookie user data, it is empty object even though the cookie is set in `_document.js`
return (
<Provider store={store}>
<Layout { ...this.props }>
<Component { ...pageProps } />
</Layout>
</Provider>
);
}
}
MyApp.getInitialProps = api.authInititalProps();
export default withRedux(makeStore)(MyApp);
As you can see I'm trying to get cookie user data in getInitialProps lifycycle. Here my api file which gathers the logic of getting the cookie data:
const WINDOW_USER_SCRIPT_KEY = '__USER__';
class API {
getServerSideToken = req => {
const { signedCookies = {} } = req;
if(!signedCookies || !signedCookies.token) {
return {}
}
return { user: signedCookies.token };
}
getClientSideToken = () => {
if(typeof window !== 'undefined') {
const user = window[WINDOW_USER_SCRIPT_KEY] || {};
return { user };
}
return { user: {} }
};
authInititalProps = () => ({ req, res }) => {
return req ? this.getServerSideToken(req) : this.getClientSideToken();
}
}
The token itself is set in `_document.js:
import Document, { Head, Main, NextScript } from 'next/document';
import { api } from '../utils/api';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const props = await Document.getInitialProps(ctx);
const userData = await api.getServerSideToken(ctx.req);
return { ...props, ...userData }
}
render() {
const { user = {} } = this.props;
return(
<html>
<Head />
<body>
<Main />
<script dangerouslySetInnerHTML={{ __html: api.getUserScript(user) }} />
<NextScript />
</body>
</html>
);
}
};
export default MyDocument;
What is happening is that I'm not getting the cookie token inside my _app.js, instead I'm getting empty object, even though the cookie is set and in the console after typing window.__USER__ I can see it. The aim of all that is to pass user data down to Layout component which is invoked inside the _app.js. Why cannot I see it inside _app.js?

Resources