Closed MarcoLeko closed 1 year ago
Thanks for this code already, do you also have a repository or somewhere simple where this reproduces?
Unfurtunately our project runs on a custom micro-frontend architecture, splittet across multiple private coporate repositories :/
Im sorry for this, usually having something reproducable eases up way more the bug finding process, but maybe you guys can tell from the first glance why getServerState
is unable to retrieve the InstantSearch
component from the webpack bundle
getServerState()
relies on a React Context to read from <InstantSearch>
. It's possible that you have duplicated versions of react-instantsearch-hooks
in your bundle. And therefore that the React Context reference changes, so getServerState()
is unable to read from the Context.
With just the code you sent it would be really hard, as I can't see how you're calling getServerState, is it equivalent to
but with Page instead of App?
I understand it's not easy to reproduce, but likely this is something to do with how the code is instantiated and would also happen without MFE architecture.
As a starter, this is our main server side rendering example in codesandbox, that you can edit to be closer to your version where it throws that error. It's also using webpack: https://codesandbox.io/s/heuristic-davinci-l7ckp8
Hey guys so I extended the bug report with the server part and more details to the entry component part of the code-base. The main differences that I can see from the codesandbox and our code base are mainly two points:
babel
only, while client part uses webpack
with babel-loader
I tried to make a code-sandbox wie a similar process to what we currently have, by dynamically requiring the client entry bundle in the server and pass it to getServerState
. Unfurtunately webpack is not able to read from bundle.js
, but you got hopefully the idea: https://codesandbox.io/s/pedantic-frost-0xgvk2?file=/src/server.js
Hi, not sure if it can fit your real architecture, but if you can import the unbundled App component in server.js it won't cause issues. https://codesandbox.io/s/zen-margulis-eu4vpe?file=/src/server.js:453-500
hmm it is really unfurtunate that it only works like this :/
The server part of our micro-frontend (called backend for frontend) is strictly decoupled from the client of the micro-frontend. Meaning importing something from the client is a violation. I think it would even mean, that as soon as we would import something from the client in the server part, we would create a client bundle in the server bundle of the application (bundle redundancy).
Do you have a guess why the getServerState is not able to retrieve the InstantSearch
component from a bundled javascript asset and only able to get it from the unbundled source entry file?
@MarcoLeko As mentioned in https://github.com/algolia/react-instantsearch/issues/3572#issuecomment-1191427446, this is because the Context variable reference changes from your client bundle to your server bundle.
You can find more informations on this problem on this StackOverflow thread.
Referring to your comment: Im wondering by checking our node_modules
there is only one version of react-instantsearch-hooks
in the node_modules
(Usually packages might have their own node_modules
, with specific versions of a package, but this is for us not the case). Just as a question are there some other ways to retrieve the serverState in the server and pass it down to the client?
export async function getRootComponentProps({ Fragment }) {
try {
serverState = await index.search('') // should replace getServerState; initial search needs to be in sync with the initialUiState prop of InstantSearch component
} catch (e) {
console.log(e);
}
return {
serverState,
};
}
const Page = ({ serverState }) => {
return (
<InstantSearchSSRProvider {...serverState}>
<InstantSearch searchClient={algoliaClient}>
<div>Marco</div>
</InstantSearch>
</InstantSearchSSRProvider>
);
};
Not that I know of. getServerState()
relies on rendering your app server-side to allow InstantSearch to initialize and retrieve the initial results.
Replicating that would probably require to:
Possibly I resolved a similar issue.
Are you using the useRouter()
hook on the app?
During the debugging of this error, I found the following error message additionaly:
error - TypeError: Cannot destructure property 'locale' of 'router' as it is null.
So I updated my code like this:
- const { locale } =useRouter()
+ const router = useRouter()
+ const { locale } = router || {}
After saving this update, the getServerState function works well.
In my thoughts, the useRouter
function will return null on the rendering process by the getServerState function.
Possibly I resolved a similar issue. Are you using the
useRouter()
hook on the app?During the debugging of this error, I found the following error message additionaly:
error - TypeError: Cannot destructure property 'locale' of 'router' as it is null.
So I updated my code like this:
- const { locale } =useRouter() + const router = useRouter() + const { locale } = router || {}
After saving this update, the getServerState function works well.
In my thoughts, the
useRouter
function will return null on the rendering process by the getServerState function.
Yeah, I have the same issue with useRouter. https://codesandbox.io/s/crazy-curie-sdeqdi?file=/components/Panel.tsx:250-283
I've also been running into this issue but I'm using Nextjs (useRouter is not the issue here). Has anyone found a solution? Update: turns out all I had to do was delete my yarn.lock
file and reinstall my node_modules
for it to work. Looks like babel version was updated, not sure exactly why that fixed it but if anyone is in the same boat try this! Might save you a couple of hours
Closing this issue, thanks for your update!
🐛 Bug description
I'm trying to implement algolia with SSR capabilities into our platform by doing a POC. There are a few steps that seems need to be done for SSR:
For us it is failing already on the first step, by throwing:
Unable to retrieve InstantSearch's server state in getServerState(). Did you mount the <InstantSearch> component?","stack":"Error: Unable to retrieve InstantSearch's server state in
getServerState(). Did you mount the <InstantSearch> component?\n at .../node_modules/react-instantsearch-hooks-server/dist/cjs/getServerState.js:100:13\n
What seems to be the case is that the
getServerState
can somehow not detect from the webpack bundle that there is aInstantSearch
component defined:Source code
entry.js
Click to expand!
```javascript const name = 'product-listing-fragment'; export function createFragment(RootComponent) { const Fragment = ({ rootComponentProps }) => { returnserver.js
Click to expand!
```javascript function getFragment(fragmentAssets) { // dynamically requires from entry point in dist folder const { Fragment } = require(path.join( paths.clientOutputDirectory, MODERN_BROWSER_STAGE || 'modern', getFragmentEntryPointFromAssets(fragmentAssets), )); return Fragment; } const Fragment = getFragment(assetsStaticConfigPerStage.$entrypoints); // path to dist entry point app.get('/', async (req, res) => { const rootComponentProps = await getRootComponentProps({ // here getServerState is called and serverState is part of rootComponentProps Fragment, }); const fragmentData = { ...someOtherData rootComponentProps: rootComponentProps || {}, }; const fragmentHtml = renderToString(createElement(Fragment, fragmentData)); return res .code(200) .send(fragmentHtml); }); } ```getRootComponentProps.js
Click to expand!
```javascript // a fragment can have initial data which can be fetched asynchronously // the params passed to this function are the query parameters export async function getRootComponentProps({ Fragment }) { let serverState = {}; try { serverState = await getServerState(JS-Bundle WITHOUT WEBPACK (BABEL ONLY) - getServerState is WORKING!
Click to expand!
```javascript "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Fragment = void 0; exports.createFragment = createFragment; var _reactInstantsearchHooksWeb = require("react-instantsearch-hooks-web"); var _lite = _interopRequireDefault(require("algoliasearch/lite")); var _jsxDevRuntime = require("react/jsx-dev-runtime"); var _jsxFileName = "/Users/lekoma/evelin/product-listing-fragment/src/server/entry.js"; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const name = 'product-listing-fragment'; function createFragment(RootComponent, getFragmentContext = () => ({}), initDependencies = () => null) { const Fragment = ({ rootComponentProps }) => { return /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(RootComponent, { ...rootComponentProps }, void 0, false, { fileName: _jsxFileName, lineNumber: 12, columnNumber: 16 }, this); }; async function init(rootElement) { const fragmentData = window?.Evelin?.data?.[name]; if (!fragmentData) { throw new Error(`Could not initialize ${name} because there was no fragment data for it inside Evelin.data. Got: ${fragmentData}`); } if (!rootElement) { throw new Error(`Could not initialize ${name} because no valid container element was given. Got: ${rootElement}`); } const { baseUrl, tenant, locale, sentry, rootComponentProps } = fragmentData; await initDependencies({ baseUrl, tenant, locale }); React.hydrate( /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(Fragment, { baseUrl: baseUrl, tenant: tenant, locale: locale, sentry: sentry, rootComponentProps: rootComponentProps }, void 0, false, { fileName: _jsxFileName, lineNumber: 34, columnNumber: 13 }, this), rootElement); } async function renderFn(rootElement, { baseUrl = '/', tenant, locale, sentry, rootComponentProps = {} }) { if (!rootElement) return; await initDependencies({ baseUrl, tenant, locale }); React.render( /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(Fragment, { baseUrl: baseUrl, tenant: tenant, locale: locale, rootComponentProps: rootComponentProps, sentry: sentry }, void 0, false, { fileName: _jsxFileName, lineNumber: 54, columnNumber: 13 }, this), rootElement); } return { Fragment, init, render: renderFn }; } const algoliaClient = (0, _lite.default)('CM7VJJA0AR', 'b65c925262ff32ce527aadd50da2bfe6'); const Page = ({ serverState }) => { return /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(_reactInstantsearchHooksWeb.InstantSearchSSRProvider, { ...serverState, children: /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(_reactInstantsearchHooksWeb.InstantSearch, { indexName: "loki_js_de_de_product", searchClient: algoliaClient, children: /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)("h1", { children: "Marco" }, void 0, false, { fileName: _jsxFileName, lineNumber: 74, columnNumber: 17 }, void 0) }, void 0, false, { fileName: _jsxFileName, lineNumber: 73, columnNumber: 13 }, void 0) }, void 0, false, { fileName: _jsxFileName, lineNumber: 72, columnNumber: 9 }, void 0); }; const { Fragment, init } = createFragment(Page); exports.Fragment = Fragment; ```JS-Bundle WITH WEBPACK & BABEL
Click to expand!
```javascript (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("react"), require("react-dom")); else if(typeof define === 'function' && define.amd) define(["react", "react-dom"], factory); else if(typeof exports === 'object') exports["Evelin"] = factory(require("react"), require("react-dom")); else root["Evelin"] = root["Evelin"] || {}, root["Evelin"]["fragments"] = root["Evelin"]["fragments"] || {}, root["Evelin"]["fragments"]["product-listing-fragment"] = factory(root["Evelin"]["fragments"]["react-base-fragment"]["React"], root["Evelin"]["fragments"]["react-base-fragment"]["ReactDom"]); })((typeof self != 'undefined' ? self : this), (__WEBPACK_EXTERNAL_MODULE_react__, __WEBPACK_EXTERNAL_MODULE_react_dom__) => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/@jsmdg/react-fragment-scripts/fragment/Fragment.jsx": /*!**************************************************************************!*\ !*** ./node_modules/@jsmdg/react-fragment-scripts/fragment/Fragment.jsx ***! \**************************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "createFragment": () => (/* binding */ createFragment) /* harmony export */ }); /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react-dom */ "react-dom"); /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react/jsx-dev-runtime */ "./node_modules/react/jsx-dev-runtime.js"); var _jsxFileName = "/Users/lekoma/evelin/product-listing-fragment/node_modules/@jsmdg/react-fragment-scripts/fragment/Fragment.jsx"; /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable import/no-dynamic-require */ const { name } = __webpack_require__(/*! ./package.json */ "./package.json"); function createFragment(RootComponent) { // eslint-disable-next-line react/prop-types const Fragment = ({ rootComponentProps }) => { return /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(RootComponent, { ...rootComponentProps }, void 0, false, { fileName: _jsxFileName, lineNumber: 10, columnNumber: 16 }, this); }; async function init(rootElement) { var _window, _window$Evelin, _window$Evelin$data; const fragmentData = (_window = window) === null || _window === void 0 ? void 0 : (_window$Evelin = _window.Evelin) === null || _window$Evelin === void 0 ? void 0 : (_window$Evelin$data = _window$Evelin.data) === null || _window$Evelin$data === void 0 ? void 0 : _window$Evelin$data[name]; if (!fragmentData) { throw new Error(`Could not initialize ${name} because there was no fragment data for it inside Evelin.data. Got: ${fragmentData}`); } if (!rootElement) { throw new Error(`Could not initialize ${name} because no valid container element was given. Got: ${rootElement}`); } const { baseUrl, tenant, locale, sentry, rootComponentProps } = fragmentData; (0, react_dom__WEBPACK_IMPORTED_MODULE_0__.hydrate)( /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(Fragment, { baseUrl: baseUrl, tenant: tenant, locale: locale, sentry: sentry, rootComponentProps: rootComponentProps }, void 0, false, { fileName: _jsxFileName, lineNumber: 30, columnNumber: 13 }, this), rootElement); } async function renderFn(rootElement, { baseUrl = '/', tenant, locale, sentry, rootComponentProps = {} }) { if (!rootElement) return; (0, react_dom__WEBPACK_IMPORTED_MODULE_0__.render)( /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(Fragment, { baseUrl: baseUrl, tenant: tenant, locale: locale, rootComponentProps: rootComponentProps, sentry: sentry }, void 0, false, { fileName: _jsxFileName, lineNumber: 48, columnNumber: 13 }, this), rootElement); } return { Fragment, init, render: renderFn }; } /***/ }), /***/ "./node_modules/@jsmdg/react-fragment-scripts/fragment/index.js": /*!**********************************************************************!*\ !*** ./node_modules/@jsmdg/react-fragment-scripts/fragment/index.js ***! \**********************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "createFragment": () => (/* reexport safe */ _Fragment__WEBPACK_IMPORTED_MODULE_0__.createFragment) /* harmony export */ }); /* harmony import */ var _Fragment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Fragment */ "./node_modules/@jsmdg/react-fragment-scripts/fragment/Fragment.jsx"); ... /************************************************************************/ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode. (() => { "use strict"; /*!*****************************!*\ !*** ./src/client/index.js ***! \*****************************/ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "Fragment": () => (/* binding */ Fragment), /* harmony export */ "init": () => (/* binding */ init) /* harmony export */ }); /* harmony import */ var _jsmdg_react_fragment_scripts_fragment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @jsmdg/react-fragment-scripts/fragment */ "./node_modules/@jsmdg/react-fragment-scripts/fragment/index.js"); /* harmony import */ var algoliasearch_lite__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! algoliasearch/lite */ "./node_modules/algoliasearch/dist/algoliasearch-lite.umd.js"); /* harmony import */ var algoliasearch_lite__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(algoliasearch_lite__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var react_instantsearch_hooks_web__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-instantsearch-hooks-web */ "./node_modules/react-instantsearch-hooks/dist/es/components/InstantSearchSSRProvider.js"); /* harmony import */ var react_instantsearch_hooks_web__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! react-instantsearch-hooks-web */ "./node_modules/react-instantsearch-hooks/dist/es/components/InstantSearch.js"); /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react/jsx-dev-runtime */ "./node_modules/react/jsx-dev-runtime.js"); var _jsxFileName = "/Users/lekoma/evelin/product-listing-fragment/src/client/index.js"; /* eslint-disable react/jsx-props-no-spreading, react/destructuring-assignment, react/prop-types, react-intl-auto/no-jsx-string-literals, import/no-default-export */ const algoliaClient = algoliasearch_lite__WEBPACK_IMPORTED_MODULE_2___default()('CM7VJJA0AR', 'b65c925262ff32ce527aadd50da2bfe6'); const Page = ({ serverState }) => { return /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(react_instantsearch_hooks_web__WEBPACK_IMPORTED_MODULE_3__.InstantSearchSSRProvider, { ...serverState, children: /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(react_instantsearch_hooks_web__WEBPACK_IMPORTED_MODULE_4__.InstantSearch, { indexName: "loki_js_de_de_product", searchClient: algoliaClient, children: /*#__PURE__*/(0, react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)("div", { children: "Marco" }, void 0, false, { fileName: _jsxFileName, lineNumber: 12, columnNumber: 17 }, undefined) }, void 0, false, { fileName: _jsxFileName, lineNumber: 11, columnNumber: 13 }, undefined) }, void 0, false, { fileName: _jsxFileName, lineNumber: 10, columnNumber: 9 }, undefined); }; const { Fragment, init } = (0, _jsmdg_react_fragment_scripts_fragment__WEBPACK_IMPORTED_MODULE_0__.createFragment)(Page); })(); /******/ return __webpack_exports__; /******/ })() }); }); }) ```[DISCLAIMER] The Webpack/Bable bundle is actually shortened because it contained all the code from algolia, which was absolete for this showcase.
💭 Expected behavior
getServerState
should be able to detect theInstantSearch
component and its initial uiState and perform the actual searchEnvironment
"react-instantsearch-hooks-server": "^6.30.2", "react-instantsearch-hooks-web": "^6.30.2",
Sorry in advance for the long snippets, webpack things 🤷🏻♂️