Skip to content

Commit a435fd7

Browse files
committed
Added rendering hooks
The renderer used by `render_component` can now be specified via the `renderer` argument. The default render now assumes access to an externally managed process which exposes react-render's API to POST requests. Rendering can now be toggled via the `RENDER` setting. This ensures that you don't need to run a render server in dev environments. Re markfinger#40
1 parent af17de0 commit a435fd7

9 files changed

Lines changed: 111 additions & 93 deletions

File tree

react/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33

44
class Conf(conf.Conf):
55
RENDER_URL = 'http://127.0.0.1:9009/render'
6+
RENDER = True
67

78
settings = Conf()

react/render.py

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,10 @@
11
import os
2-
import json
32
from optional_django import staticfiles
4-
from optional_django.serializers import JSONEncoder
5-
from .exceptions import ComponentSourceFileNotFound, ReactRenderingError
6-
from .server import server
3+
from .exceptions import ComponentSourceFileNotFound
4+
from .render_server import render_server
75

86

9-
class RenderedComponent(object):
10-
def __init__(self, markup, props):
11-
self.markup = markup
12-
self.props = props
13-
14-
def __str__(self):
15-
return self.markup
16-
17-
def __unicode__(self):
18-
return unicode(self.markup)
19-
20-
21-
def render_component(path, props=None, to_static_markup=None, json_encoder=None):
7+
def render_component(path, data=None, to_static_markup=False, renderer=render_server):
228
if not os.path.isabs(path):
239
abs_path = staticfiles.find(path)
2410
if not abs_path:
@@ -28,31 +14,4 @@ def render_component(path, props=None, to_static_markup=None, json_encoder=None)
2814
if not os.path.exists(path):
2915
raise ComponentSourceFileNotFound(path)
3016

31-
if json_encoder is None:
32-
json_encoder = JSONEncoder
33-
34-
if props is not None:
35-
serialized_props = json.dumps(props, cls=json_encoder)
36-
else:
37-
serialized_props = None
38-
39-
obj = server.render({
40-
'path': path,
41-
'serializedProps': serialized_props,
42-
'toStaticMarkup': to_static_markup
43-
})
44-
45-
markup = obj.get('markup', None)
46-
err = obj.get('error', None)
47-
48-
if err:
49-
if 'message' in err and 'stack' in err:
50-
raise ReactRenderingError(
51-
'Message: {}\n\nStack trace: {}'.format(err['message'], err['stack'])
52-
)
53-
raise ReactRenderingError(err)
54-
55-
if markup is None:
56-
raise ReactRenderingError('Render server failed to return markup. Returned: {}'.format(obj))
57-
58-
return RenderedComponent(markup, serialized_props)
17+
return renderer.render(path, data, to_static_markup)

react/render_server.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
import hashlib
3+
import requests
4+
from optional_django.serializers import JSONEncoder
5+
from .exceptions import ReactRenderingError
6+
from . import conf
7+
from .exceptions import RenderServerError
8+
9+
10+
class RenderedComponent(object):
11+
def __init__(self, markup, props):
12+
self.markup = markup
13+
self.props = props
14+
15+
def __str__(self):
16+
return self.markup
17+
18+
def __unicode__(self):
19+
return unicode(self.markup)
20+
21+
22+
class RenderServer(object):
23+
def __init__(self, url):
24+
self.url = url
25+
26+
def render(self, path, data=None, to_static_markup=False):
27+
if data is not None:
28+
props = json.dumps(data, cls=JSONEncoder)
29+
else:
30+
props = None
31+
32+
options = {
33+
'path': path,
34+
'serializedProps': props,
35+
'toStaticMarkup': to_static_markup
36+
}
37+
serialized_options = json.dumps(options)
38+
options_hash = hashlib.sha1(serialized_options.encode('utf-8')).hexdigest()
39+
40+
if not conf.settings.RENDER:
41+
return RenderedComponent('', props)
42+
43+
try:
44+
res = requests.post(
45+
self.url,
46+
data=serialized_options,
47+
headers={'content-type': 'application/json'},
48+
params={'hash': options_hash}
49+
)
50+
except requests.ConnectionError:
51+
raise RenderServerError('Could not connect to render server at {}'.format(self.url))
52+
53+
if res.status_code != 200:
54+
raise RenderServerError(
55+
'Unexpected response from render server at {} - {}: {}'.format(self.url, res.status_code, res.text)
56+
)
57+
58+
obj = res.json()
59+
60+
markup = obj.get('markup', None)
61+
err = obj.get('error', None)
62+
63+
if err:
64+
if 'message' in err and 'stack' in err:
65+
raise ReactRenderingError(
66+
'Message: {}\n\nStack trace: {}'.format(err['message'], err['stack'])
67+
)
68+
raise ReactRenderingError(err)
69+
70+
if markup is None:
71+
raise ReactRenderingError('Render server failed to return markup. Returned: {}'.format(obj))
72+
73+
return RenderedComponent(markup, props)
74+
75+
76+
render_server = RenderServer(conf.settings.RENDER_URL)

react/server.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ optional-django==0.3.0
33
webpack==4.1.1
44

55
# Dev dependencies
6-
js-host
76
nose==1.3.6
8-
django
7+
django
8+
mock==1.0.1

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
version=react.__version__,
77
packages=['react'],
88
install_requires=[
9-
'webpack==4.1.1',
109
'optional-django==0.3.0',
1110
],
1211
description='Server-side rendering, client-side mounting, JSX translation, and component bundling',

tests/perf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def run_perf_test():
2323
rendered_components.append(
2424
render_component(
2525
Components.PERF_TEST,
26-
props={'name': 'world'},
27-
to_static_markup=True
26+
{'name': 'world'},
27+
to_static_markup=True,
2828
)
2929
)
3030
end = time.time()

tests/test_django_integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ class TestDjangoIntegration(unittest.TestCase):
1313
def test_can_serialize_datetime_values_in_props(self):
1414
component = render_component(
1515
Components.HELLO_WORLD_JSX,
16-
props={
16+
{
1717
'name': 'world!',
1818
'datetime': datetime.datetime(2015, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
1919
'date': datetime.date(2015, 1, 2),
2020
'time': datetime.time(3, 4, 5),
21-
}
21+
},
2222
)
2323
deserialized = json.loads(component.props)
2424
self.assertEqual(

tests/test_rendering.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import unittest
22
from optional_django import six
3-
from react.render import render_component, RenderedComponent
3+
import mock
4+
from react.conf import Conf
5+
from react.render import render_component
6+
from react.render_server import RenderedComponent
47
from react.exceptions import ReactRenderingError, ComponentSourceFileNotFound
58
from .settings import Components
69

@@ -19,7 +22,7 @@ def test_can_render_a_component_in_jsx(self):
1922
def test_can_render_a_component_requiring_another_component(self):
2023
component = render_component(
2124
Components.HELLO_WORLD_JSX_WRAPPER,
22-
props={
25+
{
2326
'name': 'world!',
2427
'numbers': [1, 2, 3, 4, 5],
2528
},
@@ -40,7 +43,7 @@ def test_can_render_a_component_to_a_string_with_props(self):
4043
def test_render_component_returns_a_rendered_component(self):
4144
component = render_component(
4245
Components.HELLO_WORLD_JSX,
43-
props={
46+
{
4447
'name': 'world!'
4548
},
4649
to_static_markup=True,
@@ -54,7 +57,7 @@ def test_render_component_returns_a_rendered_component(self):
5457
def test_can_get_a_components_serialized_props(self):
5558
component = render_component(
5659
Components.HELLO_WORLD_JSX,
57-
props={
60+
{
5861
'name': 'world!',
5962
},
6063
)
@@ -73,16 +76,30 @@ def test_unserializable_props_raise_an_exception(self):
7376
TypeError,
7477
render_component,
7578
Components.HELLO_WORLD_JSX,
76-
props={'name': lambda: None}
79+
{'name': lambda: None}
7780
)
7881
self.assertRaises(
7982
TypeError,
8083
render_component,
8184
Components.HELLO_WORLD_JSX,
82-
props={'name': self}
85+
{'name': self}
8386
)
8487

8588
def test_missing_paths_throw_an_exception(self):
8689
self.assertRaises(ComponentSourceFileNotFound, render_component, '/path/to/nothing.jsx')
8790
# Ensure that relative paths are handled as well
88-
self.assertRaises(ComponentSourceFileNotFound, render_component, 'path/to/nothing.jsx')
91+
self.assertRaises(ComponentSourceFileNotFound, render_component, 'path/to/nothing.jsx')
92+
93+
def test_render_setting_is_respected(self):
94+
mock_settings = Conf()
95+
mock_settings.configure(RENDER=False)
96+
with mock.patch('react.conf.settings', mock_settings):
97+
rendered = render_component(
98+
Components.HELLO_WORLD_JSX,
99+
{'name': 'world!'},
100+
to_static_markup=True,
101+
)
102+
self.assertIsInstance(rendered, RenderedComponent)
103+
self.assertEqual(rendered.markup, '')
104+
self.assertEqual(str(rendered), '')
105+
self.assertEqual(rendered.props, '{"name": "world!"}')

0 commit comments

Comments
 (0)