Let only one page be server-side rendered with Meteor - reactjs

My Meteor/React application should render one static page besides the reactive one pagers with reactive UIs. The static package does not even need to be "hydrated" with the React magic after displayed in the browser. Though the server-side rendering on the server will be dynamic with React components.
I got it working, but I'm not sure if it is the intended official way to do it.
File import/client/routes.js
...
<Route path="/reactive/pages/:id" component={ReactiveComponent} />
<Route path="/static_url" />
...
File server/main.jsx
...
onPageLoad((sink) => {
if (sink.request.path === '/static_url) {
sink.renderIntoElementById('app', renderToString(
<StaticPage />,
));
}
});
...
File client/main.js
...
import { Routes } from '../imports/client/routes';
Meteor.startup(() => {
...
if (window.location.pathname !== '/offer_pdf') {
render(Routes, document.getElementById('app'));
}
});
...
Especially when rendering dependent on the URI, it seems a little bit hacky to me. Does a more elegant solution exist?

I don't think there is anything official, but in general, of course, it's a good idea to use a router for rendering different pages, so I thought it worth pointing out that you can use react-router on the server as well:
import React from "react";
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";
import { StaticRouter } from 'react-router-dom';
import App from '/imports/ui/app.jsx';
onPageLoad(sink => {
const context = {};
sink.renderIntoElementById("app", renderToString(
<StaticRouter location={sink.request.url} context={context}>
<App />
</StaticRouter>
));
/* Context is written by the routes in the app. The NotFound route, used
when, uhm, no route is found, sets the status code. Here we set it on the
HTTP response to get a hard 404, not just a soft 404. Important for Google bot.
*/
context.statusCode && sink.setStatusCode(context.statusCode);
// add title to head of document if set by route
sink.appendToHead(`<title>${context.title || 'My page'}</title>`);
});
In App you can then use the usual Switch and Route tags for specifying different routes. In that you could, for instance, only specify routes that you want to be server-rendered.

Related

Gatsby wildcard route

Is it not possible to route to the same component with a wildcard path?
If in React I have something like:
<Router>
<Switch>
<Route path="/path/:id" children={<Component />} />
</Switch>
</Router>
all the requests:
/path/123
/path/123/p
/path/123/p/1
will route to the same /path/123
How can I tell Gatsby to do the same?
createPage({
path: `/path/123/*`,
component,
context
})
Or what is the solution to this problem, a redirect engine of some sorts?
I think you are looking for client-only routes. Given a page (or template if it's created from gatsby-node.js) you can:
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/Layout"
import SomeComponent from "../components/SomeComponent"
const App = () => {
return (
<Layout>
<Router basepath="/app">
<SomeComponent path="/path" />
</Router>
</Layout>
)
}
export default App
Note: assuming a src/pages/app/[...].js page (File System Route API structure).
When a page loads, Reach Router looks at the path prop of each component nested under <Router />, and chooses one to render that best matches window.location (you can learn more about how routing works from the #reach/router documentation).
Alternatively, you can use an automated approach (plugin: gatsby-plugin-create-client-paths) by:
{
resolve: `gatsby-plugin-create-client-paths`,
options: { prefixes: [`/path/*`] },
},
Which will validate all routes under /path.
Or for a more customizable approach, in your gatsby-node.js:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/path/)) {
page.matchPath = "/path/*"
// Update the page.
createPage(page)
}
}
Disclaimer: These routes will exist on the client only and will not correspond to index.html files in an app’s built assets. If you’d like site users to be able to visit client routes directly, you’ll need to set up your server to handle those routes appropriately.

Refresh or manual URL typing in SSR React App changes a className

The App was initially made with CRA and converted into SSR. The app works fine but the problem arises only when I refresh the app or manually type in the URL to a certain page. The middle div's classname is then overridden to the classname of the middle div of the base route.
It changes for example from this:
<div id="root">
<div class="header">..</div>
<div class="newlist">..</div>
<div class="footer">..</div>
</div>
to this:
<div id="root">
<div class="header">..</div>
<div class="main-flex-container">..</div>
<div class="footer">..</div>
</div>
For extra information here is my src/index.js:
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import configureStore, { history } from './redux/store/index';
import AppRouter from './routers/AppRouter';
import './styles/styles.scss';
const store = configureStore();
require('dotenv').config();
const isServer = typeof window !== 'undefined';
if (isServer) {
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
}
and here is my server.js:
import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';
import configureStore from '../src/redux/store/index';
import AppRouter from '../src/routers/AppRouter';
import { StaticRouter } from 'react-router-dom';
const app = express();
const store = configureStore();
app.use(express.static(path.resolve(__dirname, '..', 'build')));
app.use('/*', (req, res, next) => {
fs.readFile(path.resolve('./build/index.html'), 'utf-8', (err, data) => {
if (err) {
console.log(err);
return res.send(500).send('Error happened!');
}
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={{}}>
<AppRouter />
</StaticRouter>
</Provider>,
)}</div>`,
),
);
});
});
app.listen(3000, () => {
console.log('Listening...');
});
In case this isn't enough information, here is the link to the project on GitHub.
So the crux of your problem here seems to be that when rendered on the server, the router isn't rendering the correct component based on the url as we would expect.
What is happening is not that the class name is changed, instead we are actually always rendering the root page and then react replaces the content with the "new task" page on the client before you get a chance to have a look at it in the inspector.
Summary: When using app.use with a path in express, we are mounting an express middleware at the specified path, and express removes the "mount point" from the req.url provided to the middleware. Among other solutions, we can use req.originalUrl to get the actual url. req.originalUrl is a property provided by express that gives the url as it was before any middleware changed it.
Let's investigate!
If we extract the code that renders your app to a separate variable and log that, we will see that the logged HTML string is the page with the "New List" button on it.
const renderedApp = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={{}}>
<AppRouter />
</StaticRouter>
</Provider>,
);
console.log('Rendered app', renderedApp);
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${renderedApp}</div>`,
),
);
Visiting localhost:3000/l/new and looking in the log:
Rendered app <div class="header"><a class="header__button" id="header__colabico" href="/">COLABI.CO</a><div class="header__right"><button class="header__button" id="header__tweet">TWEET</button><button class="header__button" id="header__logout">LOGOUT</button></div></div><div class="main-flex-container"><a class="home__button" href="/l/new"> <!-- -->NEW LIST<!-- --> </a><p class="home__infotext"> <!-- -->Start by pressing that big button up there!<!-- --> </p></div><div class="footer"><div class="footer__left"><a class="footer__button" href="/privacy">PRIVACY</a><a class="footer__button" href="/terms">TERMS</a></div><p class="footer__colabico"> © colabi.co</p></div>
This looks very much like the root page instead of the new list page.
Aside: Putting the rendering on a separate line instead of inline in the template string also helps with syntax highlighting, as a nice bonus.
Aside 2: By adding this logging you'll also see that your code is not run when hitting the root url. The reason for this is probably because of the static middleware at the start, which will match on the index.html page in your build folder.
Now that we are fairly certain what the problem is, let's try to see why it might be.
The StaticRouter accepts the location as a prop, and we are passing in req.url for that. Let's verify that this is what we think it is.
console.log('URL:', req.url);
When visiting localhost:3000/l/new, this is logged:
URL: /
That is certainly not what we expected!
I went to look up the documentation for req.url to see what the problem might be, and found this method:
req.originalUrl
I couldn't see the documentation for req.url, but there's a helpful box here explaining why that is (req.url comes from Node's http module).
Logging req.originalUrl too just to see what that is for us, and sure enough:
original URL: /l/new
URL: /
req.originalUrl has what we need!
At this point I searched around for references to explain why req.url might be / in our case, and it turns out it's because of this:
When we are using app.use, we are mounting an express middleware at the specified path, rather than setting up a route handler (that would be app.get or similar), and middlewares remove the path they are mounted at from their own url, because they consider that their root. When we specify the middleware to be mounted at /*, the wildcard matches all routes and the whole url is removed.
In other words, we can fix this in a few different ways:
Keep it as it is, but use req.originalUrl instead
Remove the path from app.use (app.use((req, res, next) => {/* your code */}))
Change from app.use to app.get
Hope that helps!

Meteor, React Router 4, and Authentication

I've been scouring the inet trying to find anywhere that defines how to handle authentication in meteor and react router 4 .
Basically, I want certain routes to only be available to authenticated users. Is there any documentation on it?
Aseel
Meteor has a very well developed User Accounts system. It provides ready libraries for OAuth authentication with Twitter, Facebook, etc. as well as a basic but useful UI packages. Check Meteor's official guide here first.
For implementing routing you need to track Meteor.userId() and change route via Meteor's reactive system called Tracker. Meteor.userId() returns a userId if currently connected user is logged in, and null otherwise. I provide an example code where React Router is used for routing, below. Notice that you'll will also need the historypackage to be installed and imported while working with React Router v4.
In your client/main.js;
import { Meteor } from 'meteor/meteor';
import React from 'react';
import ReactDOM from 'react-dom';
import { Tracker } from 'meteor/tracker'
import {onAuthChange, routes} from "../imports/routes/routes";
Tracker.autorun(function(){
const authenticated = !! Meteor.userId();
onAuthChange(authenticated);
});
Meteor.startup(() => {
ReactDOM.render(routes, document.getElementById('app'));
});
And in your routes.js file;
import { Meteor } from 'meteor/meteor';
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory'
import Home from './../ui/components/Home';
import Login from './../ui/components/Login';
import NotFound from './../ui/components/NotFound';
import Signup from './../ui/components/Signup';
const history = createBrowserHistory();
const unauthenticatedPages = ['/', '/signup'];
const authenticatedPages = ['/link'];
const publicPage = function () {
if (Meteor.userId()) {
history.replace('/link');
}
};
const privatePage = function () {
if(! Meteor.userId()) {
history.replace('/');
}
};
export const routes = (
<Router history = {history}>
<Switch>
<Route exact path='/:id' component= {Login} onEnter={publicPage}/>
<Route exact path='/signup' component={Signup} onEnter={publicPage}/>
<Route exact path='/link' render={ () => <Home greet='User'/> } onEnter={privatePage} />
<Route component={NotFound}/>
</Switch>
</Router>
);
export const onAuthChange = function (authenticated) {
console.log("isAuthenticated: ", authenticated);
const path = history.location.pathname;
const isUnauthenticatedPage = unauthenticatedPages.includes(path);
const isAuthenticatedPage = authenticatedPages.includes(path);
if (authenticated && isUnauthenticatedPage) { // pages: /signup and /
console.log(`Authenticated user routed to the path /link`);
history.replace('/link');
} else if (!authenticated && isAuthenticatedPage) {
console.log(`Unauthenticated user routed to the path /`);
history.replace('/');
}
};
Here's a neat way to have public routes and authenticated routes:
https://gist.github.com/lucnat/643988451c783a8428a2811dbea3d168
public components are visible by everyone, they use the PublicLayout
authenticated components are visible by authenticated users only -
they use the AuthenticatedLayout
We could have an arbitrary number of layouts. In the example above, there are two layouts - each with it's own navbar.
I've been trying to get a more updated method using functional components.
I've tried implementing a conditional check similar to the documentation of React-router.
This was working after giving history.push to a desired route after waiting for Meteor.loginWithPassword to complete.
But refreshing the browser ended up rendering login page again.
Meteor is having an intermediate state of Meteor.loggingIn().
Handling this state in the Authentication check fixed this issue.
Feel free to give feedback.
I've created a gist with an implementation for authentication of routes in Meteor - React-router stack with functional components and hooks.
Check this gist with basic structure of the implementation.
https://gist.github.com/rinturj84/0ef61005bf3a4ca5fb665dfc5f77e3d1
Actually, the best idea to do that is to create Multiple separated routers because you can take benefits from using Meteor Reactive-var.
This is a sample :
export default function App() {
if (Meteor.user()) {
return (
<React.StrictMode>
<Global_Router Client={null} About={About} HomeNavbar={HomePageNav} HomePage_Home={HomePage_Home}
HomeFooter={Footer} Homepage_Contacts={Homepage_Contacts}/>
</React.StrictMode>
);
}
else {
return(
<RouterClient/>
);
}
Router client
export const RouterClient = () => {
return (
<Router className="container-fluid">
<Switch >
<Route exact path="/Client" >
<HomeCLient/>
</Route>
<Route path="*">
<Redirect to="/Client" />
</Route>
</Switch>
</Router>
)
};
Respectively,you can create a router for the admin too. In general, that is the most efficient way to do that.

How do I go to a specific page in React based on a URL param

As an example when entering http://localhost:3000/ui/?goto=/ui/entity/e2 in the browser I'd like to go to the Entity component e2.
This is my router:
<Route path="/ui/" component={App}>
<IndexRoute component={EntitiesPage} />
<Route component={Entity} path="entity/:id" />
<Route component={NotFound} path="*" />
</Route>
This is the App component:
import React from 'react'
const App = React.createClass( {
render() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// go here: gotoUrl;
} else {
return (
<div className="App">
{this.props.children}
</div>
)
}
}
})
export default App
this.context is empty.
this.props has:
history
location
route
routeParams (empty)
routes
UPDATE:
I've ended up using this:
import React from 'react'
import { withRouter } from 'react-router'
const App = React.createClass( {
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
this.props.router.replace(gotoUrl);
}
},
render() {
return (
<div className="App">
{this.props.children}
</div>
);
}
})
export default withRouter(App)
One thing that might be tripping you up is that render should have no side effects.
A "side effect" is anything that changes what's going on in your app*: updating state, making AJAX calls, or in this case, altering the page location. The render method should only read from the current state of the component, then return a value.
Because you're already using React.createClass, the best way to handle this is by adding a separate method that React handles specially: componentWillMount. I'd recommend you put your "redirect" logic here.
In order to properly change the page location, you'll need access to the browser history object which react-router manipulates. You can import this from the react-router library itself and directly call methods on it:
// At top of file
import { browserHistory } from 'react-router'
// Then, in your component:
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// NOTE: this may have security implications; see below
browserHistory.push(gotoUrl);
}
}
Source: documentation.
I'd suggest that, instead of using query.goto, you instead select a parameter that can be easily validated, such as the entity ID itself (a simple regex can make sure it's valid). Otherwise, an unscrupulous user might send a link to another user and cause them to access a page that they didn't mean to.
*Note: there are stricter definitions of "side effect" out there, but this one is pretty useful for React development.
You should use browserHistory
import { browserHistory } from 'react-router';
...
if (gotoUrl) {
browserHistory.push(gotoUrl)
}
Let me know if this works

Link to changes state but does not change URL

Using renderToString to render my components server-side. All of that is working just fine. If I manually enter a URL like /register, my components are rendering perfectly.
Problem is, when using <Link> in my app, the state is changing but my URL is not updating at all.
route.js:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { browserHistory, Router, match } from 'react-router';
import routes from './routes';
import store from './stores';
// Server rendering works identically when using async routes.
// However, the client-side rendering needs to be a little different
// to make sure all of the async behavior has been resolved before the
// initial render,to avoid a mismatch between the server rendered and client rendered markup.
match({ location:browserHistory, routes }, (error, redirectLocation, renderProps) => {
render((
<Provider store={store}>
<Router {...renderProps} />
</Provider>
), document.getElementById('root'));
});
I have a feeling it could be due to browserHistory, is there something I'm missing?
I was giving match() the wrong arguments. Instead of:
match({ location:browserHistory, routes }
It should have been:
match({ history:browserHistory, routes }

Resources