React SSR React Router Dom Switch, Route, Link "Invariant Failed" - reactjs

I beleive the proper discription of this issue is explained here by timdorr. I tried exporting App.js from the bundle but get window undefined errors. SO still stuck
SSR/Client React Router Dom "Switch" breaks for me with a "Invariant Failed". I believe it says it has something to do with Switch not been allowed outside "Router", which it is inside.
The minimal project link is below, that may be easier way look at the project. I have listed the main files below
1: SERVER SIDE RENDER ENDPOINT
// EXPRESS ROUTER
const express = require("express");
const aRouter = express.Router();
// REACT UTILITIES
import React from "react";
import { renderToNodeStream } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { createMemoryHistory } from "history";
import Loadable from "react-loadable";
// REDUX UTILITIES
import { init } from "../src/module/store";
import { Provider } from "react-redux";
// CUSTOM COMPONENTS
import App from "../src/App";
// UTILITIES
import fs from "fs-extra";
import renderUtils from "../utils/renderUtils";
// ASSETS
import { initState } from "../assets/store/init";
aRouter.get(["/", "/home"], async function (req, res) {
console.log("RENDER HOME");
try {
// INITIAL WRITE TO CLIENT - START HEAD
res.setHeader('Content-Type', 'text/html');
res.write("<!DOCTYPE html>");
res.write("<html style='scrollbar-width: none;'>");
let headHTML = await fs.readFile("./public/head.html", "utf-8");
let scriptsHTML = await fs.readFile("./public/scripts.html", "utf-8");
res.write(headHTML);
res.write("<body>");
res.write(`<div id = "app-container">`);
// INITALISING STATE
var initialState = initState();
initialState.article.articles = {
"abcde": {
title: "My First Article",
body: "This is my first article"
},
"fghij": {
title: "My Second Article",
body: "This is my second article"
},
"klmno": {
title: "My Third Article",
body: "This is my third article"
}
};
initialState.article.fetched = true;
initialState.ui.user = { type: "" };
initialState.ui.global = {
team: "Arsenal",
teamID: 19
};
const history = createMemoryHistory({ initialEntries: [req.originalUrl] });
const store = init(history, initialState);
// THE ISSUE SEEMS TO BE TO DO WITH THIS SERVER SIDE STATIC BROWSER AND THE CLIENT BORWSER ROUTER
const stream = renderToNodeStream(
<Provider store = {store}>
<StaticRouter history = {history} location = {req.originalUrl} context = {{}}>
<App />
</StaticRouter>
</Provider>
);
stream.pipe(res, { end: false });
stream.on("end", renderUtils.onRenderEnd.bind(this, res, store, scriptsHTML));
} catch (err) { renderUtils.onRenderError.bind(this, res, "RENDER HOME ERROR", err.message); }
});
var self = (module.exports = aRouter);
2: CLIENT INDEX
// REACT
import React from "react";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import { BrowserRouter } from "react-router-dom";
import { createMemoryHistory } from "history";
import { Provider } from "react-redux";
// REDUX
import { init } from "./module/store";
// CREATE STORE
let history = createMemoryHistory();
let store = init(history, window.INITIAL_STATE);
// MAIN APP COMPONENT
import App from "./App";
// MOUNTED STYLES
import "./style/client/index.scss";
const renderApp = () => {
ReactDOM.hydrate(
<Provider store = {store}>
<BrowserRouter history = {history}>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("app-container")
);
};
store.subscribe(() => renderApp());
3: APP - CLIENT
// REACT
import React, { PureComponent } from "react";
import { Switch, Route } from "react-router-dom";
import PropTypes from "prop-types";
// REDUX STORE
import { connect } from "react-redux";
import { getName, getAge, getPosition } from "./module/user/userReducer";
import { getUIElement, setUIElement } from "./module/uiReducer";
// IMPORT CUSTOM COMPONENTS
import Routes from "./Routes";
class App extends PureComponent {
componentDidMount = () => this.props.setUI("user", "type", "admin");
render = () => {
return (
<div className = "app">
<span>My App</span>
<span>Name : {this.props.name}</span>
<span>Age : {this.props.age}</span>
<span>Position : {this.props.position}</span>
<span>Team : {this.props.team}</span>
<span>Team ID : {this.props.teamID}</span>
<span>Type : {this.props.type}</span>
<div>
<Switch>
<Route path = "/" component = {MyLocation} />
<Route path = "/contact" render = {() => (<MyLocation location = "Contact" />)} />
</Switch>
</div>
</div>
);
};
};
App.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
position: PropTypes.string.isRequired,
team: PropTypes.string.isRequired,
teamID: PropTypes.number.isRequired,
type: PropTypes.string.isRequired,
setUI: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
name: getName(state),
age: getAge(state),
position: getPosition(state),
team: getUIElement(state, "global", "team"),
teamID: getUIElement(state, "global", "teamID"),
type: getUIElement(state, "user", "type")
});
const mapDispatchToProps = dispatch => ({
setUI: (component, element, value) => dispatch(setUIElement({ component, element, value }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
The full minimal react app here
It just breaks when I add the Switch and Routes. The Switch "IS INSIDE" the Browser Router. I have read articles which explain to send the same React Router Dom to the client, but I tried them explanations and they dont work for me.
To run the project simply run "yarn", "npm run build" and "npm start". The app has just one page with some filler text
Issue : Breaks at Switch
Required: Work at Switch
Tried: Explantions that explain to bring same instance of react-router-dom to client from server and use webpack alies etc.
Timdorr (Shared at start of question) explains this.
There is a new React.createContext API that we use in 5.0 to replace the legacy context usage. This involves creating a "context" pair, a set of Provider and Consumer components. We create that in a singleton module that is imported into Router and Link directly. In this new API, you have to use the exact same instance. Two separately-created contexts will never match up and won't be visible to each other.
Whats also funny is this works live on Heroku "production", but doesnt work locally "production". Im thinking heroku have some fallback code catching it.
Any help be great;
Daniel

After a lot of playing around I discovered the issue stemmed from
import { withRouter } from "react-router-dom";
export default withRouter(Component);
I think withRouter doesn't exists anymore on this new version and as as to why it worked with Heroku is a mystery. I think Heroku has great version controlling and debugging/handling.
I started using the hook useHistory instead and converted my classic components to function component with hooks and all is well now
Daniel

Related

React Router Dom v6.4 doesn't allow history.listen (prior suggestions deprecated) with createBrowserRouter or createMemoryRouter

react-router-dom v. 6.4.2 doesn't allow history.listen as referenced in the code example below. This is for a mfe with module federation.
In the code example using history.listen, if a link is clicked in the remote (loaded as mfe) then the memory history (memory router now) current path will be updated. It will then call onNavigate to tell the host container which is using browser history (browser router now) that the current path has changed.
Prior suggestions were to use UNSAFE_NavigationContext, useHistory, unstable_HistoryRouter, import {...} from 'history', etc. Apparently, those prior methods were temporary migration aids from v5 to v6.3 and with v6.4+ are now deprecated in favor of the new data api's in 6.4. See here
we do not intend to support custom histories moving forward. This API
is here as a migration aid. We recommend removing custom histories
from your app.
Additionally, from the maintainers of RRD:
We recommend updating your app to use one of the new routers from 6.4.
After searching here and within both open and closed issues on remix-RRD I have been unable to find a workable solution based on the above for replacing history.listen, .push or .location with the new data api's (routers) using createBrowserRouter or createMemoryRouter as referenced here
There are many open issues on the react-router-dom page relating to this use case.
Original marketing/src/bootstrap.tsx from remote
import React from 'react'
import { createRoot } from 'react-dom/client'
import { createMemoryHistory, createBrowserHistory } from 'history' <= Not Supported
import App from './App'
let root: { render: (arg0: JSX.Element) => void } | null = null
// Mount function to start up the app
const mount = (el: any, { onNavigate, defaultHistory, initialPath }: any) => {
if (!el) {
root = null
return
}
// If defaultHistory in development and isolation use BrowserHistory
const history =
defaultHistory ||
// Otherwise use MemoryHistory and initial path from container
createMemoryHistory({
initialEntries: [initialPath],
})
if (onNavigate) {
history.listen(onNavigate) <= Not Supported
}
root = root ? root : createRoot(el)
root.render(<App history={history} />)
return {
onParentNavigate({ pathname: nextPathname }: any) {
const { pathname } = history.location <= Not Supported
if (pathname !== nextPathname) {
history.push(nextPathname) <= Not Supported
}
},
}
}
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root')
if (devRoot) {
mount(devRoot, { defaultHistory: createBrowserHistory() })
}
}
// We are running through container
// and we should export the mount function
export { mount }
Replacement marketing/src/bootstrap.tsx from remote (in progress)
import React from 'react'
import { createRoot } from 'react-dom/client'
import {
createBrowserRouter,
createMemoryRouter,
} from 'react-router-dom'
import App from './App'
import ErrorPage from './pages/ErrorPage'
import Landing from './components/Landing'
import Pricing from './components/Pricing'
let root: { render: (arg0: JSX.Element) => void } | null = null
const routes = [
{
path: '/',
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <Landing />,
errorElement: <ErrorPage />,
},
{
path: 'pricing',
element: <Pricing />,
errorElement: <ErrorPage />,
},
],
},
]
// Mount function to start up the app
const mount = (
el: Element,
{
onNavigate,
defaultRouter,
}: {
onNavigate: (() => void) | null
defaultRouter: any
},
): unknown => {
if (!el) {
root = null
return
}
// if in development and isolation, use browser router. If not, use memory router
const router = defaultRouter || createMemoryRouter(routes)
if (onNavigate) {
router.listen(onNavigate) // There is no history.listen anymore. router.listen is not a function
}
root = root ? root : createRoot(el)
root.render(<App router={router} />)
}
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root')
if (devRoot) {
mount(devRoot, { defaultRouter: createBrowserRouter(routes) })
console.log('defaultRouter')
}
}
// We are running through container
// and we should export the mount function
export { mount }
Original marketing/src/App.tsx from remote
import './MuiClassNameSetup'
import React from 'react'
import { Switch, Route, Router } from 'react-router-dom'
import Landing from './components/Landing'
import Pricing from './components/Pricing'
export default function _({ history }: any) {
return (
<div>
<Router history={history}>
<Switch>
<Route exact path="/pricing" component={Pricing} />
<Route path="/" component={Landing} />
</Switch>
</Router>
</div>
)
}
Replacement marketing/src/App.tsx from remote (in progress)
import './MuiClassNameSetup'
import React from 'react'
import {
RouterProvider,
} from 'react-router-dom'
export default function App({ router }: any) {
return <RouterProvider router={router} />
}
Original container/src/components/MarketingApp.tsx from host
import { mount } from 'marketing/MarketingApp'
import React, { useRef, useEffect } from 'react'
import { useHistory } from 'react-router-dom' <= Not Supported
export default function _() {
const ref = useRef(null)
const history = useHistory() <= Not Supported
useEffect(() => {
const { onParentNavigate } = mount(ref.current, {
initialPath: history.location.pathname,
onNavigate: ({ pathname: nextPathname }: any) => {
const { pathname } = history.location <= Not Supported
if (pathname !== nextPathname) {
history.push(nextPathname) <= Not Supported
}
},
})
history.listen(onParentNavigate) <= Not Supported
}, [history])
return <div ref={ref} />
}
Replacement container/src/components/MarketingApp.tsx from host (in progress)
import { mount } from 'marketing/MarketingApp'
import React, { useRef, useEffect } from 'react'
export default function _() {
const ref = useRef(null)
useEffect(() => {
mount(ref.current, {
onNavigate: () => {
console.log('The container noticed navigation in Marketing')
},
})
})
return <div ref={ref} />
}
Looking for a solution to replace history.listen, history.location and history.push that works with the new v6.4 data api's?
One of the maintainers of RRD just posted a new implementation detail to replace history.listen which is for v6.4+. See router.subscribe() below.
let router = createBrowserRouter(...);
// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');
// And instead of history.replace you can do:
router.navigate('/path', { replace: true });
// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));
Unfortunately, the new implementation is also unstable and is considered a beta test implementation.
Now, for the bad news 😕 . Just like unstable_HistoryRouter we also
consider this type of external navigation and subscribing to be
unstable, which is why we haven't documented this and why we've marked
all the router APIs as #internal PRIVATE - DO NOT USE in
JSDoc/Typescript. This isn't to say that they'll forever be unstable,
but since it's not the normally expected usage of the router, we're
still making sure that this type of external-navigation doesn't
introduce problems (and we're fairly confident it doesn't with the
introduction of useSyncExternalStore in react 18!)
If this type of navigation is necessary for your app and you need a
replacement for unstable_HistoryRouter when using RouterProvider then
we encourage you use the router.navigate and router.subscribe methods
and help us beta test the approach! Please feel free to open new GH
issues if you run into any using that approach and we'll use them to
help us make the call on moving that towards future stable release.

Mock Router in React testing library and jest

I'm writing unit test with React testing library and Jest and wants to check if my React Component is successfully able to navigate to next Page.
import { fireEvent, render, screen } from "#testing-library/react";
import React from 'react';
import { Provider } from 'react-redux';
import '#testing-library/jest-dom';
import appStore from '../../src/app/redux/store';
import { MemoryRouter, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const setup = (initialEntries = []) => {
let inMemHistory = createMemoryHistory({ initialEntries });
const utils = render(
<Router history={inMemHistory}>
<Provider store={appStore}>
<Component-1 />
</Provider>
</Router>
);
const saveButtonElem = screen.getByRole('button', { name: "Save and Continue" });
return {
saveButtonElem,
inMemHistory,
...utils,
}
};
Test:
test('should be able to navigate', async () => {
const {
saveButtonElem,
inMemHistory,
getByText,
queryByText,
queryAllByText,
} = setup(["/component_add"]);
// Content of Test
// Saving Config
fireEvent.click(saveButtonElem);
console.info("Current Path", inMemHistory.location.pathname);
// Got /component_add on console
// Expected /component_data after clicking on save button
})
I've tried waiting for 5 second after clicking save button and then tried to print path, but results are same.
Assuming you use react-router, You can use the Memory router for the testing which is easier and performant. I might have typos or syntax errors as I type without IDE support. But it should help you with idea on what I propose.
Option 1:
it("should route to the expected page", () => {
let mockHistory, mockLocation;
render(
<MemoryRouter initialEntries={["/currentUri"]}>
<Component1 />
// Dummy route that routes for all urls
<Route
path="*"
render={({ history, location }) => {
mockHistory= history;
mockLocation= location;
return null;
}}
/>
</MemoryRouter>
);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(mockLocation.pathname).toBe("/expectedUri");
});
Option 2:
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const renderWithHistory = (initialEntries= [], Component) => {
let inMemHistory = createMemoryHistory({
initialEntries
});
return {
...render(
<Router history={inMemHistory}>
<Component />
</Router >
), history };
};
it("should route to the expected page", () => {
const { history } = renderWithHistory(['/currentUri'], Component1);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(history.location.pathname).toBe("/expectedUri");
});

React SSR - React Router Dom staticContext undefined on the client

I am creating a blogging application using React with Server Side Rendering functionality. I create the application using Create React App.
I encounter an issue where my data coming from the server is rendered for a second then gone after react takes over the rendering process. I am using React Router Dom to pass the data from the server to the client react. Basically I am following this tutorial to create the application https://www.youtube.com/watch?v=NwyQONeqRXA but the video is lacking some information like getting the data on the API. So I reference this repository for getting the data on the API https://github.com/tylermcginnis/rrssr
Base on resources I gathered I ended up the following codes.
server.js
import express from "express";
import fs from "fs";
import path from "path";
import { StaticRouter, matchPath } from "react-router-dom";
import React from "react";
import ReactDOMServer from "react-dom/server";
import serialize from "serialize-javascript";
import App from "../src/App";
import routes from "../src/routes";
const app = express();
const PORT = 3001;
app.use(express.static(path.resolve(__dirname, "..", "build")));
app.get("*", (req, res, next) => {
// point to the html file created by CRA's build tool
const filePath = path.resolve(__dirname, "..", "build", "index.html");
fs.readFile(
path.resolve(filePath),
"utf-8",
(err, html_data) => {
if (err) {
console.error(err);
return res.status(500).send(`Some error happened: ${err}`);
}
const activeRoute =
routes.find(route => matchPath(req.url, route)) || {};
const promise = activeRoute.fetchInitialData
? activeRoute.fetchInitialData(req.path)
: Promise.resolve();
promise
.then(rawData => {
console.log('rawData', rawData[0]);
const context = { posts: rawData };
const markup = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
return res
.status(200)
.send(
html_data
.replace(
'<div id="root" class="container-fluid"></div>',
`<div id="root" class="container-fluid">${markup}</div>`
)
.replace(
"__INITIAL_DATA__={}",
`__INITIAL_DATA__=${serialize(rawData)}`
)
);
})
.catch(next);
}
);
});
app.listen(PORT, () => {
console.log(`App launched at http://localhost:${PORT}`);
});
Node JS Server entry point index.js
require("ignore-styles");
require("#babel/register")({
ignore: [/(node_modules)/],
presets: ["#babel/preset-env", "#babel/preset-react"]
});
require("./server");
Client react index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.css";
import "./index.css";
import "./fonts/VarelaRound-Regular.ttf";
ReactDOM.hydrate(
<BrowserRouter>
<App post_data={window.__INITIAL_DATA__} />
</BrowserRouter>,
document.getElementById("root")
);
Clieant react App.js
import React, { Component } from "react";
import "./App.css";
import { Route, Link, Redirect, Switch } from "react-router-dom";
import routes from "./routes";
class App extends Component {
render() {
return (
<div>
<Switch>
{routes.map(
({ path, exact, component: Component, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={props => (
<Component {...props} {...rest} />
)}
/>
)
)}
</Switch>
</div>
);
}
}
export default App;
When I am trying to output the data on the component it was gone and now staticContext pass from the server is undefined. What seems to be the issue here? Am I missing some configuration or library?
import React from "react";
export default class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: this.props.staticContext
? this.props.staticContext.posts
: []
};
console.log("constructor", this.props); // STATIC CONTEXT UNDEFINED ON THIS OUTPUT
}
componentDidMount() {
console.log("componentDidMount", this.props);// STATIC CONTEXT UNDEFINED ON THIS OUTPUT
}
}

ReactSSR: Expected server HTML to contain a matching <div> in <body>

Sorry to interrupt you guys to check my question, I have searched abot my question in stackoverflow and read this before: same question as mine
It mentioned this way to solve this warning, but I don't think this is a really good way to handle the question: suppressHydrationWarning={true} prop can also be used on the rendered element. However, as documentation points, this prop must be used scarcely. The better solution is using hydrate() or render() appropriately.
So, here's my trouble when I use React SSR:
I start up nodejs server, and then I request a route in browser.
When server received my request, it should be returned server render
template to browser.
I can see elements are rendered in first screen which means dom are successfully mounted in .
When I click a element which will trigger route in configuration can also render.For now, everything is all right.
Here comes a thing: when I refresh page in browser which route calls '/text1' or '/text2' will also comes a warning like my question's title: Expected server HTML to contain a matching in .
I suspect whether between my route in nodejs and some particular code in client didn't handle well which caused my question.
Here's my particular code[fake]:
// app.js
const http = require('http')
const fs = require('fs')
const path = require('path')
const demo = require('./demo')
const clientScripts = demo('Client')
let scriptsTag = ''
clientScripts.map((script) => {
scriptsTag += `<script src=${script}></script>`
})
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin","*");
res.setHeader("Access-Control-Allow-Headers","*");
// ssr
const ssrObj = require('./static/entry/serverEntry')
const dom = ssrObj.inital('server').dom
const store = ssrObj.inital('server').store
// const title = ssrObj.inital('server').title
console.log('in: ', dom)
res.setHeader("Content-Type","text/html;charset=utf-8");
res.end(`
<html>
<head>
<title>React & React Router4 SSR</title>
</head>
<body>
<!-- server side -->
<div id="root">${dom}</div>
<script>window.__PRELOADED_STATE__ = ${JSON.stringify(store)}</script>
<!-- ok with client side -->
${scriptsTag}
</body>
</html>
`);
});
server.listen(1234, () => {
console.log('开始监听1234端口')
})
// demo.js
const path = require('path')
const fs = require('fs')
let targetFile = ''
// suppose webpack configuration are ok, its' server output set in '/dist'
const fileList = fs.readdirSync(path.resolve(__dirname, '../dist'))
const container = []
module.exports = (params) => {
fileList.map((file) => {
const ext = path.extname(file).slice(1)
if (ext === 'js') {
const reg = new RegExp(`${params}`,"gim");
if (reg.test(file)) {
container.push(file)
}
}
})
return container
}
// /entry/serverEntry.js
require('babel-polyfill')
require('babel-register')({
presets: [ 'env' ]
})
const App = require('../common/initalEntry')
module.exports = App
// /entry/client.js
require('babel-polyfill')
require('babel-register')({
presets: [ 'env' ]
})
const App = require('../common/initalEntry')
App.inital('client').dom
// /common/initalEntry.js
import React from 'react';
// dom
import {hydrate} from 'react-dom' // client side use hydrate to replace render in react16
import {renderToString} from 'react-dom/server'
// router
import {StaticRouter, BrowserRouter} from 'react-router-dom'
// store
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import * as reducers from '../store/reducers'
import { App } from './App'
export function inital (url = '') {
if (url === 'server') {
console.log(1, url)
const serverStore = createStore(reducers.counter)
return {
dom: renderToString(
<Provider store={serverStore}>
<StaticRouter location={url} context={{}}>
<App type={url} />
</StaticRouter>
</Provider>
),
store: serverStore
}
} else if (url === 'client') {
console.log(2, url, App)
const clientStore = createStore(reducers.counter, window.__PRELOADED_STATE__)
delete window.__PRELOADED_STATE__
return {
dom: hydrate(
<Provider store={clientStore}>
<BrowserRouter>
<App type={url} />
</BrowserRouter>
</Provider>
, document.getElementById('root')
),
store: clientStore
}
}
}
// common/App.js
import React from 'react';
import {Route, Link} from 'react-router-dom'
class Text1 extends React.Component {
constructor (props) {
super(props)
}
render () {
return (
<div>i am text1.</div>
)
}
}
class Text2 extends React.Component {
render () {
return (
<div>i am text2.</div>
)
}
}
export class App extends React.Component {
constructor (props) {
super(props)
}
componentDidMount () {
console.log(this.props, '<<<<<<<')
}
goTo () {
}
render () {
return (
<div>
<Link to="/text1">go text1</Link>
<Link to="/text2">go text2</Link>
<Route path="/text1" component={Text1}></Route>
<Route path="/text2" component={Text2}></Route>
</div>
)
}
}
Above all, these are my configuration about react ssr which causes this question.Thanks to reviewing my question and pls give me some idea to handle this question.I will very apprciated to your help.
Here's my whole code: just viewing and running the server/ directory is ok
Again, thanks for your help.
I deleted the code in App.js, then no longer show the warning, here's my modify:
import React from 'react';
import {Route, Link} from 'react-router-dom'
export class App extends React.Component {
constructor (props) {
super(props)
}
goTo () {
console.log('click me')
}
render () {
return (
<div>
<p onClick={this.goTo.bind(this)}>123</p>
</div>
)
}
}
// I finally solve this problem when I find something a litte bit strange in /common/initalEntry.js
import React from 'react';
// dom
import {hydrate, render} from 'react-dom'
import {renderToString} from 'react-dom/server'
// router
import {StaticRouter, BrowserRouter} from 'react-router-dom'
// store
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import * as reducers from '../store/reducers'
import { App } from './App'
export function inital (type, url = '') {
if (type === 'server') {
const serverStore = createStore(reducers.counter)
return {
dom: renderToString(
<Provider store={serverStore}>
{/* What I pass in location is a empty value which calls 'url' which causes me a lot of time to figure out what happened.The right way is pass the request url received by node.js server to 'location', then no more warning */}
<StaticRouter location={url} context={{}}>
<App />
</StaticRouter>
</Provider>
),
store: serverStore
}
} else if (type === 'client') {
// ...ignore
}
}

Redux Store not populated before Component Render

I am working on the authentification procedure for an app I'm developing.
Currently, the user logins in through Steam. Once the login is validated the server redirects the user to the app index, /, and issues them a pair of JWTs as GET variables. The app then stores these in a Redux store before rewriting the URL to hide the JWT tokens for security purposes.
The app then decodes the tokens to obtain info about the user, such as their username and avatar address. This should be rendered in the app's SiteWrapper component, however, this is where my problem occurs.
What seems to be happening is SiteWrapper component loads before the App component finishes saving the tokens and thus throws errors as variables are not defined. Most of the fixes that seem relevant are for API requests, however, in this case, that is not the case. I already have the data in the URL. I'm not sure if the same applies.
Any suggestions on how to fix this? Any other best practice advice would be appreciated. I'm new to both React and Redux.
Error
Index
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from './redux/store';
// console debug setup
window.store = store;
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
serviceWorker.unregister();
App
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { connect } from 'react-redux';
import "tabler-react/dist/Tabler.css";
import history from './utils/history';
import {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
} from "./redux/app";
import {
HomePage
} from './pages';
class App extends Component {
componentDidMount() {
//Get tokens from URL when app loads and then hide them from url.
const urlParams = new URLSearchParams(window.location.search);
if(urlParams.has('access_token') && urlParams.has('refresh_token')){
this.props.storeRefreshJWTToken(urlParams.get('refresh_token'));
this.props.storeAccessJWTToken(urlParams.get('access_token'));
//Load user info from obtained tokens.
this.props.loadUserFromJWTRefreshToken();
}
history.push('/');
}
render() {
return (
<React.StrictMode>
<Router basename={process.env.PUBLIC_URL} history={history}>
<Switch>
<Route exact path="/" component={HomePage}/>
</Switch>
</Router>
</React.StrictMode>
);
}
}
const mapStateToProps = (state) => ({});
const mapDispatchToProps = {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
SiteWrapper
import * as React from "react";
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import {
Site,
Nav,
Grid,
List,
Button,
RouterContextProvider,
} from "tabler-react";
class SiteWrapper extends React.Component {
componentDidMount() {
console.log(this.props);
this.accountDropdownProps = {
avatarURL: this.props.user.avatar,
name: this.props.user.display_name,
description: "temp",
options: [
{icon: "user", value: "Profile"},
{icon: "settings", value: "Settings"},
{isDivider: true},
{icon: "log-out", value: "Sign out"},
],
};
}
render(){
return (
<Site.Wrapper
headerProps={{
href: "/",
alt: "Tabler React",
imageURL: "./demo/brand/tabler.svg",
navItems: (
<Nav.Item type="div" className="d-none d-md-flex">
<Button
href="https://github.com/tabler/tabler-react"
target="_blank"
outline
size="sm"
RootComponent="a"
color="primary"
>
Source code
</Button>
</Nav.Item>
),
accountDropdown: this.accountDropdownProps,
}}
navProps={{ itemsObjects: this.props.NavBarLinks }}
routerContextComponentType={withRouter(RouterContextProvider)}
footerProps={{
copyright: (
<React.Fragment>
Copyright © 2018
Thomas Smyth.
All rights reserved.
</React.Fragment>
),
nav: (
<React.Fragment>
<Grid.Col auto={true}>
<List className="list-inline list-inline-dots mb-0">
<List.Item className="list-inline-item">
Developers
</List.Item>
<List.Item className="list-inline-item">
FAQ
</List.Item>
</List>
</Grid.Col>
</React.Fragment>
),
}}
>
{this.props.children}
</Site.Wrapper>
);
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
user: state.App.user
};
};
export default connect(mapStateToProps)(SiteWrapper);
Reducer
import initialState from './initialState';
import jwt_decode from 'jwt-decode';
//JWT Auth
const STORE_JWT_REFRESH_TOKEN = "STORE_JWT_REFRESH_TOKEN";
export const storeRefreshJWTToken = (token) => ({type: STORE_JWT_REFRESH_TOKEN, refresh_token: token});
const STORE_JWT_ACCESS_TOKEN = "STORE_JWT_ACCESS_TOKEN";
export const storeAccessJWTToken = (token) => ({type: STORE_JWT_ACCESS_TOKEN, access_token: token});
// User
const LOAD_USER_FROM_JWT_REFRESH_TOKEN = "DEC0DE_JWT_REFRESH_TOKEN";
export const loadUserFromJWTRefreshToken = () => ({type: LOAD_USER_FROM_JWT_REFRESH_TOKEN});
export default function reducer(state = initialState.app, action) {
switch (action.type) {
case STORE_JWT_REFRESH_TOKEN:
return {
...state,
jwtAuth: {
...state.jwtAuth,
refresh_token: action.refresh_token
}
};
case STORE_JWT_ACCESS_TOKEN:
return {
...state,
JWTAuth: {
...state.jwtAuth,
access_token: action.access_token
}
};
case LOAD_USER_FROM_JWT_REFRESH_TOKEN:
const user = jwt_decode(state.jwtAuth.refresh_token);
return {
...state,
user: user
};
default:
return state;
}
}
Store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk'
import App from './app';
const combinedReducers = combineReducers({
App
});
const store = createStore(combinedReducers, applyMiddleware(ReduxThunk));
export default store;
componentDidMount() is going to run after first render. So on first render the this.props.user inside will be null at that point.
You could:
move the async call to componentWillMount() (not recommended)
put a guard in the SiteWrapper() so it can handle null case
not render Home from the App until the async call has finished
in componentDidMount() you are expecting user to be set.
You can:
Set user in initial state with some "isAuth" flag that you can easily check
As componentWillMount() is deprecated you can use componentDidUpdate() to check if user state is changed and do some actions.
Use asyns functions when sawing YWT
You could of course check for null and don't try to show the user's details in your SiteWrapper's componentDidMount()-method. But why? Do you have an alternative route to go if your user couldn't be found and is null? I guess no. So you basically have two options:
The ideal solution is to implement async actions and show an activity
indicator (e.g. spinner) until the jwt-token is loaded. Afterwards
you can extract your user information and fully render your
component as soon as the fetch is succesfully completed.
If you can't use async action, for whatever reason, I would suggest
the "avoid-null" approach. Put a default user in your initial
state and it should be done. If you update the user prop, the
component will rerender anyways (if connected properly).
I have solved my issue. It seems this was one of those rare cases where trying to keep things simple and develop was bad.
Now, when my application loads I either show the user information or a login button.
Once the key is loaded from the URL the component rerenders to show the user information.
This does increase the number of renders, however, it is probably the best way of doing it.

Resources