markfinger / python-react

Server-side rendering of React components
MIT License
1.62k stars 116 forks source link

Possible to integrate with React Router? #40

Closed jrasanen closed 9 years ago

jrasanen commented 9 years ago

I'm not that familiar with Webpack yet, but would it be possible to use React Router somehow?

Like:

What would I put to react router's route handlers? "jsx__Signup" or equivalent?

Thanks!

markfinger commented 9 years ago

jsx_Signup would be equivalent to require('./jsx/Signup.jsx'), if that's any help.

I'm not that familiar with react-router, so I can't really help you with integration questions.

markfinger commented 9 years ago

If you want the app to function across the entire site, you'll need to bundle the entire thing. Though, the renderer will still need access to particular components, so it'll probably end up bundle the component again.

I'd suggest tinkering with webpack. Once you get into the realm of SPAs, you end up with a lot of moving parts, so you'll need to work out divisions and page structures.

jrasanen commented 9 years ago

Thanks! Previously I have been creating bundles for SPAs with browserify. I think I can achieve the same with webpack & umd/commonjs or just bundling react components together as you said. I'll try to remember to post the final solution here afterwards.

HorizonXP commented 9 years ago

Going to comment on this, since this is something I'd really like to incorporate.

Looking at react-router, it may make sense to wait until version 1.0.0 is released (it's on beta3 right now). They've updated their API to render routes directly via React.render. That said, using beta3, I tried to render it via python-react and obtained the following error:

ReactRenderingError at /
react: TypeError: undefined is not a function
    at autoGenerateWrapperClass (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react/lib/ReactDefaultInjection.js:53:19)
    at Object.getComponentClassForElement (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react/lib/ReactNativeComponent.js:59:49)
    at validatePropTypes (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react/lib/ReactElementValidator.js:361:45)
    at ReactElementValidator.createElement (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react/lib/ReactElementValidator.js:408:5)
    at Component.<anonymous> (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react-render/lib/Component.js:78:27)
    at Component.getFactory (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react-render/lib/Component.js:51:12)
    at Component._render (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react-render/lib/Component.js:68:8)
    at Component.renderToStaticMarkup (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react-render/lib/Component.js:92:8)
    at Object.reactRender (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/react-render/lib/index.js:39:15)
    at Func.call (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/lib/Func.js:43:23)
    at Host.callFunction (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/lib/Host.js:71:10)
    at Layer.handle [as handle_request] (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/route.js:131:13)
    at Route.dispatch (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/layer.js:95:5)
    at /Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:277:22
    at param (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:349:14)
    at paramCallback (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:401:21)
    at Host.functionLookup (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/lib/Host.js:54:5)
    at paramCallback (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:404:7)
    at param (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:384:5)
    at Function.process_params (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:410:3)
    at next (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/express/lib/router/index.js:271:10)
    at /Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/body-parser/lib/read.js:121:5
    at done (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/body-parser/node_modules/raw-body/index.js:233:14)
    at IncomingMessage.onEnd (/Users/xpatel/Development/BuildScience/portal/portal/node_modules/js-host/node_modules/body-parser/node_modules/raw-body/index.js:279:7)
    at IncomingMessage.g (events.js:199:16)
    at IncomingMessage.emit (events.js:104:17)
    at _stream_readable.js:908:16
    at process._tickCallback (node.js:355:11)

It looks like it's failing to create a factory from the react-router Router component? We may have to take this up with them, but I'd like to figure out what's required by python-react in order for this to work. It could be that because the Router component is somewhat virtual, that could be causing the issue.

markfinger commented 9 years ago

react-engine looks like it'd be the best solution to the problem.

js-host has been an interesting project, but I'm looking to cut it all out and delegate the JS stuff to specific servers.

python-webpack's dev branch has already shifted over to using webpack-build. Perf is better, it fixes a long-standing list of bugs, adds HMR, and is generally easier to debug without the fairly opaque js-host codebase.

On the python-react side of things, it's probably easiest to switch over to react-engine.

Re #42

markfinger commented 9 years ago

I believe that react-engine's built to support a fairly contained codebase, which may make integrations with python a little harder.

Might be easiest to just add a little express server to react-render, but to leave enough hooks that folks can use something like react-engine if they want.

TheoRyley commented 9 years ago

Might be easiest to just add a little express server to react-render, but to leave enough hooks that folks can use something like react-engine if they want.

This is what mic159 ended up doing in his fork (#28). It seems to work. https://github.com/mic159/react-render

markfinger commented 9 years ago

@TheoRyley

Yeah, I pursed something like that around the later stages of last year. python-react was stripped back to just rendering, lots of folks complained about boilerplate, having to deal with webpack directly, etc - so I ended up re-implementing some of it. See #24


Current musings...

Current dev branch on python-webpack supports pumping data into the config layer, so config files are actually config factories. The benefit of which is that you can interact with webpack from python and avoid the boilerplate of having to generate different config files for every bundle and every environment. Ex: https://github.com/markfinger/python-webpack/blob/73f0e600e4c071e72d94f918f9bdc444f67ed072/examples/example.webpack.js

I've got a few loose ends on the python-webpack side to sort out before I'll have time to look at python-react, but the basic flow I was aiming for was...

from webpack.compiler import webpack
from react.render import render_component

bundle = webpack(
    'path/to/config_file.js',
    extra_context={
        'component': '/path/to/component.jsx',
        'container': '#some-element',
        'props': 'reactProps'
    }
)

markup = render_component('/path/to/component.jsx', props={...})

By providing an example config file which covers some boilerplate and integrates the basic mount stuff that's currently in python-react, you could shift most of the complexity over to the webpack side.

The config'd look a little like:

module.exports = function(opts) {
  return {
    context: __dirname,
    entry: './mount.js',
    // ...
    resolve: {
      alias: {
        __react_component__: opts.context.component
      }
    },
    plugins: [
       new webpack.DefinePlugin({
         __react_mount_container__: opts.context.container,
         __react_mount_props_variable__: opts.context.propsVar
       }),
       // ...
    ],
    // ...
  };
};

mount.js would be something like

var React = require('react');
var Component = require('__react_component__');

var props = window.__react_mount_props_variable__;

var element = React.createElement(Component, props);

var container = document.getElementById(__react_mount_container__)

React.render(element, container);

In a template you'd pass in bundle, props, and markup

<div id="react-container">{{ markup }}</div>

<script>window.reactProps = {{ props }};</script>

{{ bundle.render_js }}

The above illustrates more boilerplate on the python side, but exposing the internals and flow of data should assist with folks understand what's happening. Plus it means that you can opt in to some convenience functions or simply interact with things at a low level.

The current system is not particularly flexible and the most common questions I get are only answered with "that's not possible". The above opens more scope for user error, but I'd rather deal with people breaking their setup over forcing an opinionated and limited workflow.

markfinger commented 9 years ago

The above setup also means that you can use react-hot-loader, which is a big plus.

markfinger commented 9 years ago

@HorizonXP it might be worth seeing if you can pass the current route in as a prop. Eg:

render_component('...', props={'route': '/foo/bar'})
jrasanen commented 9 years ago

Oh yeah, I by the way ended up using https://github.com/larrymyers/react-mini-router I pass the current route to it using a context processor. Works well.

Mini router provides a mixin and you can just pass the state to it with "path" parameter.

TheoRyley commented 9 years ago

Mini router provides a mixin and you can just pass the state to it with "path" parameter.

React Router 1.0 now provides similar functionality using a Location object: http://rackt.github.io/react-router/tags/v1.0.0-beta3.html#Server%20Rendering

I wonder, though, for a SPA if there is any reason to use python-react rather than using your own express server or something like react-engine directly.

markfinger commented 9 years ago

@TheoRyley It usually depends on the scope of the project and what you'll need to integrate with.

My own stuff tends to be a simple setup with express and a tiny jade template which gets fed markup strings and some assets that mount themselves.

I'm also working with a shop that's pretty die-hard about using python+django for everything, which is why all this stuff exists. Most of the complexity in the project is in response to folk who dislike JS and are offended by the idea of a non-python server :unamused:

Outside of when I'm working with python folk, webpack-build's prob the only part of this stack that I really use. I usually just pump a request into it before serving a page up. The nice stuff that keeps me from using webpack's CLIs or API:

HorizonXP commented 9 years ago

So I managed to get this to work, but I had to modify a bit of the code on the Python side and on the Javascript side. I forked both of your repos in order to make my modifications.

It's not pretty, nor is it best-practices, but it works, for now.

https://github.com/HorizonXP/python-react-router https://github.com/HorizonXP/react-router-render

markfinger commented 9 years ago

1.0.0 adds support for overriding the default renderer and has removed the js-host integration.

You can define a renderer to be used via the renderer arg

render_component(
    # ...
    renderer=my_renderer,
)

If you need to pass weird data down without influencing the props, just use a nested dict and have your renderer pass the proper structure along to whatever lib you're using.

render_component(
    # ...
    props={
        'route': '/foo/bar',
        'props': {
            # ...
        },
    },
    renderer=my_renderer,
)

If you want to avoid replicating the renderer arg everywhere, just compose a high-order function that wraps render_component and point all your calls at that.

Regarding the server setup, without js-host you can use whatever you want. So maybe react-engine would be useful, otherwise @HorizonXP's react-router-render should work