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 }
Related
Using React.js & react router v6
Is there a way I can extract information from the current URL? Let's say my current URL is this: localhost:3000/about/x567gfh67ssd90g
I simply want to perform a condition based on the current URL. Is there any possible way of doing this?
import { useLocation } from "react-router-dom";
const Component = () => {
const location = useLocation();
useEffect(() => {
console.log(location);
});
return ();
};
You can get pathname using useLocation.
You can also add connected-react-router to your project. This way you will have access to all the current route information in your redux-store. This will also allow you to "Time travel" through your SPA. When you hit the backbutton instead of going to the previous page you can go back through state.
import React from 'react';
import { render } from 'react-dom';
import { Provider, ReactReduxContext } from 'react-redux';
import { ConnectedRouter } from "connected-react-router";
import { store, history } from './Store/store';
import { App } from './App/App';
render(
<Provider store={ store } context={ ReactReduxContext } >
<ConnectedRouter context={ ReactReduxContext } history={ history }>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
If you install the redux-devTools in your browser you can look at all the URL data like this- Obviously this is a little more advanced but you it will give you access to everything you need.
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.
I have a simple demo react app that uses react-router-dom (5.2) to show one of 3 "pages".
The app is included on a page that has a button:
index.html:
<button data-app-button data-sku='woo-beanie'>Click Me</button>
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
document.addEventListener('click', function (event) {
if (event.target.closest('button[data-app-button]')) {
// send instructions to react
}
});
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
I want to be able to navigate to a page in the react site, passing through the buttons data-attributes. How is this done with react and react-router ?
UPDATE
#Doppoio's solution works - as long as I'm on a different "page" in my react app. However I have a route like this:
<Route
path="/tryon/:id/:product_sku?">
</Route>
If I start in app from a route of say /faqs and my external button navigates to /tryon/242/jumper-23 my component is awar of the product_sku property.
However when I'm on a page in app of /tryon/242 and then i click an external button to navigate to /tryon/242/jumper-23 the component should be aware of the jumper-23 optional parameter. Currently it isn't.
How do i make the Tryon component detect the change in url of just the optional parameter?
Somewhere in your code under Router, you can add history to window object. And call it from there.
const SomeComponentInsideRouter = () => {
const history = useHistory();
useEffect(() => {
window.reactHistory = history; // Add reference to history via window object.
}, []);
return null;
};
And call it via window.reactHistory
document.addEventListener("click", function (event) {
if (event.target.closest("button[data-app-button]")) {
// send instructions to react
window.reactHistory.push("/about");
}
});
Here's sandbox link
https://codesandbox.io/s/happy-ganguly-b0u2o?file=/src/index.js
Update to mention changed props:
Changes to the props can be detected using componentDidUpdate
https://reactjs.org/docs/react-component.html#componentdidupdate
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!
//startup file
import {BrowserRouter} from 'react-router';
let root=(<BrowserRouter>
//Some components
</BrowserRouter>);
ReactDOM.render(Root, document.getElementById("app"));
// function to navigate or route
import createBrowserHistory from 'history lib createBrowserHistor';
const history = createBrowserHistory();
let browseTo=function(path){
history.push({pathname: path})
})
After calling above Function only URL path changing actual route is not perform.
Like URL localhost:8080 changes to localhost:8080/login not routing to login component.
https://github.com/ReactTraining/react-router/issues/4059
browserHistory is not exposed by react-router in v4, only in v2.