I'm trying to set up server-side rendering with the newest version of react-router v.4. I followed this tutorial https://react-router.now.sh/ServerRouter.
I get following error when I refresh browser: Invariant Violation: React.Children.only expected to receive a single React element child.
my routes.jsx file:
export default () =>
<div>
<Header />
<Match pattern="/" component={Home} />
<Match pattern="/about" component={About} />
<Miss component={NotFound} />
</div>;
and in index.jsx I'm rendering app
import BrowserRouter from 'react-router';
import Routes from './routes';
ReactDOM.render(<BrowserRouter> <Routes /> </BrowserRouter>, document.getElementById('app'));
Now as server I'm using express.js. Here is my configuration:
import Routes from '../routes';
server.use((req, res) => {
const context = createServerRenderContext();
let markup = renderToString(
<ServerRouter location={req.url} context={context} > <Routes /> </ServerRouter>);
const result = context.getResult();
if (result.redirect) {
res.writeHead(301, {
Location: result.redirect.pathname,
});
res.end();
} else {
if (result.missed) {
res.writeHead(404);
markup = renderToString(
<ServerRouter location={req.url} context={context}> <Routes /> </ServerRouter>);
}
res.write(markup);
res.end();
}
});
I didn't find any tutorial for server-rendering with this version of react-routes except official.
Can anyone help me what I'm doing wrong ? thanks.
Solved !
First problem was that I had spaces around <Routes /> tag.
Correct solution:
<ServerRouter location={req.url} context={context}><Routes /></ServerRouter>);
Second problem was in included <Header /> tag in routes.jsx file.
I had the following error (Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. Check the render method of StatelessComponent)
File Header.jsx contained the following line of code:
import Link from 'react-router';
Correct solution: (I forgot to put curly brackets ):
import { Link } from 'react-router';
The big issue is that the<BrowserRouter> is only expected to have one child, so you should wrap it's children in a div. This is done so that React Router is environment agnostic (you can't render a div in React Native, so RR expects you to include the appropriate wrapper).
export default () =>
<BrowserRouter>
<div>
<Header />
<Match pattern="/" component={Home} />
<Match pattern="/about" component={About} />
<Miss component={NotFound} />
</div>
</BrowserRouter>;
As a secondary issue, you are including the <BrowserRouter> in your <App> component, so it will be rendered on the server. You do not want this. Only the <ServerRouter> should be rendered on the server. You should move the <BrowserRouter> further up your client side component hierarchy to avoid this.
// App
export default () =>
<div>
<Header />
<Match pattern="/" component={Home} />
<Match pattern="/about" component={About} />
<Miss component={NotFound} />
</div>;
// index.js
render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('app'))
because BrowserRouter doesn't exist on react-router, try install and import it from react-router-dom
I believe the answers listed above is outdated. As of today, the official react-router docs suggest using StaticRouter instead of ServerRouter for server side rendered apps.
A fantastic documentation can be found here.
For anybody coming later, Ryan Florence has added a snippet of how to accomplish this.
SSR in React Router v4
// routes.js
const routes = [
{
path: '/',
component: Home,
exact: true
},
{
path: '/gists',
component: Gists
},
{
path: '/settings',
component: Settings
}
]
// components
class Home extends React.Component {
// called in the server render, or in cDM
static fetchData(match) {
// going to want `match` in here for params, etc.
return fetch(/*...*/)
}
state = {
// if this is rendered initially we get data from the server render
data: this.props.initialData || null
}
componentDidMount() {
// if rendered initially, we already have data from the server
// but when navigated to in the client, we need to fetch
if (!this.state.data) {
this.constructor.fetchData(this.props.match).then(data => {
this.setState({ data })
})
}
}
// ...
}
// App.js
const App = ({ routes, initialData = [] }) => (
<div>
{routes.map((route, index) => (
// pass in the initialData from the server for this specific route
<Route {...route} initialData={initialData[index]} />
))}
</div>
)
// server.js
import { matchPath } from 'react-router'
handleRequest((req, res) => {
// we'd probably want some recursion here so our routes could have
// child routes like `{ path, component, routes: [ { route, route } ] }`
// and then reduce to the entire branch of matched routes, but for
// illustrative purposes, sticking to a flat route config
const matches = routes.reduce((matches, route) => {
const match = matchPath(req.url, route.path, route)
if (match) {
matches.push({
route,
match,
promise: route.component.fetchData ?
route.component.fetchData(match) : Promise.resolve(null)
})
}
return matches
}, [])
if (matches.length === 0) {
res.status(404)
}
const promises = matches.map((match) => match.promise)
Promise.all(promises).then((...data) => {
const context = {}
const markup = renderToString(
<StaticRouter context={context} location={req.url}>
<App routes={routes} initialData={data}/>
</StaticRouter>
)
if (context.url) {
res.redirect(context.url)
} else {
res.send(`
<!doctype html>
<html>
<div id="root">${markup}</div>
<script>DATA = ${escapeBadStuff(JSON.stringify(data))}</script>
</html>
`)
}
}, (error) => {
handleError(res, error)
})
})
// client.js
render(
<App routes={routes} initialData={window.DATA} />,
document.getElementById('root')
)
Related
Following this tutorial series to try to build a simple React, Electron, and firebase app.
This project mirrors this demo project. I'm getting a lot of compile errors, mostly outdated content and dependencies, but managed to fix most of them up. The main thing I'm struggling with now is upgrading some code from react-router v5 to v6, specifically in app.js
import React, { useState, useEffect } from "react";
import { Router, Routes, Route, Navigate } from "react-router-dom";
import AddMoviePage from "../pages/add-movie-page";
import EditMoviePage from "../pages/edit-movie-page";
import AccountPage from "../pages/account-page";
import MoviesPage from "../pages/movies-page";
import NotFoundPage from "../pages/not-found-page";
import { auth } from "../data/firebase";
import Nav from "./nav";
import { createMemoryHistory } from "history";
function AuthenticatedRoute(props) {
const { isAuthenticated, children, ...routeProps } = props;
return <Route {...routeProps}>{isAuthenticated ? children : <Navigate to="/account" />}</Route>;
}
function App() {
const [user, setUser] = useState(null);
const isAuthenticated = user !== null;
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser);
});
return unsubscribe;
}, []);
const history = createMemoryHistory();
console.log(history);
return (
<Router history={history}>
<Nav user={user} />
<Routes>
<Route path="/account">
<AccountPage user={user} />
</Route>
<AuthenticatedRoute path="/" exact isAuthenticated={isAuthenticated}>
<MoviesPage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/add" isAuthenticated={isAuthenticated}>
<AddMoviePage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/edit/:id" isAuthenticated={isAuthenticated}>
<EditMoviePage user={user} />
</AuthenticatedRoute>
<Route path="*">
<NotFoundPage />
</Route>
</Routes>
</Router>
);
}
export default App;
I'm getting the following error and can't really figure out what's going on:
Uncaught TypeError: Cannot read properties of undefined (reading 'pathname')
The above error occurred in the <Router> component.
Issues
The main issue here is that you are importing and using the low-level Router component instead of one of the high-level routers (i.e. BrowserRouter, MemoryRouter, HashRouter, etc). The Router component has a couple required props and history isn't one of them.
Router Interface:
declare function Router(
props: RouterProps
): React.ReactElement | null;
interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial<Location> | string; // <-- required!
navigationType?: NavigationType;
navigator: Navigator; // <-- required!
static?: boolean;
}
The high-level routers all instantiate/manage a history reference internally and pass the required props and render the base Router.
Additional issues found in the code:
Another issue is that in react-router-dom#6 custom route components are no longer valid. Only Route components can be rendered by the Routes component. You'll instead convert your older v5 custom route components, a.k.a. AuthenticatedRoute, either into Wrapper components that render the children prop, or as the preferred method a Layout Route.
A final related issue is that Route components and only be rendered by the Routes component or other Route components in the case of building nested routes. In other words, the only valid children components of a Route component is another Route component. The routed content you want to be rendered on a route is passed to the Route component's element prop.
Solution
Convert AuthenticatedRoute to a layout route.
import { Navigate, Outlet } from 'react-router-dom';
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
// Don't render the protected content or redirect until we confirm
// authentication status.
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
It seems you are wanting to really use a MemoryRouter since you are instantiating your own MemoryHistory object. Import and render the MemoryRouter directly. Move the route "children" onto their respective route's element prop.
Example:
...
import {
MemoryRouter as Router, // <-- import high-level router
Routes,
Route,
Navigate,
Outlet
} from "react-router-dom";
...
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
function App() {
const [user, setUser] = useState(); // <-- initially not auth'd or unauth'd
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser); // <-- sets to user object or null
});
return unsubscribe;
}, []);
return (
<Router> // <-- Now really a MemoryRouter
<Nav user={user} />
<Routes>
<Route path="/account" element={<AccountPage user={user} />} />
<Route element={<AuthenticatedRoute isAuthenticated={user} />}>
<Route path="/" element={<MoviesPage user={user} />} />
<Route path="/add" element={<AddMoviePage user={user} />} />
<Route path="/edit/:id" element={<EditMoviePage user={user} />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Router>
);
}
export default App;
I have a website made with Docusaurus v2 that currently contains documentation. However, I would like to add a page of a list of workflows where if a workflow in the list is clicked, the user would be shown a page of additional details of that workflow. For now it seems docusaurus.config seems to be handling most of the routing, but is there a way I can add a dynamic route like /workflows/:id? I made a separate standalone app which had a Router object and it worked if my App.js looks like this:
// App.js
import Navigation from './Navigation'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
function App() {
return (
<Router>
<Navigation />
<Switch>
<Route path="/" exact component={Home}></Route>
<Route path="/workflows" exact component={Workflows}></Route>
<Route path="/workflows/:id" component={WorkflowItem}></Route>
</Switch>
</Router>
)
}
Is it possible to add the Router somewhere in Docusaurus?
Thanks!
I solved this by creating a simple plugin to add my own custom routes. Documentation here.
Let's call the plugin plugin-dynamic-routes.
// {SITE_ROOT_DIR}/plugin-dynamic-routes/index.js
module.exports = function (context, options) {
return {
name: 'plugin-dynamic-routes',
async contentLoaded({ content, actions }) {
const { routes } = options
const { addRoute } = actions
routes.map(route => addRoute(route))
}
}
}
// docusaurus.config.js
const path = require('path')
module.exports = {
// ...
plugins: [
[
path.resolve(__dirname, 'plugin-dynamic-routes'),
{ // this is the options object passed to the plugin
routes: [
{ // using Route schema from react-router
path: '/workflows',
exact: false, // this is needed for sub-routes to match!
component: '#site/path/to/component/App'
}
]
}
],
],
}
You may be able to use the above method to configure sub-routes as well but I haven't tried it. For the custom page, all you need is the Switch component (you are technically using nested routes at this point). The Layout component is there to integrate the page into the rest of the Docusaurus site.
// App.js
import React from 'react'
import Layout from '#theme/Layout'
import { Switch, Route, useRouteMatch } from '#docusaurus/router'
function App() {
let match = useRouteMatch()
return (
<Layout title="Page Title">
<Switch>
<Route path={`${match.path}/:id`} component={WorkflowItem} />
<Route path={match.path} component={Workflows} />
</Switch>
</Layout>
)
}
I'm using both Express and React for my webapp, because we are migrating rendering from Handlebars to React. However we want to maintain the logic in Express and render only in React. The problem is when I want to do a redirect. This is the client.js with the logic:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { BrowserRouter } from "react-router-dom";
// In production, we want to hydrate instead of render
// because of the server-rendering
if (process.env.NODE_ENV === 'production') {
ReactDOM.hydrate(
<BrowserRouter>
<App {...window.__APP_INITIAL_STATE__} />
</BrowserRouter>
, document.getElementById('app'));
} else {
ReactDOM.render(
<BrowserRouter>
<App {...window.__APP_INITIAL_STATE__} />
</BrowserRouter>
, document.getElementById('app'));
}
// Hot reload is that easy with Parcel
if (module.hot) {
module.hot.accept();
}
and this is a simple path of Express:
app.get('/signup', (req,res)=>{
const initialState = {component:'SignUp'};
let context = {};
const markup = ReactDOM.renderToString(
<StaticRouter location={req.url} context={context}>
<App {...initialState} />
</StaticRouter>
);
if (context.url) {
res.writeHead(302, {
Location: context.url
});
res.end();
}
else {
res.send(generateHtml(markup,initialState));
}
});
the problem is: when I try to redirect like this (and change this.state.redirect):
render(){
return this.state.redirect ? <Redirect to="/login" /> :(
<div className="ro">HI </div> );
}
browser go to /login path, but without call the server. It simple shows a blank page.
This is also my App component:
export default class App extends Component {
constructor(props){
super(props);
console.log(props);
}
render(){
if(this.props.component === 'Root'){
return <Root />;
}
else if(this.props.component === 'Login'){
return <Login />;
}
else if(this.props.component === 'SignUp'){
return <SignUp />;
}
else{
return (
<div>
<h1>Hello word {this.props.component}</h1>
<Button color="danger">Danger!</Button>
</div>
);
}
}
}
that is a Root component that manage where to go.
How to fix this? I don't want to use react-router to handle the route, we want to maintain all on Express.
Thanks
When client-side rendering, instead of setting state to render a <Redirect>, you can window.location.replace("/login");
to simulate a <Redirect push>: window.location.href = "/login";
On the server-side, you can continue to use the <Redirect>
You can host your express app on a subdomain :
<Redirect to="http://express.myapp.com/login" />
I started to experiment with react router, and dynamic matches.
I wanted to create a function which matches the slug of the URL to a slug in a JSON file.
The error I get:
TypeError: Unable to get property 'slug' of undefined or null reference
I think that the 'Slug' of the url is undefined, but I am not sure on how to fix it.
screenshot of error
my code for routes.js:
import React from 'react';
import Header from './components/header/header.js';
import Home from './components/home/home.js';
import About from './components/about/about.js';
import NotFound from './components/notFound/notFound.js'
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import PostPage from './components/postpage/postpage.js'
import posts from './files/data.json';
class Routes extends React.Component {
render(){
return (
<Router>
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/:slug" component={props => {
const postt = posts.posts.filter (post => props.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} />
} } />
}}/>
<Route component={NotFound} />
</Switch>
</div>
</Router>
);
}
}
export default Routes;
PostsPage.js:
import React from 'react';
import Post from '../post/post.js'
const PostPage = (props) => (
<div>
<Post {...props.post}/>
</div>
);
export default PostPage;
and posts.js:
import React from 'react';
import { Link } from 'react-router-dom';
import './post.css';
class Post extends React.Component {
render(){
return(
<div>
<div >
<h2 className='subTitle'><Link to={`/post/${this.props.slug}`} className='link'>{this.props.title}</Link></h2>
<p className='content'>{this.props.excerpt}</p>
</div>
</div>
);
}
}
export default Post;
If you made it this far thank you for helping
slug variable is given inside match props which you are missing.
<Route path="/:slug" render={props => {
const postt = posts.posts.filter (post => props.match.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} />
} } />
}}/>
Also, do not inline component use a render function instead. From the docs:
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).
https://reacttraining.com/react-router/web/api/Route/render-func
One of the ways you can get this fixed is by using .find() instead of .filter() like this :
const postt = posts.find (post => props.match.params.slug === post.slug)
And then inside your <Router /> make sure to send the rest of {...props} as well :
<Route path="/:slug" component={props => {
const postt = posts.find (post => props.match.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} {...props} />
} } />
Every react component is passed through the following function, found in node_modules ReactElement.js:
ReactElement.createElement = function (type, config, children){
.
.
.
}
This also Includes <Route> and <Router>. Consider the following JSX Code from a React Tutorial :
<Router history={hashHistory}>
<Route path="/" component={Layout}>
<IndexRoute component={Featured} />
<Route path="Settings" component={Settings} />
<Route path="Archives" component={Archives} />
</Route>
</Router>
The above code will be transpiled into the code shown below using Babel:
"use strict";
React.createElement(
Router,
{ history: hashHistory },
React.createElement(
Route,
{ path: "/", component: Layout },
React.createElement(IndexRoute, { component: Featured }),
React.createElement(Route, { path: "Settings", component: Settings }),
React.createElement(Route, { path: "Archives", component: Archives })
)
);
From this code, the "children" of the outer <Route> are the inner 3 <Route>s. We know that :
<MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
class MyContainer extends React.Component{
render(){
return (
.
.
//This will be MyFirstComponent or MySecondComponent
{this.props.children}
.
.
);
}
}
But the same is not true for the <Route>s. The value of this.prop.children for the JSX Router code applies to the component prop, but not the Router itself. Why is this behavior of this.props.children different for <Route> than any other ReactComponent?
Because Router deletes its Route children's children.
Here's the createRouteFromReactElement function from RouteUtils.js in react-router 3.0.1:
export function createRouteFromReactElement(element) {
const type = element.type
const route = createRoute(type.defaultProps, element.props)
if (route.children) {
const childRoutes = createRoutesFromReactChildren(route.children, route)
if (childRoutes.length)
route.childRoutes = childRoutes
delete route.children
}
return route
}
Notice the line fifth from the end: delete route.children.
Why does it do that? Consider this invariant warning from Route.prototype.render:
render() {
invariant(
false,
'<Route> elements are for router configuration only and should not be rendered'
)
}