-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Feeding data into universal rendering components #2090
Comments
This touches on the way the server currently communicates with React -- as you mention, by bootstrapping variables in jade templates. I was surprised when I saw this. This is preventing keystone from being a true SPA with no page reloads. Errors are handled by reloading the page with a Keystone.errors variable bootstrapped in. Why does it not grab the error from the response body of an ajax request and modify the DOM with the error message appropriately? Isn't this the point in React? The project seems to have half-adopted React as it is now. Also...all for redux. At the moment, the state store is bootstrapped variables in a script tag. We would need some other state store and redux is the way to go. Very keen to hear what @JedWatson thinks as I can work on this. |
Isn't that more to do with the incomplete API? (Also high on my priority list). I see the data bootstrapping as a separate issue from SPA support -- it just provides initial data, not all the data all the time. |
Yeh that's true for the List data. The exception would be transient data like |
Wow. Seems like that needs a fix. Is there an open issue tracking that? |
Not that I know of. Once I'm done with #1206 I will open one and hopefully get started on it. |
👍 |
About feeding data components: I have tried to use the universal rendering. There is one problem: when component start to render the keystone database isn't started yet and I get response 502 from the server. The data fetched successfully after render on the client. I don't know what's happened, but now starts normal. I use store-prototype to pass data. |
Data Injection RDDHere is my proposal to add Redux & data injection capabilities to the Universal Routing and Rendering support. Universal Route & Render with React & ReduxFor Universal JavaScript projects, simply pass a few options into your import list from 'components/ui/list';
import { connect } from 'react-redux';
export default React => {
const List = list(React);
const component = (props) => <List { ...props }/>;
const mapStateToProps = ({ teams, teamListClass, teamClass }) => ({
list: teams,
listClass: teamListClass,
itemClass: teamClass
});
return connect(mapStateToProps)(component);
}; You'll need reducers defined for those props ( import teams from './teams';
export default {
teams,
teamClass: (state = '') => state, // just use the initial state
teamListClass: (state = '') => state
}; Add the component to your routes in import React from 'react';
import { Router, Route } from 'react-router';
import createTeams from 'components/teams';
const createRoutes = (React) => {
// A route for a teams page
const Teams = createTeams(React);
return (
<Router>
<Route path='/teams' component={ Teams } />
</Router>
);
};
export default createRoutes; You could can use it by importing it into your import routes from './path/to/react-routes';
import reducers from './path/to/reducers';
// And in your Keystone.init() block:
Keystone.init({
'react routes': routes, // expects a factory that takes React and returns a React Router element.
'redux reducers': reducers, // expects an object { key1: reducer, key2: reducer }
'redux middleware', // optional
'react root', // optional DOM node ID to treat as render root. Default: keystone-wrapper
// ... Unit Testing Smart ComponentsIf you'd like to unit test your smart components, you'll need to create a store. To facilitate that, there's a new const createStore = keystone.createStore;
test('Children', assert => {
const msg = 'should render children';
const teamClass = 'team';
const initialState = {
teams: [
{
name: 'A Team',
id: '1'
}, {
name: `Charlie's Angels`,
id: 2
}, {
name: 'Team CoCo',
id: 3
}
],
teamClass,
teamListClass: 'team-list'
};
const store = createStore({ initialState, reducers });
const Teams = createTeams(React);
const el = (
<Teams store={ store } />
);
const $ = dom.load(render(el));
const expected = 3;
const actual = $(`.${ teamClass }`).length;
assert.equal(actual, expected, msg);
assert.end();
}); Building Your Client AppIn general, you're free to do whatever you want with your client app. Here's how to bootstrap it with the server-rendered data: import React from 'react';
import universal from 'keystone/universal/client';
import routes from './path/to/react-routes';
import reducers from './path/to/reducers';
// returns a function that must be invoked to trigger render
const app = universal({ React, routes, reducers });
// The app function will return your store so you can dispatch actions.
const store = app();
// Do stuff in your client app to trigger re-renders.
// e.g., subscribe to server updates, etc...
store.dispatch({
type: 'SET_TITLE',
title: 'Client render'
}); Redux MiddlewareYou may want to pass Redux middleware into the store. Here's how to do it on the server: Keystone.init({
'redux middleware': reduxMiddleware,
// ... And in the client app, simply add it to the call to import routes from './path/to/react-routes';
import reducers from './path/to/reducers';
import reduxMiddleware from `./path/to/redux-middleware';
// Add middleware here:
const app = universal({ React, routes, reducers, reduxMiddleware }); Injecting Data from the ServerData is made available to React components via the
Note: This bit needs discussion and fleshing out. I won't implement it in the first pass. Automatic Context DataSome data will be made available to your app's stores automatically:
|
Keystone 4 is going in maintenance mode. Expect no major change. see #4913 for details. |
I've been looking at how to compile data from the server and make it available for the new universal React routing & rendering capability that was recently added to 0.4.x.
That data should obviously include user data, relevant lists, etc... but we don't want to pass it explicitly through the component view hierarchy. We haven't discussed the topic in-depth, but the community seems to be converging around Redux and it's rapidly expanding ecosystem of developer tools. There's a really great tutorial course by @gaearon.
Currently, it appears we take a bit of a wild west approach to bootstrapping the client page with data from the server, with no consistent user-extensible mechanism that I can see to determine what data is intended to be delivered to the client.
It also seems that there are bits and pieces of it being written by different page views (e.g., via
viewLocals
and arbitrary data being shoved directly intorender()
calls). Additionally, our current React admin session store doesn't appear to be designed for universal rendering and the Node request/response cycle. This stuff hasn't been a huge issue so far because users define their own views and can essentially stuff any data they want anywhere they want, but that approach won't work with baked in support for universal rendering.Thankfully, both Express and React provide mechanisms that may be an effective path forward.
First, Express has a
res.locals
environment that is intended to be a safe way to gather data for the current request/response cycle, only. This is a good place to store things like authentication data, current user preferences, CSRF, "view locals", and so on. We should use it for its intended purpose, and create a special key that users of Keystone can easily extend with whatever custom data they need to generate on the server for delivery to the client.Second, React has a
context
mechanism that seems to be a good candidate for this stuff. It's a way of implicitly passing data down through the entire React component hierarchy as opposed to explicitly passing everything from parent to children inprops
.Receiving context is opt-in, and a React component must opt-in to the particular context keys that they're interested in. Additionally, Redux & React-Redux offer a simple way to pass store context into the app.
I'm thinking that we should create a special
res.locals.context
key that Keystone users can add to in their own middleware, and then we should ensure that it gets added to a Redux store and made available in the client bootstrap (currently we set a bunch of variables directly on aKeystone
object on the client page... we can do something similar, or keep doing that).I need a working mechanism to pass state into universal routes for both server and client rendering right now. I'm using it for language preferences and user data in the short-term. I'd love to hear your feedback.
The text was updated successfully, but these errors were encountered: