Import a library only on the server side to use in getInitialProps

Viewed 578

My setup

I'm using getInitialProps on the _app.js file to preload data on the server side:

import App from "next/app";

// The problematic import
import nodeFetchCache from "node-fetch-cache";

function MyApp({ Component, pageProps, customProps }) {
  return <Component customProps={customProps} {...pageProps} />;
}

MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext);
  
  // When I remove the top import and use a classic "fetch" here, it works fine
  const res = await nodeFetchCache(
    `${process.env.NEXT_PUBLIC_API_URL}/api/node/last_items`
  );

  const items = await res.json();

  return {
    ...appProps,
    customProps: {
      footerArticles: items.data.articles,
      footerVideos: items.data.videos,
    },
  };
};

export default MyApp;

My problem

This code yields an error:

Unhandled Runtime Error TypeError: The "original" argument must be of type Function

Call Stack promisify node_modules/util/util.js (614:0) eval node_modules/@npmcli/move-file/index.js (25:0) Object../node_modules/@npmcli/move-file/index.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (716:1) Object.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval node_modules/cacache/lib/entry-index.js (16:17) Object../node_modules/cacache/lib/entry-index.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (1598:1) Object.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval node_modules/cacache/ls.js (3:14) Object../node_modules/cacache/ls.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (1686:1) Object.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval node_modules/cacache/index.js (3:11) Object../node_modules/cacache/index.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (1543:1) Object.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval webpack-internal:///./node_modules/node-fetch-cache/src/classes/caching/file_system_cache.js (5:65) Module../node_modules/node-fetch-cache/src/classes/caching/file_system_cache.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (4513:1) Module.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval webpack-internal:///./node_modules/node-fetch-cache/src/index.js (13:95) Module../node_modules/node-fetch-cache/src/index.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (4557:1) Module.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval webpack-internal:///./pages/_app.js (10:74) Module../pages/_app.js file:///Users/dev/app/.next/static/chunks/pages/_app.js (40:1) Module.options.factory /_next/static/chunks/webpack.js (685:31) webpack_require file:///Users/dev/app/.next/static/chunks/webpack.js (37:33) fn /_next/static/chunks/webpack.js (354:21) eval node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?page=%2F_app&absolutePagePath=private-next-pages%2F_app! (5:15) eval node_modules/next/dist/client/route-loader.js (207:48)

From what other devs told me, it seems like Next is trying to import server side code on the client. Even when I remove the await nodeFetchCache(...) call, the only fact that I import 'node-fetch-cache' at the top gives me an error.

My question

Is what I'm trying to achieve even possible ? Isn't getInitialProps supposed to run only on the server side? And isn't code/tree shaking supposed to eliminate unused imports from the client side bundle?

I'm a bit lost here. Any help would be very much appreciated.

1 Answers

Isn't getInitialProps supposed to run only on the server side?

Only in certain conditions.

Usually, getInitialProps runs both on the server (on initial page loads) and on the client (on client-side navigations). The exception to this rule is when the page you're navigating to uses getServerSideProps.

From the getInitialProps documentation:

For the initial page load, getInitialProps will run on the server only. getInitialProps will then run on the client when navigating to a different route via the next/link component or by using next/router. However, if getInitialProps is used in a custom _app.js, and the page being navigated to implements getServerSideProps, then getInitialProps will run on the server.

As a consequence, unlike getServerSideProps/getStaticProps, getInitialProps's code does not get automatically eliminated from the client-side bundle by Next.js. This means that webpack will include any libraries you use inside getInitialProps in the client-side bundle. Ideally, you'll want to write isomorphic code in getInitialProps - code that can run both on the client and on the server.


In your case, node-fetch-cache is a Node.js library that cannot run in the browser. From what I can see the possible solutions are:

#1 Use an isomorphic package

The straightforward solution would be to use an isomorphic library to replace node-fetch-cache, like isomorphic-fetch. This would allow you to use it both on the client and the server without the additional hassle.

#2 Use webpack's IgnorePlugin

If you want to run the node-fetch-cache code on the server only inside getInitialProps, you could use webpack's IgnorePlugin to ignore the module on the client-side, preventing any potential error.

// next.config.js
module.exports = {
    // Other configs you may have...
    webpack: function (config, { isServer, webpack }) {
        if (!isServer) {
            config.plugins.push(
                new webpack.IgnorePlugin({ resourceRegExp: /node-fetch-cache/ })
            );
        }
        return config;
    }
}

#3 Use webpack's resolve.alias

Similar to the previous solution, you could use set resolve.alias to false to tell webpack to ignore node-fetch-cache on the client-side.

// next.config.js
module.exports = {
    // Other configs you may have...
    webpack: function (config) {
        config.resolve.alias['node-fetch-cache'] = false;
        return config;
    }
}

Note that, for solutions #2 and #3 you need to refactor your current code to properly handle the client-side scenario, as node-fetch-cache won't be available to use there.

MyApp.getInitialProps = async (appContext) => {
    const appProps = await App.getInitialProps(appContext);
  
    if (appContext.ctx.req) { // Check if on the server-side
        // Dynamically import `node-fetch-cache` on the server-only  
        const { default: nodeFetchCache } = await import('node-fetch-cache');
        const res = await nodeFetchCache(`${process.env.NEXT_PUBLIC_API_URL}/api/node/last_items`);
        const items = await res.json();

        return {
            ...appProps,
            customProps: {
                footerArticles: items.data.articles,
                footerVideos: items.data.videos
            }
        };
    } else {
        // Handle client-side here
    }
};
Related