Storybook re-renders story after receiving MSW response - reactjs

When I trigger requests to the backend within a Storybook story, the story is re-rendered after receiving the response from Mock Service Worker (MSW). How can I prevent the re-rendering from happening?
Here's the story:
I followed the tutorial on https://storybook.js.org/addons/msw-storybook-addon to set up MSW for my Storybook stories.
So I:
installed msw and msw-storybook-addon
generated a service worker: npx msw init static/
updated my preview.js to call the initialize() function, added the mswDecorator, and set some global handlers (e. g. for "/api/customers")
When opening a component in Storybook that includes a request to "/api/customers" the following happens:
Initially MSW is telling me that it's enabled: [MSW] Mocking enabled.
I click a button to manually send a request to "/api/customers"
MSW is telling me that this API request is covered: [MSW] 15:21:20 GET /api/customers/ (200 OK)
I print the request results on the console - that works and I can see the expected results printed
But right after that the story is unintentionally re-rendered - this is what I want to suppress.
FF is telling me:
The connection to http://localhost:6006/__webpack_hmr was interrupted
while the page was loading.
Chrome doesn't give me that piece of information.
Here's the setup:
package.json:
{
...
"msw": {
"workerDirectory": "static"
}
}
main.js (Storybook):
// imports ...
const toPath = (filePath) => path.join(process.cwd(), filePath);
module.exports = {
core: {
builder: "webpack5",
},
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.#(js|jsx|ts|tsx)"],
addons: [
"#storybook/addon-links",
"#storybook/addon-essentials",
],
features: {
emotionAlias: false,
},
webpackFinal: (config) => {
return {
...config,
module: {
...config.module,
rules: custom.module.rules, // rules how to handle file extensions
},
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
"#emotion/core": toPath("node_modules/#emotion/react"),
"emotion-theming": toPath("node_modules/#emotion/react"),
},
},
};
},
staticDirs: ["../path/to/static/"], // that's our public
};
Here's some code:
preview.js
export const parameters = {
...
msw: {
handlers: [
rest.get("/api/customers", (req, res, ctx) => {
let customers = ...;
return res(
ctx.json({
data: customers,
})
);
}),
],
},
}
ExampleStory.stories.jsx:
export default {
title: "Customers",
};
const getCustomers = () => {
// ... set axios config etc.
axios.get("/api/customers", config)
.then((response) => response.data.data)
.then((customers) => console.log(customers)); // yep, the customers gets printed!
};
export const ExampleStory = () => {
return (
<div>
Simple demo story
<button onClick={getCustomers}>Get customers</button>
</div>
);
};
Node Modules:
Storybook#6.5.9
msw#0.42.3
msw-storybook-addon#1.6.3

Related

Uncaught ReferenceError: process is not defined error when converting react (TS) app to PWA with webpack and babel config

I have created a react (TS) using babel + webpack. It is working fine. Now I am converting it to a PWA. I followed this webpack article to updated webpack.config.js. To register the service worker, instead of doing it in index.html, I created a service-worker.tsx and referencing it in index.tsx.
I am getting this error
service-worker.ts:22 Uncaught ReferenceError: process is not defined
at register (service-worker.ts:22:33)
at eval (index.tsx:14:60)
at ./src/index.tsx (main.js:219:1)
at webpack_require (main.js:253:41)
at main.js:322:37
at main.js:324:12
I have added 3 files, where I changed the configuration to integrate workbox, for registering and un registering.
webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const path = require("path");
// const htmlPlugin = new HtmlWebPackPlugin({
// template: "./src/index.html",
// filename: "./index.html",
// });
const htmlWebPackPlugin = new HtmlWebPackPlugin({
title: "PWA Webpack Demo",
template: "./src/index.html",
filename: "./index.html",
});
const workBoxPlugin_Generate = new WorkboxPlugin.GenerateSW({
// these options encourage the ServiceWorkers to get in there fast
// and not allow any straggling "old" SWs to hang around
clientsClaim: true,
skipWaiting: true,
});
module.exports = {
entry: "./src/index.tsx",
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
output: {
// NEW
path: path.join(__dirname, "dist"),
filename: "[name].js",
}, // NEW Ends
plugins: [htmlWebPackPlugin, workBoxPlugin_Generate],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: ["ts-loader"],
},
// css rules
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
service-worker.tsx (error at export default function register)
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
export default function register() {
if ("serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString(),
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker.",
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log("New content is available; please refresh.");
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log("Content is cached for offline use.");
}
}
};
}
};
})
.catch(error => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get("content-type")!.indexOf("javascript") === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode.",
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
index.tsx
import { createRoot } from 'react-dom/client';
import App from "./app";
//import * as serviceWorker from './serviceWorker';
import registerServiceWorker from './service-worker'
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(<App />)
console.log('Registering service worker', 'Starting...')
registerServiceWorker()
After a little bit of research I found out that I needed to update the webpack.config.js with the following process definitions. It was not a problem of export as I was initially thinking. In my service worker I am using process.env entities.
In fact you can add other process.env parameters, I got my information from this article and this stackoverflow
In the webpack.config.js add the following:
const webpack = require("webpack");
const processEnvPlugin = new webpack.DefinePlugin({"process.env.PUBLIC_URL": JSON.stringify(process.env.PUBLIC_URL)});
plugins: [htmlWebPackPlugin, workBoxPlugin_Generate, processEnvPlugin]

unexpected token error testing component with graphql

I'm trying to test my components with jest. I'm using apollo client to query date from apollo server. I have follewed apollo client docs for testing queries, but without success.
I'm using nextjs and mongoDB with mongoose.
I'm getting this error:
FAIL components/organisms/createPost/CreatePost.test.jsx
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Details:
/Users/eyub/Documents/Learning/Projects/lim/node_modules/uuid/dist/esm-browser/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { default as v1 } from './v1.js';
^^^^^^
SyntaxError: Unexpected token 'export'
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1796:14)
here is my component
export default function CreatePost({}) {
const [createPost] = useMutation(CREATE_POST, {
refetchQueries: [{ query: GET_USER }, 'getUserData'],
})
const sumbitHandler = (event) => {
event.preventDefault()
createPost({ variables: { text: event.target[0].value } })
event.target[0].value = ''
}
return (
<Component onSubmit={sumbitHandler}>
<textarea placeholder="What's happening?" />
<button>
<Icon icon={'add'} />
</button>
</Component>
)
}
here is my test:
import { MockedProvider } from '#apollo/client/testing'
import { CREATE_POST } from '#graphql/client/mutation'
import '#testing-library/jest-dom'
import { render } from '#testing-library/react'
import CreatePost from './CreatePost'
const newPostData = {
text: '',
}
const mocks = [
{
request: {
query: CREATE_POST,
variables: {
text: '',
},
},
result: { data: newPostData },
},
]
describe('<CreatePost />', () => {
test('mocking test pass', () => {
const component = render(
<MockedProvider mocks={mocks} addTypename={false}>
<CreatePost />
</MockedProvider>
)
component.debug()
})
})
here my jest config:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
// Add any custom config to be passed to Jest
const customJestConfig = {
// Add more setup options before each test is run
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleNameMapper: {
'^#components/(.*)$': '<rootDir>/components/$1',
'^#utils/(.*)$': '<rootDir>/utils/$1',
'^#graphql/(.*)$': '<rootDir>/graphql/$1',
},
collectCoverageFrom: ['./components/**/*.{js,jsx}'],
moduleDirectories: ['node_modules', '<rootDir>/'],
testEnvironment: 'jest-environment-jsdom',
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

FetchError: invalid json response body while deploying the Nextjs application on a server [duplicate]

I don't understand these errors when I export as production npm run build , but when I test npm run dev it works just fine. I use getStaticProps and getStaticPath fetch from an API route.
First when I npm run build
FetchError: invalid json response body at https://main-website-next.vercel.app/api/products reason: Unexpected token T in JSON at position
0
at D:\zummon\Main Website\main-website-next\node_modules\node-fetch\lib\index.js:272:32
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async getStaticPaths (D:\zummon\Main Website\main-website-next\.next\server\pages\product\[slug].js:1324:18)
at async buildStaticPaths (D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\utils.js:16:80)
at async D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\utils.js:26:612
at async D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\tracer.js:1:1441 {
type: 'invalid-json'
}
\pages\product\[slug]
import { assetPrefix } from '../../next.config'
export default function Page(){...}
export const getStaticProps = async ({ params: { slug }, locale }) => {
const res = await fetch(`${assetPrefix}/api/products/${slug}`)
const result = await res.json()
const data = result.filter(item => item.locale === locale)[0]
const { title, keywords, description } = data
return {
props: {
data,
description,
keywords,
title
}
}
}
export const getStaticPaths = async () => {
const res = await fetch(`${assetPrefix}/api/products`)
const result = await res.json()
const paths = result.map(({ slug, locale }) => ({ params: { slug: slug }, locale }))
return {
fallback: true,
paths,
}
}
next.config.js
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
assetPrefix: isProd ? 'https://main-website-next.vercel.app' : 'http://localhost:3000',
i18n: {
localeDetection: false,
locales: ['en', 'th'],
defaultLocale: 'en',
}
}
API routes
// pages/api/products/index.js
import data from '../../../data/products'
export default (req, res) => {
res.status(200).json(data)
}
// pages/api/products/[slug].js
import db from '../../../data/products'
export default ({ query: { slug } }, res) => {
const data = db.filter(item => item.slug === slug)
if (data.length > 0) {
res.status(200).json(data)
} else {
res.status(404).json({ message: `${slug} not found` })
}
}
// ../../../data/products (data source)
module.exports = [
{ locale: "en", slug: "google-sheets-combine-your-cashflow",
title: "Combine your cashflow",
keywords: ["Google Sheets","accounting"],
description: "...",
},
...
]
Second when I remove the production domain, I run npm run build but still get the error like
TypeError: Only absolute URLs are supported
at getNodeRequestOptions (D:\zummon\Main Website\main-website-next\node_modules\node-fetch\lib\index.js:1305:9)
at D:\zummon\Main Website\main-website-next\node_modules\node-fetch\lib\index.js:1410:19
at new Promise (<anonymous>)
at fetch (D:\zummon\Main Website\main-website-next\node_modules\node-fetch\lib\index.js:1407:9)
at getStaticPaths (D:\zummon\Main Website\main-website-next\.next\server\pages\[slug].js:938:21)
at buildStaticPaths (D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\utils.js:16:86)
at D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\utils.js:26:618
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async D:\zummon\Main Website\main-website-next\node_modules\next\dist\build\tracer.js:1:1441 {
type: 'TypeError'
}
My next.config.js after remove
const isProd = process.env.NODE_ENV === 'production'
module.exports = { //remove
assetPrefix: isProd ? '' : 'http://localhost:3000',
i18n: {
localeDetection: false,
locales: ['en', 'th'],
defaultLocale: 'en',
}
}
My package.json when I npm run build script
{
"name": "main-website-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start"
},
"dependencies": {
"next": "10.0.6",
"react": "17.0.1",
"react-dom": "17.0.1"
}
}
You should not call an internal API route inside getStaticProps. Instead, you can safely use your API logic directly in getStaticProps/getStaticPaths. These only happen server-side so you can write server-side code directly.
As getStaticProps runs only on the server-side, it will never run on
the client-side. It won’t even be included in the JS bundle for the
browser, so you can write direct database queries without them being
sent to browsers.
This means that instead of fetching an API route from
getStaticProps (that itself fetches data from an external source),
you can write the server-side code directly in getStaticProps.
Furthermore, your API routes are not available during build-time, as the server has not been started at that point.
Here's a small refactor of your code to address the issue.
// /pages/product/[slug]
import db from '../../../data/products'
// Remaining code..
export const getStaticProps = async ({ params: { slug }, locale }) => {
const result = db.filter(item => item.slug === slug)
const data = result.filter(item => item.locale === locale)[0]
const { title, keywords, description } = data
return {
props: {
data,
description,
keywords,
title
}
}
}
export const getStaticPaths = async () => {
const paths = db.map(({ slug, locale }) => ({ params: { slug: slug }, locale }))
return {
fallback: true,
paths,
}
}

Amplify React Native - Duplicate Error using amplify add api

I'm using this Amplify guide https://aws-amplify.github.io/docs/js/tutorials/building-react-native-apps/#connect-to-your-backend-1 and when I create an API using "aplify add api" the app fails.
I'm using "expo" and I'm using IphoneX for test phase.
My app code is
import React, { Component } from 'react';
import { StyleSheet, Text, Button, View, Alert } from 'react-native';
import Amplify, { API } from 'aws-amplify';
import amplify from './aws-exports';
import awsmobile from './aws-exports';
import { withAuthenticator } from 'aws-amplify-react-native';
Amplify.configure(amplify);
Amplify.configure(awsmobile);
state = { apiResponse: null };
class App extends Component {
async getSample() {
const path = "/items"; // you can specify the path
const apiResponse = await API.get("theListApi" , path); //replace the API name
console.log('response:' + apiResponse);
this.setState({ apiResponse });
}
render() {
return (
<View style={styles.container}>
<Text>test</Text>
<Button title="Send Request" onPress={this.getSample.bind(this)} />
<Text>Response: {this.state.apiResponse && JSON.stringify(this.state.apiResponse)}</Text>
</View>
);
}
}
export default withAuthenticator(App);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#63C8F1',
alignItems: 'center',
justifyContent: 'center',
},
});
executing "expo start" the command line return this message error:
jest-haste-map: Haste module naming collision: theListFunction
The following files share their name; please adjust your hasteImpl:
* <rootDir>/amplify/backend/function/theListFunction/src/package.json
* <rootDir>/amplify/#current-cloud-backend/function/theListFunction/src/package.json
Failed to construct transformer: DuplicateError: Duplicated files or mocks. Please check the console for more info
at setModule (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:620:17)
at workerReply (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:691:9)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async Promise.all (index 391) {
mockPath1: 'amplify/backend/function/theListFunction/src/package.json',
mockPath2: 'amplify/#current-cloud-backend/function/theListFunction/src/package.json'
}
(node:1506) UnhandledPromiseRejectionWarning: Error: Duplicated files or mocks. Please check the console for more info
at setModule (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:620:17)
at workerReply (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:691:9)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async Promise.all (index 391)
(node:1506) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1506) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
or
Error: Duplicated files or mocks. Please check the console for more info
at setModule (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:620:17)
at workerReply (/Users/j_hen/Documents/jdev/smartApp/sourcecode/mySmaertProject/node_modules/jest-haste-map/build/index.js:691:9)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async Promise.all (index 391)
What is wrong? How can I use the API correctly?
Amplify creates a copy of your current cloud backend configuration in amplify/#current-cloud-backend/.
You don't need those files to build your app, so you can ignore them in order to get rid of the error.
To do so, you can create a blacklist and add the folder to it.
Create a rn-cli.config.js file in the root of your project.
./rn-cli.config.js:
// works with older react native versions
// const blacklist = require('metro').createBlacklist;
const blacklist = require('metro-config/src/defaults/blacklist');
module.exports = {
resolver: {
blacklistRE: blacklist([/#current-cloud-backend\/.*/]),
},
};
Reference issue.
TypeScript considerations:
(As stated in Mush's answer)
If you are using typescript you should create the blacklist on
metro.config.js NOT rn-cli.config.js.
module.exports = {
resolver: {
blacklistRE: /#current-cloud-backend\/.*/
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
};
As stated
here.
2022
In Metro v0.64.0 blacklist was renamed to blocklist, release notes
My current solution is to edit the metro.config.js (or create a new one) and add the following
const exclusionList = require('metro-config/src/defaults/exclusionList');
module.exports = {
resolver: {
blacklistRE: exclusionList([/amplify\/#current-cloud-backend\/.*/]),
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
};
If you are using typescript you should create the blacklist on metro.config.js NOT rn-cli.config.js.
module.exports = {
resolver: {
blacklistRE: /#current-cloud-backend\/.*/
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
};
As stated here.
I just ran into this problem and had to create ./rn-cli.config.js with the following as blacklist was in a different folder:
const blacklist = require('metro-config/src/defaults/blacklist');
module.exports = {
resolver: {
blacklistRE: blacklist([/#current-cloud-backend\/.*/]),
},
};
After updating to Expo SDK41 this issue came back.
I needed to change previous rn-cli.config.js to metro.config.js (even though I'm not using TS) and install #expo/metro-config as devDependency.
Expo + Late 2021 Update
Not sure if this is fix is expo specific, but the current way to do this is with blacklist (rather than exclusionList). Like so:
metro.config.js
const blacklist = require("metro-config/src/defaults/blacklist")
module.exports = {
resolver: {
blacklistRE: blacklist([/amplify\/#current-cloud-backend\/.*/]),
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
}
For anyone who is still facing this issue I had to change something after this issue re-appeared.
For me, I had to change blacklist in line 1 to exclusionList
metro.config.js
const blacklist = require("metro-config/src/defaults/exclusionList")
module.exports = {
resolver: {
blacklistRE: blacklist([/amplify\/#current-cloud-backend\/.*/]),
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
}

React routing with Hapi

I have a requirement of using Hapi with create-react-app where Hapi acts as a proxy for api requests and also serves the React app.
I'm trying to get the routing working, but it doesn't seem to work with the current Hapi configuration.
Here is my server code:
const Path = require('path');
const Hapi = require('hapi');
const Inert = require('inert');
const init = async () => {
const server = new Hapi.Server({
port: process.env.PORT || 5000,
routes: {
files: {
relativeTo: Path.join(__dirname, '../build')
}
}
});
await server.register(Inert);
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: '.'
}
}
});
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [
{
module: 'good-console',
args: [{ request: '*', response: '*' }]
},
'stdout'
]
}
};
await server.register({
plugin: require('good'),
options,
});
await server.start();
console.log('Server running at:', server.info.uri);
};
init();
The index.html file loads fine when localhost:5000 is open. I have configured a route /dashboard in the react-router part. Hitting localhost:5000/dashboard gives a 404.
Questions:
How do I configure the routes in Hapi so that React takes over the routing after the index.html is rendered?
The current server code serves the app from the build folder after the app is built. How do I configure it for hot reload without ejecting from the create-react-app
Note: The routing works when running the react app with npm start. But this is without the Hapi server running.
I'm new to using Hapi so any pointers are appreciated.
So I played around with various hapi+inert combo and this is what ended up working for me.
server.js
const Path = require('path');
const Hapi = require('hapi');
const Inert = require('inert');
const routes = require('./routes');
const init = async () => {
console.log('Routes are', routes);
const server = new Hapi.Server({
port: process.env.PORT || 5000,
routes: {
files: {
relativeTo: Path.join(__dirname, '../build')
}
}
});
await server.register(Inert);
server.route(routes);
/**
* This is required here because there are references to *.js and *.css in index.html,
* which will not be resolved if we don't match all remaining paths.
* To test it out, comment the code below and try hitting /login.
* Now that you know it doesn't work without the piece of code below,
* uncomment it.
*/
server.route({
method: 'GET',
path: '/{path*}',
handler: {
directory: {
path: '.',
redirectToSlash: true,
index: true,
}
}
});
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [
{
module: 'good-console',
args: [{ request: '*', response: '*' }]
},
'stdout'
]
}
};
await server.register({
plugin: require('good'),
options,
});
await server.start();
console.log('Server running at:', server.info.uri);
};
init();
/routes/index.js
/**
* Use this for all paths since we just need to resolve index.html for any given path.
* react-router will take over and show the relevant component.
*
* TODO: add a 404 handler for paths not defined in react-router
*/
const fileHandler = {
handler: (req, res) => {
console.log(res.file('index.html'));
return res.file('index.html');
}
}
const routes = [
{ method: 'GET', path: '/login', config: fileHandler },
]
module.exports = routes;
The key thing to observe here is that for any named path (in this case /login) we always return the index.html file. For all other paths we tell hapi to return files from out of our build directory so that any references to *.css or *.js file in our index.hml will be resolved and we don't face a 404.
I'm not sure how react-router takes over the path resolution once index.html is loaded, but it beyond the scope of this question and is a topic of discussion for another time maybe.
As for the second question regarding hot-reload, I am still trying to figure it out. For now I am running both the hapi server and react-app independently, as I need /api for use in the react-app. Any suggestions or answers are welcome.
Here is how I did it. Tested it. Version "#hapi/hapi": "^20.0.1".
const path = require("path")
const Hapi = require('#hapi/hapi')
const Boom = require('#hapi/boom');
const server = Hapi.server({
port: 3000,
host: '0.0.0.0',
routes: {
files: {
relativeTo: path.join(__dirname, 'YOU BUILD REACT DIR')
}
}
});
(async () => {
await server.register([
require('vision'),
require('inert')
]);
server.route(
[{
method: 'GET',
path: '/{path*}',
options: {
ext: {
onPreResponse: {
method(req, h) {
//for other path prefix /Api
const isApi = req.path.substr(1)
.toLowerCase()
.trim()
.split('/')[0]
.replace(/\//g, "") === "api"
const response = req.response
if (response && req.response.output && req.response.output.statusCode === 404) {
if (isApi)
return Boom.notFound("Not Found")
return h.file('index.html');
}
return h.continue
},
}
}
},
handler: {
directory: {
path: ".",
listing: false,
index: true
}
}
},
{
method: 'GET',
path: '/Api/test',
handler: () => "OK"
}
])
await server.start();
console.log('Server running on %s', server.info.uri)
})()

Resources