Bit of an odd situation here - I have a website written in Vue and I want to demo a library I've written in react. I can avoid server side rendering (SSR) by wrapping ReactDOM.hydrate(ReactApp, document.getElementById('react'area')) but I don't want to do that. I want to render everything SSR, but I don't see how it's possible.
Here is my renderOnServer.js for vue:
process.env.VUE_ENV = 'server'
const fs = require('fs')
const path = require('path')
const filePath = './App/dist/server.js'
const code = fs.readFileSync(filePath, 'utf8')
const vue_renderer = require('vue-server-renderer').createBundleRenderer(code)
//prevent XSS attack when initialize state
var serialize = require('serialize-javascript')
var prerendering = require('aspnet-prerendering')
module.exports = prerendering.createServerRenderer(function (params) {
return new Promise((resolve, reject) => {
const context = {
url: params.url,
absoluteUrl: params.absoluteUrl,
baseUrl: params.baseUrl,
data: params.data,
domainTasks: params.domainTasks,
location: params.location,
origin: params.origin,
xss: serialize("</script><script>alert('Possible XSS vulnerability from user input!')</script>")
}
const serverVueAppHtml = vue_renderer.renderToString(context, (err, _html) => {
if (err) { reject(err.message) }
resolve({
globals: {
html: _html,
__INITIAL_STATE__: context.state
}
})
})
})
});
So basically I'm configuring SSR above to read server.js:
import { app, router, store } from './app'
export default context => {
return new Promise((resolve, reject) => {
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject(new Error({ code: 404 }))
}
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({ store, context })
}
}))
.then(() => {
context.state = store.state
resolve(app)
})
.catch(reject)
}, reject)
})
}
and server.js above is just looking for the right vue component and rendering. I have a test react component:
import React from 'react'
export default class ReactApp extends React.Component {
render() {
return (
<div>Hihi</div>
)
}
}
and my vue component:
<template>
<div id="page-container">
<div id="page-content">
<h3 class="doc-header">Demo</h3>
<div id="react-page">
</div>
</div>
</div>
</template>
<script>
<script>
import ReactApp from './ReactApp.jsx'
import ReactDOM from 'react-dom'
export default {
data() {
return {
}
},
}
ReactDOM.hydrate(ReactApp, document.getElementById('#react-page'))
</script>
But obviously it won't work because I can't use document in SSR.
Basically, the purpose of hydrate is to match react DOM rendered in browser to the one that came from the server and avoid extra render/refresh at load.
As it was pointed in the comments hydrate should be used on the client-side and React should be rendered on the server with renderToString.
For example, on the server it would look something like this:
const domRoot = (
<Provider store={store}>
<StaticRouter context={context}>
<AppRoot />
</StaticRouter>
</Provider>
)
const domString = renderToString(domRoot)
res.status(200).send(domString)
On the client:
<script>
const domRoot = document.getElementById('react-root')
const renderApp = () => {
hydrate(
<Provider store={store}>
<Router history={history}>
<AppRoot />
</Router>
</Provider>,
domRoot
)
}
renderApp()
</script>
Technically, you could render React components server-side and even pass its state to global JS variable so it is picked up by client React and hydrated properly. However, you will have to make a fully-featured react rendering SSR support(webpack, babel at minimum) and then dealing with any npm modules that are using window inside (this will break server unless workarounded).
SO... unless it is something that you can't live without, it is easier, cheaper and faster to just render React demo in the browser on top of returned Vue DOM. If not, roll up your sleeves :) I made a repo some time ago with react SSR support, it might give some light on how much extra it will be necessary to handle.
To sum everything up, IMO the best in this case would be to go with simple ReactDOM.render in Vue component and avoid React SSR rendering:
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="my-compiled-react-bundle.js"></script>
<script>
function init() {
ReactDOM.render(ReactApp, document.getElementById('#react-page'))
}
init();
</script>
Related
I am using parcel as a bundler in React.js project.
How to load npm modules asynchronously in react.js?
There is only one page that uses one specific npm module so I didn't need to load it at first loading.
By avoiding this, I would like to reduce the bundle size.
Could you let me the proper way to do this?
========================
And also, if I understood anything wrongly about the bundle size optimization and lazy loading, please let me know.
By using Dynamic Import you may import the package when you really need the package.
You can use a dynamic import inside an useEffect hook like:
const Page = (props) => {
useEffect(
() => {
const [momentjsPromise, cancel] = makeCancelable(import("moment"));
momentjsPromise.then((momentjs) => {
// handle states here
});
return () => {
cancel?.();
};
},
[
/* don't forget the dependencies */
],
);
};
You can use dynamic imports.
Let's say you want to import my-module:
const Component = () => {
useEffect(() => {
import('my-module').then(mod => {
// my-module is ready
console.log(mod);
});
}, []);
return <div>my app</div>
}
Another way is to code-splitt Component itself:
// ./Component.js
import myModule from 'my-module';
export default () => <div>my app</div>
// ./App.js
const OtherComponent = React.lazy(() => import('./Component'));
const App = () => (
<Suspense>
<OtherComponent />
<Suspense>
);
my-module will be splitted along with Component.
These two patterns should work with any bundler, but it will work client side only.
How would I be able to test the router in the code below? When using React you are able to use MemoryRouter to pass initialEntries to mock a route change but I cannot find an alternative for preact-router. I looked at the Preact docs and the preact-router docs but I am unable to find a clear solution.
import 'preact/debug';
import { h, render } from 'preact';
import HomePage from './pages/homepage';
import Router from 'preact-router';
import AsyncRoute from 'preact-async-route';
import './styles/index.scss';
const App = () => (
<Router>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
export default App;
This is a little old, but I figured I would share what I found.
The first and quickest thing to do is to just use the route function in preact-router.
import { render, route } from 'preact-router';
import App from './App';
describe('<App/>', () => {
it('renders admin', async () => {
const { container, findByText } = render(<App/>);
// Go to admin page
route('/admin');
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
});
While this works, I don't like that it relies on the brower's real history. Luckily, the <Router> component accepts a history prop of type CustomHistory. So you can use an in-memory implementation of a History API to make this happen. I think I've seen docs that suggest using the history package - however I had to make an adjustment
import { createMemoryHistory } from 'history';
class MemoryCustomHistory {
constructor(initialEntries = undefined) {
this.wrapped = createMemoryHistory({initialEntries});
}
get location() {
return this.wrapped.location;
}
// Listen APIs not quite compatible out of the box.
listen(callback) {
return this.wrapped.listen((locState) => callback(locState.location));
}
push(path) {
this.wrapped.push(path);
}
replace(path) {
this.wrapped.replace(path);
}
}
Next, update your app to accept a history property to pass to the <Router>
const App = ({history = undefined} = {}) => (
<Router history={history}>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
Finally, just update the tests to wire your custom history to the app.
it('renders admin', async () => {
const history = new MemoryCustomHistory(['/admin]);
const { container, findByText } = render(<App history={history}/>);
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
Here's my lazy component:
const LazyBones = React.lazy(() => import('#graveyard/Bones')
.then(module => ({default: module.BonesComponent}))
export default LazyBones
I'm importing it like this:
import Bones from './LazyBones'
export default () => (
<Suspense fallback={<p>Loading bones</p>}>
<Bones />
</Suspense>
)
And in my test I have this kind of thing:
import * as LazyBones from './LazyBones';
describe('<BoneYard />', function() {
let Bones;
let wrapper;
beforeEach(function() {
Bones = sinon.stub(LazyBones, 'default');
Bones.returns(() => (<div />));
wrapper = shallow(<BoneYard />);
});
afterEach(function() {
Bones.restore();
});
it('renders bones', function() {
console.log(wrapper)
expect(wrapper.exists(Bones)).to.equal(true);
})
})
What I expect is for the test to pass, and the console.log to print out:
<Suspense fallback={{...}}>
<Bones />
</Suspense>
But instead of <Bones /> I get <lazy /> and it fails the test.
How can I mock out the imported Lazy React component, so that my simplistic test passes?
I'm not sure this is the answer you're looking for, but it sounds like part of the problem is shallow. According to this thread, shallow won't work with React.lazy.
However, mount also doesn't work when trying to stub a lazy component - if you debug the DOM output (with console.log(wrapper.debug())) you can see that Bones is in the DOM, but it's the real (non-stubbed-out) version.
The good news: if you're only trying to check that Bones exists, you don't have to mock out the component at all! This test passes:
import { Bones } from "./Bones";
import BoneYard from "./app";
describe("<BoneYard />", function() {
it("renders bones", function() {
const wrapper = mount(<BoneYard />);
console.log(wrapper.debug());
expect(wrapper.exists(Bones)).to.equal(true);
wrapper.unmount();
});
});
If you do need to mock the component for a different reason, jest will let you do that, but it sounds like you're trying to avoid jest. This thread discusses some other options in the context of jest (e.g.
mocking Suspense and lazy) which may also work with sinon.
You don't need to resolve lazy() function by using .then(x => x.default) React already does that for you.
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component. React code splitting
Syntax should look something like:
const LazyBones = React.lazy(() => import("./LazyBones"))
Example:
// LazyComponent.js
import React from 'react'
export default () => (
<div>
<h1>I'm Lazy</h1>
<p>This component is Lazy</p>
</div>
)
// App.js
import React, { lazy, Suspense } from 'react'
// This will import && resolve LazyComponent.js that located in same path
const LazyComponent = lazy(() => import('./LazyComponent'))
// The lazy component should be rendered inside a Suspense component
function App() {
return (
<div className="App">
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
</div>
)
}
As for Testing, you can follow the React testing example that shipped by default within create-react-app and change it a little bit.
Create a new file called LazyComponent.test.js and add:
// LazyComponent.test.js
import React, { lazy, Suspense } from 'react'
import { render, screen } from '#testing-library/react'
const LazyComponent = lazy(() => import('./LazyComponent'))
test('renders lazy component', async () => {
// Will render the lazy component
render(
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
)
// Match text inside it
const textToMatch = await screen.findByText(/I'm Lazy/i)
expect(textToMatch).toBeInTheDocument()
})
Live Example: Click on the Tests Tab just next to Browser tab. if it doesn't work, just reload the page.
You can find more react-testing-library complex examples at their Docs website.
I needed to test my lazy component using Enzyme. Following approach worked for me to test on component loading completion:
const myComponent = React.lazy(() =>
import('#material-ui/icons')
.then(module => ({
default: module.KeyboardArrowRight
})
)
);
Test Code ->
//mock actual component inside suspense
jest.mock("#material-ui/icons", () => {
return {
KeyboardArrowRight: () => "KeyboardArrowRight",
}
});
const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
{<myComponent>}
</Suspense>);
const componentToTestLoaded = await componentToTest.type._result; // to get actual component in suspense
expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");
This is hacky but working well for Enzyme library.
To mock you lazy component first think is to transform the test to asynchronous and wait till component exist like:
import CustomComponent, { Bones } from './Components';
it('renders bones', async () => {
const wrapper = mount(<Suspense fallback={<p>Loading...</p>}>
<CustomComponent />
</Suspense>
await Bones;
expect(wrapper.exists(Bones)).toBeTruthy();
}
I was wondering if anyone else has had the problem where they have created a Server side rendering NodeJS app which works perfectly locally but then doesn't load server side once deployed to heroku.
I have created an app using Jared Palmer's awesome RazzleJS in combination with Redux, React Router and React Router Config.
The way it works is that in my server.js file I check the component that is loading for a static function called fetchData, if the function exists then the function is run which is a promise based request to an API using axios in a thunk.
In my server.js file another function then runs that checks all promises have completed before finally rendering the HTML for the page.
Locally this works perfectly and even with Javascript disabled the page is loading complete with the data fetched.
Having deployed this to heroku (single dyno - hobby plan) however if I disable javascript the page is loading with the data missing, suggesting that page is being rendered before the promises resolve. The data is then being loaded correctly using the equivalent ComponentDidMount dispatch for the data.
I currently have the following code:
Server.js
function handleRender(req, res) {
const sheet = new ServerStyleSheet();
const store = createStore(rootReducer, compose(applyMiddleware(thunk)));
const branch = matchRoutes(Routes, req.url);
const promises = branch.map(({ route }) => {
let fetchData = route.component.fetchData;
return fetchData instanceof Function
? fetchData(store, req.url)
: Promise.resolve(null);
});
return Promise.all(promises).then(() => {
const context = {};
const html = renderToString(
sheet.collectStyles(
<Provider store={store}>
<StaticRouter context={context} location={req.url}>
{renderRoutes(Routes)}
</StaticRouter>
</Provider>
)
);
const helmet = Helmet.renderStatic();
const styleTags = sheet.getStyleTags();
const preloadedState = store.getState();
if (context.url) {
res.redirect(context.url);
} else {
res
.status(200)
.send(renderFullPage(html, preloadedState, styleTags, helmet));
}
});
}
Example React Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProductData } from '../thunks/product-data';
class Test extends Component {
static fetchData(store, url) {
store.dispatch(fetchProductData());
}
componentDidMount() {
if(this.props.productData.length === 0 ) {
this.props.fetchProductData() // Successfully fetches the data
}
}
render() {
return (
<div>
{ this.props.productData && this.props.productData.map( (product, i) => {
return <div key={i}>{product.title}</div>
})}
</div>
);
}
}
const mapStateToProps = state => {
return {
productData: state.productData
}
};
const mapDispatchToProps = dispatch => {
return {
fetchProductData(){
dispatch(fetchProductData());
}
}
};
export const TestContainer = connect(mapStateToProps, mapDispatchToProps)(Test);
This is just an example of the component layout as the ones I actually have are quite complex but in this instance productData would be set to [] in defaultState.
Also all reducers and actions are working correctly locally it is only when deployed to Heroku on a hobby plan that the server side rendering seems to no longer work?
So after a morning of research the reason that it wasn't working in my live environment was because I have a HOC wrapping component for the purposes of tracking analytics.
React-router-config however can't handle the fact that the fetchData function is another level deeper in the hierarchy and so all my promises were resolved with null.
Now that I have removed the HOC component again, server side rendering is once again working properly :)
I am following this tutorial: https://crypt.codemancers.com/posts/2017-06-03-reactjs-server-side-rendering-with-router-v4-and-redux/ which i think is the 'standard' way of doing server side rendering in react (?).
Basically what happens is i use react router (v4) to make a tree of all the components that are about to get rendered:
const promises = branch.map(({ route }) => {
return route.component.fetchInitialData
? route.component.fetchInitialData(store.dispatch)
: Promise.resolve();
});
Wait for all those promises to resolve and then call renderToString.
In my components i have a static function called fetchInitialData which looks like this:
class Users extends React.Component {
static fetchInitialData(dispatch) {
return dispatch(getUsers());
}
componentDidMount() {
this.props.getUsers();
}
render() {
...
}
}
export default connect((state) => {
return { users: state.users };
}, (dispatch) => {
return bindActionCreators({ getUsers }, dispatch);
})(Users);
And all this works great except that getUsers is called both on the server and the client.
I could of course check if any users are loaded and not call getUsers in componentDidMount but there must be a better, explicit way to not make the async call twice.
After getting more and more familiar with react i feel fairly confident i have a solution.
I pass a browserContext object along all rendered routes, much like staticContext on the server. In the browserContext i set two values; isFirstRender and usingDevServer. isFirstRender is only true while the app is rendered for the first time and usingDevServer is only true when using the webpack-dev-server.
const store = createStore(reducers, initialReduxState, middleware);
The entry file for the browser side:
const browserContext = {
isFirstRender: true,
usingDevServer: !!process.env.USING_DEV_SERVER
};
const BrowserApp = () => {
return (
<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes, { store, browserContext })}
</BrowserRouter>
</Provider>
);
};
hydrate(
<BrowserApp />,
document.getElementById('root')
);
browserContext.isFirstRender = false;
USING_DEV_SERVER is defined in the webpack config file using webpack.DefinePlugin
Then i wrote a HOC component that uses this information to fetch initial data only in situations where it is needed:
function wrapInitialDataComponent(Component) {
class InitialDatacomponent extends React.Component {
componentDidMount() {
const { store, browserContext, match } = this.props;
const fetchRequired = browserContext.usingDevServer || !browserContext.isFirstRender;
if (fetchRequired && Component.fetchInitialData) {
Component.fetchInitialData(store.dispatch, match);
}
}
render() {
return <Component {...this.props} />;
}
}
// Copy any static methods.
hoistNonReactStatics(InitialDatacomponent, Component);
// Set display name for debugging.
InitialDatacomponent.displayName = `InitialDatacomponent(${getDisplayName(Component)})`;
return InitialDatacomponent;
}
And then the last thing to do is wrap any components rendered with react router with this HOC component. I did this by simply iterating over the routes recursively:
function wrapRoutes(routes) {
routes.forEach((route) => {
route.component = wrapInitialDataComponent(route.component);
if (route.routes) {
wrapRoutes(route.routes);
}
});
}
const routes = [ ... ];
wrapRoutes(routes);
And that seems to do the trick :)