-
-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve async handling #30
Comments
I agree it would be nice, but it's actually impossible. Preact doesn't (and can't) know anything about side effects like redux or fetches done in That being said, outside the context of |
@developit I see a lot of isomorphic implementations struggling with this problem. Many seem to make it even more complex with specialized router integrations. Here is my current take on this. I basically leave everything to the app as if it would be rendered on the client, but expect a signal (action) that indicates that its now ready to be sent to the client side. The only thing I don't like is the fact that I have to call /**
* Import dependencies.
*/
import {h} from 'preact';
import render from 'preact-render-to-string';
import {Provider} from 'preact-redux';
import {applyMiddleware, createStore} from 'redux';
import {createEpicMiddleware} from 'redux-observable';
import {StaticRouter} from 'react-router';
import {IntlProvider} from 'react-intl';
/**
* Import local dependencies.
*/
import Root from './component';
import {rootReducer} from './reducer';
import rootEpic from './epic';
import {ROOT_STATE_READY_TO_RENDER} from './actions';
/**
* Export the promise factory.
*/
export default function (req) {
/**
* Create a new promise for the current request.
*/
return new Promise((resolve) => {
/**
* Injected reducer to process the "ready to render" action.
*/
let finalRender = true;
const serverReducer = (state = {}, action) => {
console.log('action => ', action);
if (action.type === ROOT_STATE_READY_TO_RENDER && finalRender) {
finalRender = false;
resolve(renderRoot());
}
return state;
};
/**
* Create the epic middleware.
*/
const epicMiddleware = createEpicMiddleware(rootEpic);
/**
* Create the store.
*/
let store = createStore(
rootReducer(serverReducer),
applyMiddleware(epicMiddleware)
);
/**
* Render the application.
*/
const renderRoot = () => {
let context = {};
let html = render(
<Provider store={store}>
<IntlProvider locale="en">
<StaticRouter location={req.url} context={context}>
<Root/>
</StaticRouter>
</IntlProvider>
</Provider>
);
let state = store.getState();
delete state._server_;
delete state.router;
return {context, html, state};
};
/**
* Initial render.
* TODO We are rendering twice on the server to trigger all actions and pre-loading exactly like on the client.
* TODO This could be reduced to a single render once this is resolved https://github.com/ReactTraining/react-router/issues/4407
*/
renderRoot();
});
} |
Ahh - you might be interested in the more advanced server renderer I've been pondering: let renderer = createRenderer();
renderer.render(
<Provider store={store}>
...
</Provider>
);
// later (async) after you get that even letting you know everything is loaded:
let html = renderer.flush(); // renders to string The main difference here is that this would actually be a full DOM renderer - it needs to mount, so it uses a lightweight DOM implementation. That might open up some really neat opportunities for caching within a tree, and it only serializes to HTML when you call Does that sound a bit more like what you're looking for? |
@BerndWessels I've created a JSFiddle demo showing what I described. It's a little less obvious what's going on since it's in a browser context, but this is a server-side renderer that you instantiate, and can call https://jsfiddle.net/developit/mLkwc1u3/ let renderer = createRenderer(<App />);
// as many times as you want:
setTimeout( () => {
console.log( renderer.html() );
}, 100); |
@developit Wow that sounds really cool and pretty close to what I was hoping for. Where does Or lets rephrase this, do you think this is a stable and performant solution for server side rendering? Is a headless DOM massively more overhead than render-to-string alone? |
Alright I just tested some things out via server-side-rendering-comparison and as I suspected - it's even faster than render-to-string! Over 25% faster! |
Here's what I used: |
@developit Awesome !!! Thanks you so much !!! |
@developit The implementation of |
Hi there - use the gist one, it's faster. I was only using hits to track className so instead I changed it to do that explicitly. The gist is the one I benchmarked, it should be quite solid. I will probably turn this into a proper module soon, but it requires some running around to get authorization for that :( |
Great, thank you so much - works fantastic for me now! |
this is cool |
Here is how I use all this for my server-side rendering. Basically I run the app exactly like I would on the client, only that on the server I inject an additional reducer that waits for Another thing I love about this is that I can use Server: /**
* Import dependencies.
*/
import {h} from 'preact';
import createRenderer from './preact-dom-renderer';
import render from 'preact-render-to-string';
import {Provider} from 'preact-redux';
import {applyMiddleware, createStore} from 'redux';
import {createEpicMiddleware} from 'redux-observable';
import {StaticRouter} from 'react-router';
import {addLocaleData, IntlProvider} from 'react-intl';
import {head, split} from 'ramda';
/**
* Import local dependencies.
*/
import Root from './component';
import {rootReducer} from './reducer';
import rootEpic from './epic';
import {ROOT_STATE_READY_TO_RENDER} from './actions';
import intlDE from 'react-intl/locale-data/de.js';
import intlEN from 'react-intl/locale-data/en.js';
import intlMessagesDE from '../public/assets/translations/de.json';
import intlMessagesEN from '../public/assets/translations/en.json';
/**
* Export the promise factory.
*/
export default function (req) {
/**
* Create a new promise for the current request.
*/
return new Promise((resolve) => {
/**
* Injected reducer to process the "ready to render" action.
*/
const serverReducer = (state = {}, action) => {
console.log('action => ', action);
if (action.type === ROOT_STATE_READY_TO_RENDER) {
let state = store.getState();
delete state._server_;
delete state.router;
resolve({context: context, html: renderer.html(), state});
}
return state;
};
/**
* Load the locale data for the users language.
*/
const locale = req.query.language ? head(split('-', req.query.language)) : 'de'; // TODO support en-gb fallback to en?
const locales = {
de: {data: intlDE, messages: intlMessagesDE},
en: {data: intlEN, messages: intlMessagesEN}
};
// Set the locale data.
addLocaleData(locales[locale].data);
/**
* Create the epic middleware.
*/
const epicMiddleware = createEpicMiddleware(rootEpic);
/**
* Create the store.
*/
let store = createStore(
rootReducer(serverReducer),
applyMiddleware(epicMiddleware)
);
/**
* Now we can run the app on the server.
* https://github.com/developit/preact-render-to-string/issues/30#issuecomment-288752733
*/
const renderer = createRenderer();
// Router context for capturing redirects.
let context = {};
// Run the app on the server.
renderer.render(
<Provider store={store}>
<IntlProvider locale={locale} messages={locales[locale].messages}>
<StaticRouter location={req.url} context={context}>
<Root/>
</StaticRouter>
</IntlProvider>
</Provider>
);
});
} Client: /**
* Import dependencies.
*/
import {h, render} from 'preact';
import {Provider} from 'preact-redux';
import {applyMiddleware, compose, createStore} from 'redux';
import {createEpicMiddleware} from 'redux-observable';
import createHistory from 'history/createBrowserHistory'
import {ConnectedRouter, routerMiddleware} from 'react-router-redux'
import {addLocaleData, IntlProvider} from 'react-intl';
import {head, split} from 'ramda';
/**
* Import local dependencies.
*/
import rootReducer from './reducer';
import rootEpic from './epic';
import intlDE from 'bundle-loader?lazy!react-intl/locale-data/de.js';
import intlEN from 'bundle-loader?lazy!react-intl/locale-data/en.js';
import intlMessagesDE from 'bundle-loader?lazy!../public/assets/translations/de.json';
import intlMessagesEN from 'bundle-loader?lazy!../public/assets/translations/en.json';
/**
* Load the locale data for the users language.
*/
function getQueryStringValue (key) {
return decodeURIComponent(window.location.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURIComponent(key).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1"));
}
// Get the users language.
const locale = getQueryStringValue('language') ? head(split('-', getQueryStringValue('language'))) : 'de'; // TODO support en-gb fallback to en?
const locales = {
de: {data: intlDE, messages: intlMessagesDE},
en: {data: intlEN, messages: intlMessagesEN}
};
console.log(locale);
// Load the locale data for the users language asynchronously.
locales[locale].data((localeData) => {
// Set the locale data.
addLocaleData(localeData);
// Load the locale messages for the users language asynchronously.
locales[locale].messages((localeMessages) => {
/**
* Create the browser history access.
*/
const history = createHistory();
/**
* Create the epic middleware.
*/
const epicMiddleware = createEpicMiddleware(rootEpic);
/**
* Create the store.
*/
let store;
if (process.env.NODE_ENV === 'development') {
// Development mode with Redux DevTools support enabled.
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Prevents Redux DevTools from re-dispatching all previous actions.
shouldHotReload: false
}) : compose;
// Create the redux store.
store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(routerMiddleware(history), epicMiddleware))
);
} else {
// Production mode.
store = window.__INITIAL_STATE__ ? createStore(
rootReducer,
window.__INITIAL_STATE__,
applyMiddleware(routerMiddleware(history), epicMiddleware)
) : createStore(
rootReducer,
applyMiddleware(routerMiddleware(history), epicMiddleware)
);
// TODO delete initial state for garbage collection?
}
/**
* Render the application.
*/
let root = document.body.lastElementChild;
const renderRoot = () => {
let Root = require('./component').default;
requestAnimationFrame(() => {
root = render(
<Provider store={store}>
<IntlProvider locale={locale} messages={localeMessages}>
<ConnectedRouter history={history}>
<Root/>
</ConnectedRouter>
</IntlProvider>
</Provider>,
document.body,
root
);
});
};
/**
* Enable hot module reloading in development mode.
*/
if (process.env.NODE_ENV === 'development') {
if (module.hot) {
// Handle updates to the components.
module.hot.accept('./component', () => {
console.log('Updated components');
renderRoot();
});
// Handle updates to the reducers.
module.hot.accept('./reducer', () => {
console.log('Updated reducers');
let rootReducer = require('./reducer').default;
store.replaceReducer(rootReducer);
});
// Handle updates to the epics.
module.hot.accept('./epic', () => {
console.log('Updated epics');
let rootEpic = require('./epic').default;
epicMiddleware.replaceEpic(rootEpic);
});
}
}
/**
* Finally render the app.
*/
renderRoot();
});
}); |
@BerndWessels wow, that is an amazingly complete example. Nice work, I am probably going to steal some of the ideas haha |
@developit You are welcome and thank you for your great work. The complete repo is here. |
@developit Awesome example rendering with undom, very helpful! Just a reminder for anyone using this on the server to remove the |
@Stanback good call! I'd actually recommend one slight change there - instead of just removing the element, un-render it via preact so everything gets nicely disconnected: tearDown: () => render(<nothing />, parent, root).remove() |
@developit Good suggestion 👍 I've updated my gist |
@Stanback Have you benchmarked your serializeHtml against @developit 's gist ? |
@BerndWessels It's basically the same code as he posted in the gist, but with some ES6 syntax. You can see both versions in the undom readme. I don't think there would be much performance difference - I made some changes since I needed to support void (aka. self-closing) tags (the code in his gist will render a line break as I've opened an issue here with regards to innerHTML, hopefully there can be some kind of supported plugin for html serialization in the future developit/undom#7 |
@Stanback Makes sense - I figured you had modified it to more closely match the output of |
After my initial suggestion I agree that implementing asynchronous rendering on the client is not feasible. Thus, I switched to doing it on the server side, based on the same principle, have a lifecycle method returning a Promise. I added a The readme explains it in more detail, it would be pointless to copy it here. |
@Satyam Looks great, I haven't had a chance to test but I'm planning to soon. I'm already using |
Hi
It would be good to have an interface like this:
So the render promise will only resolve once everything has been rendered and all async actions have been finished. This would make it easier to get the redux state too and you wouldn't have to get the redux state artificially before calling render (plus artificially figuring out the route n stuff - all the things that make the server side different from the client side).
Basically run the complete application server side until its stabilized and ready to render.
The text was updated successfully, but these errors were encountered: