I keep getting the following error when attempting server side rendering using ReactJS & node.
React attempted to use reuse markup in a container but the checksum was invalid.
I've seen an answer that passing the same props on the server and client resolves this issue. In my example, I don't have any props, so I'm not sure that the answer applies to my problem.
You can view my full example on my github account.
I'm including the important code below. Any insight is greatly appreciated.
JSX
/** #jsx React.DOM */
var React = require('react');
var index = React.createClass({
render: function() {
return (
<html>
<head>
<script src="bundle.js"></script>
</head>
<body>
<div>
hello
</div>
</body>
</html>
);
}
});
if (typeof window !== "undefined") {
React.renderComponent(<index/>, document.documentElement);
} else {
module.exports = index;
}
Server
require('node-jsx').install();
var express = require('express'),
app = express(),
React = require('react'),
index = require('./index.jsx');
var render = function(req, res){
var component = new index();
var str = React.renderComponentToString(component);
res.set('Content-Type', 'text/html');
res.send(str);
res.end();
}
app.get('/',render);
app.use(express.static(__dirname));
app.listen(8080);
Change
React.renderComponent(<index/>, document.documentElement);
to
React.renderComponent(<index/>, document);
and the notice goes away.
Related
I'm trying to load Google Maps API using AngularJS:
<html data-ng-app="search-app">
<head data-ng-controller="GoogleMaps">
<script ng-src="{{mapsUrl}}" type="text/javascript"></script>
....
</head>
and controller for that part:
search.controller('GoogleMaps', [
'$scope','$sce',
function GoogleMaps($scope,$sce) {
var mapsUrl = '//maps.google.com/maps/api/js?sensor=false&key=my_api_key';
$scope.mapsUrl = $sce.trustAsResourceUrl(mapsUrl);
}
]);
but when the Google Map API is called within the search controller it throws and error
this.setMap is not a function
for
function CustomMarker(latlng, map, args) {
this.latlng = latlng;
this.args = args;
this.setMap(map);
}
but when I will replace {{mapsUrl}} with full URL in the HTML header it will works.
Any thoughts on that?
I have ended up appending URL to the header as a script on load event
function require(url, callback)
{
var element = document.createElement("script");
element.src = url;
element.type="text/javascript";
element.addEventListener('load', callback);
document.getElementsByTagName("head")[0].appendChild(e);
}
I am creating and clustering my markers with the markerclusterer library and this code:
function populateMapLocationData(locations) {
NgMap.getMap({id:'map'}).then(function(map) {
$scope.assetMap = map;
$scope.initMarkerClusterer(locations);
});
};
$scope.initMarkerClusterer = function(locations) {
$scope.bounds = new google.maps.LatLngBounds();
var markers = $scope.createMarker(locations);
var mcOptions = { imagePath: 'https://cdn.rawgit.com/googlemaps/js-marker-clusterer/gh-pages/images/m' };
var markerCluster = new MarkerClusterer($scope.assetMap, markers, mcOptions);
$scope.assetMap.fitBounds($scope.bounds);
$scope.assetMap.panToBounds($scope.bounds);
};
$scope.createMarker = function(location) {
var latLng = new google.maps.LatLng(parseFloat(location.lat), parseFloat(location.lang));
$scope.bounds.extend(latLng);
var marker = new google.maps.Marker({position: latLng, title: asset.name});
google.maps.event.addListener(marker, 'click', function() {
var infowindow = new google.maps.InfoWindow();
var center = new google.maps.LatLng( parseFloat(asset.lat), parseFloat(asset.lang) );
infowindow.setContent("content");
infowindow.setPosition(center);
infowindow.open($scope.assetMap);
google.maps.event.addListener($scope.assetMap, 'click', function(event) {
infowindow.close();
});
});
return marker;
};
And this works fine on the first iteration.
Come to hitting populateMapLocationData function again with new locations, the bounds change and the map centers and zooms to the new location of the new markers so I think it is working however all the previous markers are still there.
What I want to achieve is when I call populateMapLocationData with a new set of locations, clear the existing markers and update the map with new ones.
I have seen markers[key].setMap(null); can be used but I haven't had any success.
Any advice is appreciated, thanks
Actually, if you are using Google's original markerclusterer.js, to remove a marker you just need to use its MarkerClusterer.prototype.removeMarker function and to remove an array of markers you just use its MarkerClusterer.prototype.removeMarkers Luckily, ngMaps's markerclusterer.js is just a wrapper for the original.
More on that in Google's documentation
Ex:
vm.dynMarkers = [ {marker1}, {marker2}, ...] // your marker array
vm.markerClusterer = new MarkerClusterer(map, vm.dynMarkers, {}); // creater your marker cluster
vm.markerClusterer.removeMarkers(vm.dynMarkers); // remove all markers
I've made a plunker example for you to follow wherein I used the ngMaps example library as a base so that it's easier for you (make sure to use your own API key): https://plnkr.co/edit/RZNc2KLdO8qmW0o2Kimq?p=preview
Here's the embedded code as well:
var app = angular.module('myApp', ['ngMap']);
app.controller('mapController', function($http, $interval, NgMap) {
var vm = this;
vm.removeAllMarkers = removeAllMarkers;
vm.dynMarkers = [];
vm.map;
NgMap.getMap().then(function(map) {
for (var i = 0; i < 1000; i++) {
var latLng = new google.maps.LatLng(markers[i].position[0], markers[i].position[1]);
vm.dynMarkers.push(new google.maps.Marker({
position: latLng
}));
}
vm.markerClusterer = new MarkerClusterer(map, vm.dynMarkers, {});
vm.map = map;
});
function removeAllMarkers() {
vm.markerClusterer.removeMarkers(vm.dynMarkers);
vm.dynMarkers = [];
console.log('all markers in cluster removed');
}
});
map,
div[map] {
display: block;
width: 600px;
height: 400px;
}
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>Dynamic ngMap demo</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<script src="https://maps.google.com/maps/api/js?key=AIzaSyCD0rWgXRVBa7PgaTWXVp_gU51pgHGviYY"></script>
<script src="https://code.angularjs.org/1.3.15/angular.js"></script>
<script src="https://rawgit.com/allenhwkim/angularjs-google-maps/master/build/scripts/ng-map.js"></script>
<script src="https://rawgit.com/allenhwkim/angularjs-google-maps/master/testapp/scripts/markerclusterer.js"></script>
<script>
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = 'https://raw.githubusercontent.com/googlemaps/js-marker-clusterer/gh-pages/images/m'; //changed image path
</script>
<script src="https://rawgit.com/allenhwkim/angularjs-google-maps/master/testapp/scripts/markers.js"></script>
</head>
<body>
<h1>Marker Cluster</h1>
<hr />
<div ng-controller="mapController as vm">
<ng-map zoom="2" center="[43.6650000, -79.4103000]"></ng-map>
<button ng-click="vm.removeAllMarkers();" type="button">Remove All Markers</button>
</div>
</body>
</html>
Update 3
I have been using AngularJs for several years, and want to try ReactJs. With AngularJs I can define a directive and put the node inside the HTML DOM, like this:
<!DOCTYPE html>
<html>
<head>
....
</head>
<body>
<!--helloWorld is a directive-->
<hello-world></hello-world>
</body>
</html>
However, in React, to my knowledge, one needs to call
React.render(
<HelloWorld />,
targetElement);
to mount the component. Is there a way in React to mount the component automatically?
I have created a codepen here to show my idea as an experiment. The main piece of code is as below:
function R() {
var className = 'Hello';
if (window[className]) {
var elements = document.getElementsByTagName(className);
angular.forEach(elements, function (element) {
React.render(React.createElement(window[className]), element);
});
}
setTimeout(R, 50);
}
Every 50ms, we will check if the Class has been created. If it is I will render it under the element in the real DOM node <Hello> </Hello>.
*Warning: this code works only in trivial cases.
The JSX code to create react class:
<script type="text/jsx">
var Hello = React.createClass({
render: function() {
return <h1>Hello</h1>;
}
});
// React.render could be called here, like below:
React.render(
<Hello> </Hello>,
document.getElementById('example')
);
// but I want the <Hello> </Hello> stays in the html DOM. So that I don't
// repeat calling React.render. The first piece of code, will find the
// node and render the ReactClass there.
</script>
In html:
<Hello> </Hello>
I don't want to use setTimeout, but don't know if there are other approaches.
I don't really understand what you're trying to do but you could use EventEmitter
to bind a listener when you class is created.
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var myclassfactory= assign({}, EventEmitter.prototype, {
addListener: function(myclassName,callback) {
this.on(myclass,callback);
},
createClass: function(myclassName) {
//create your class ===>>>>>> React.CreateClass !!
this.emit(myclassName);
}
});
Then add a listener in your view:
myclassfactory.addListener(myclassName, render(myclassName));
var render = function(myclassName) {
React.render(myclassName, myelement);
}
I want to know how you set the title of each page with ReactJS. And more, I use react-router-component and i want use same tech to set title to each page at server side by using React.renderComponentToString.
My current root Component :
module.exports = App = React.createClass
getInitialState: ->
return title: 'My title'
render: ->
<html lang="fr">
<head>
<link rel="stylesheet" href="/assets/css/main.css" />
<script src="/assets/js/bundle.js"></script>
<title>{#state.title}</title>
</head>
<body>
<div id="body-content">
<div id="main-container">
<Content path={#props.path} />
</div>
</div>
</body>
</html>
And my Content component :
Content = React.createClass
render: ->
<Locations id="main" className="App" path={#props.path}>
<Location path="/" handler={MainPage} />
<Location path="/users/:username" handler={UserPage} />
</Locations>
Top-level React component
var React = require('react');
var AppStore = require('../stores/AppStore');
var Application = React.createClass({
propTypes: {
path: React.PropTypes.string.isRequired,
onSetTitle: React.PropTypes.func.isRequired
},
render() {
var page = AppStore.getPage(this.props.path);
this.props.onSetTitle(page.title);
return (
<div className="container">
<h1>{page.title}</h1>
<div return <div dangerouslySetInnerHTML={{__html: page.body}}>;
</div>
);
}
});
module.exports = Application;
Client-side startup script (entry point)
var React = require('react');
var Dispatcher = require('./core/Dispatcher');
var Application = require('./components/Application');
Dispatcher.register((payload) => {
var action = payload.action;
if (action.actionType === ActionTypes.CHANGE_LOCATION) {
app.setProps({path: action.path});
}
});
var app = React.createElement(Application, {
path: window.location.pathname,
onSetTitle: (title) => document.title = title
}));
app = React.render(app, document.body);
More info: https://gist.github.com/koistya/24715d295fbf710d1e24
Demo Project: https://github.com/kriasoft/react-starter-kit
to pass the title to your App component server side, you have to pass it the same way as you're passing the path, i.e. as props and not state.
So first you'll need to change:
<title>{#state.title}</title>
To:
<title>{#props.title}</title>
Then in your backend pass the wanted title to the App component when instantiating it, like so:
var url = require('url');
var ReactAsync = require('react-async');
var App = require('./path/to/your/root-component.jsx');
// app = your express app:
app.get('*', function (req, res) {
var path = url.parse(req.url).pathname;
var title = getTitleFromPath(path); // Made up function to get title from a path..
ReactAsync.renderComponentToStringWithAsyncState(
App({path: path, title: title}),
function (err, markup) {
if (err) return res.send(500, err);
res.send('<!DOCTYPE html>\n' + markup);
}
);
});
Hope that helps!
As for setting the title client side I think your solution with setting the document.title probably is the best option.
Update
I've now tested the above and the title is set correctly, however React gets a different checksum for the server generated component and the client generated one, which is no good:
Uncaught Error: Invariant Violation: You're trying to render a component to the document using server rendering but the checksum was invalid
So actually you shouldn't use this solution!
Will have to play around a little more...
I am using LinkedIn API and I need to load their login scripts when my user hits a certain route. However , from what I've read in stackoverflow it is not possible to just put the script elements inside a partial .
my code is straight forward :
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular-route.js">
...
<script src="http://platform.linkedin.com/in.js">
api_key: ...
authorize: true
onLoad: onLinkedInLoad
</script>
<script type="in/Login">
Hello, <?js= firstName ?> <?js= lastName ?>.
</script>
<script src="js/linkedinFuncs.js"></script>
</div>
The 3 last scripts (the linkedin ones) only needs to be included when the user hits the 'login' route . Any thoughts?
If anyone experiences this issue :
1)Make sure you load jquery BEFORE angular
2)Run the latest angular (this issue is resolved on 1.2.9 but was unresolved for me on 1.2.0)
You could load it from the code in your relevant congtroller programmatically by using something like this
(function injectScript() {
var src = 'http://platform.linkedin.com/in.js',
script = document.createElement('script');
script.async = true;
script.src = src;
var api_key = 'YOUR_KEY_HERE';
//script.authorize = true;
script.text = 'api_key: ' + api_key + '\n' + 'authorize: true\n';
script.onload = function() {
IN.Event.on(IN, 'systemReady', function() {
loadDeferred.resolve(IN);
});
};
document.getElementsByTagName('head')[0].appendChild(script);
})();