The function below is meant to check if user is authorized before rendering a route with react-router. I got the basic code from somewhere I forgot.
This was working fine when I had a simple synchronous check to local storage, but when I introduced an Axios call to the server things got messy.
I read a lot of the SO's questions on like issues, ie, promises not returning value and I seem to have modified my code to conform to the regular pitfalls.
I particularly used the last answer to this question to fix my setup, but the issue remains.
On the code below, the console.logs output the parameters correctly, meaning that the failure with related to the return statement.
The specific error is:
Error: AuthRoute(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import axios from 'axios';
const PRIVATE_ROOT = '/account';
const PUBLIC_ROOT = '/';
const RenderRoute = ({response, isPrivate, Route, Redirect, component, ...props}) => {
console.log('is_logged', response.data.is_logged); // boolean
console.log('isPrivate', isPrivate); // boolean
console.log('response', response); // axios object
console.log('route', Route); // function route
console.log('component', component); // home component
console.log('props', {...props}); // route props
if (response.data.is_logged) {
// set last activity on local storage
let now = new Date();
now = parseInt(now.getTime() / 1000);
localStorage.setItem('last_active', now );
return isPrivate
? <Route { ...props } component={ component } />
: <Route { ...props } component={ component } />;
} else {
return isPrivate
? <Redirect to={ PUBLIC_ROOT } />
: <Route { ...props } component={ component } />;
}
}
const AuthRoute = ({component, ...props}) => {
const { isPrivate } = component;
let last_active_client = localStorage.getItem('last_active') ? localStorage.getItem('last_active') : 0;
let data = {
last_active_client: last_active_client
}
let getApiSession = new Promise((resolve) => {
resolve(axios.post('/api-session', data));
});
getApiSession.then(response => RenderRoute({response, isPrivate,Route, Redirect, component, ...props})
).catch((error) => {
console.log(error);
});
}
export default AuthRoute;
This is what worked for me after comments above. It was necessary to create a separate component class. It's not tremendously elegant, but works.
The code needs to be placed into componentWillReceiveProps() for it to update at each new props. I know componentWillReceiveProps() is being deprecated. I will handle that separately.
/auth/auth.js
import React from 'react';
import RenderRoute from './components/render_route';
const AuthRoute = ({component, ...props}) => {
let propsAll = {...props}
return (
<RenderRoute info={component} propsAll={propsAll} />
)
}
export default AuthRoute;
/auth/components/render_route.js
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import axios from 'axios';
const PRIVATE_ROOT = '/account';
const PUBLIC_ROOT = '/';
class RenderRoute extends React.Component {
constructor (props) {
super(props);
this.state = {
route: ''
}
}
componentWillReceiveProps(nextProps, prevState){
const { isPrivate } = this.props.info;
let last_active_client = localStorage.getItem('last_active') ? localStorage.getItem('last_active') : 0;
let data = {
last_active_client: last_active_client
}
axios.post('/api-session', data).then(response => {
let isLogged = response.data.is_logged;
if(response.data.is_logged) {
// set last activity on local storage
let now = new Date();
now = parseInt(now.getTime() / 1000);
localStorage.setItem('last_active', now );
this.setState({ route: isPrivate
? <Route { ...this.props.propsAll } component={ this.props.info } />
: <Route { ...this.props.propsAll } component={ this.props.info } />
})
} else {
this.setState({ route: isPrivate
? <Redirect to={ PUBLIC_ROOT } />
: <Route { ...this.props.propsAll } component={ this.props.info } />
})
}
}).catch((error) => {
console.log(error);
});
}
render() {
return (
<div>
{this.state.route}
</div>
)
}
}
export default RenderRoute;
Related
i have an application in react and I want to protect all pages with a password
i have header navbar and footer
and when I do this with protected route class if the mail and password are good it redirect me to the component without NavBar and header, only component
how can I protect display all page
<ProtectedRoute exact path="/" component={analyticsDashboard} />
with
import React from 'react'
import { Redirect } from 'react-router-dom'
import { is } from '#babel/types';
import AppRouter from '../Router';
class ProtectedRoute extends React.Component {
render() {
const Component = this.props.component;
const isAuthenticated = localStorage.getItem('token');
if(isAuthenticated==='0')
{
return <Redirect to={{ pathname: '/pages/login' }} />
}
return <Component/>
}
}
export default ProtectedRoute;
and thanks to tell me which method can I use on production instead of local storage
you can create a HOC as following
first call this function when you want to set the token value to local storage
// call this function in the root component when you want to set the token
function setLocalStorage (key, value) {
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
const event = new Event('itemInserted');
event.value = value;
event.key = key;
document.dispatchEvent(event);
originalSetItem.apply(this, arguments);
};
}
Second, create a hoc
import { Redirect } from 'react-router-dom'
export default function(ComposedComponent) {
class Authentication extends Component {
state = {
isAuthenticated: !!localStorage.getItem('token')
};
localStorageSetHandler({ key, value }) {
if (key !== 'token') return;
this.setState(({
isAuthenticated: !!value,
}))
}
componentDidMount() {
document.addEventListener("itemInserted", this.localStorageSetHandler, false);
}
render() {
const { isAuthenticated } = this.state;
if (!isAuthenticated) {
return <Redirect to={{ pathname: '/pages/login' }} />;
}
return <ComposedComponent />;
}
}
}
then use the hoc you've created to create protected routes like so
<Route
path={'/path'}
component={RequireAuth(lazy(() => import('./component path')))} // or you can use component={RequireAuth(YourComponent)}
/>
I made a react library converting menu data returned by microservice to dynamic route.
project structure image is below:
--react-app
----node_modules
------ ...
------ rb-react ---> my react library
------ ...
----src
------pages
--------Dashboard.js
--------Prod.js
--------User.js
-------- ...
library rb-react can not import pages, ie Dashboard.js
let rootPath = path.resolve(__dirname, '../src/pages');
let menuPath = menuCommand.replace('#', '');
let importObject = asyncComponent(() =>
import(rootPath + '/' + menuCommand.split('/')[1])
);
if (isEmpty(routeResultArray)) {
routeResultArray.push(
<Route key={menuKey} exact path='/' component={importObject} />
);
}
node --version
v12.6.0
npm --version
6.11.3
Here is some code of my project
dynamic import class
export default function asyncComponent(importComponent) {
class RbImport extends Component {
constructor(props) {
super(props);
this.state = {
component: null
};
}
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : <></>;
}
}
return RbImport;
}
dynamic route
import React from 'react';
import path from 'path';
import { HashRouter, Route, Switch } from 'react-router-dom';
import ScrollToTop from './ScrollToTop';
import App from '../../App';
import { RbLibraryComponent } from './RbLibraryComponent';
import { RbLocalStorage } from '../util/RbLocalStorage';
import { NotFound } from '../notfound/NotFound';
import { isEmpty } from '../util/RbUtil';
import asyncComponent from '../util/RbImport';
export class RbRoute extends RbLibraryComponent {
constructor() {
super();
this.state = {};
this.RbLocalStorage = new RbLocalStorage();
this.composeRoute = this.composeRoute.bind(this);
this.recursiveComposeRoute = this.recursiveComposeRoute.bind(this);
}
composeRoute() {
let routeResultArray = [];
let userMenus = this.RbLocalStorage.loadUserMenu();
this.recursiveComposeRoute(routeResultArray, userMenus, null);
return routeResultArray;
}
recursiveComposeRoute(routeResultArray, menuDataList, prefixKey) {
if (isEmpty(menuDataList)) {
return;
}
prefixKey = isEmpty(prefixKey) ? '' : prefixKey;
let userMenuCount = menuDataList.items.length;
for (let idx = 0; idx < userMenuCount; idx++) {
let userMenu = menuDataList.items[idx];
if (isEmpty(userMenu)) {
continue;
}
let menuKey = isEmpty(prefixKey) ? idx : prefixKey + '-' + idx;
let menuCommand = userMenu.command;
if (isEmpty(menuCommand)) {
//nothing
} else {
let rootPath = path.resolve(__dirname, '../src/pages');
let menuPath = menuCommand.replace('#', '');
let importObject = asyncComponent(() =>
import(rootPath + '/' + menuCommand.split('/')[1])
);
if (isEmpty(routeResultArray)) {
routeResultArray.push(
<Route key={menuKey} exact path='/' component={importObject} />
);
}
routeResultArray.push(
<Route key={menuKey} path={menuPath} component={importObject} />
);
}
if (isEmpty(userMenu.items)) {
//nothing
} else {
this.recursiveComposeRoute(routeResultArray, userMenu, menuKey);
}
}
}
render() {
let { menu, productionName, userName } = this.props;
if (menu === null || menu === undefined) {
return <></>;
}
return (
<HashRouter>
<ScrollToTop>
<App menu={menu} userName={userName}>
<Switch>
{this.composeRoute()}
<Route component={NotFound} />
</Switch>
</App>
</ScrollToTop>
</HashRouter>
);
}
}
export default RbRoute;
Exception Message at Chrome Console
Uncaught (in promise) Error: Cannot find module '/src/pages/Dashboard'
at webpackContextResolve (^.*$:29)
at webpackContext (^.*$:24)
at RbRoute.js:221 --> (Compiled Source) return _interopRequireWildcard(require("".concat(rootPath + '/' + menuCommand.split('/')[1])));
If you are trying to use async import, you would need to specify the path to the file that you are trying to import. Only then , webpack would be able to create a separate chunk for that file. Here, the way i understand, you are trying to make your import statements dynamic. That would not work. You would need to specify the full path of each of the async import that you make. (this path needs to be available at build time)
Could you please check out require.context if you need to load all modules in a directory.
https://webpack.js.org/guides/dependency-management/
Hi I've seen this same error and multiple possible solutions but none has been able to solve my issue (Probably because I'm lacking in depth understanding of the whole React structure).
I know that context.insertCss.apply(context, styles); isn't receiving the context and that's why the error is thrown, I've added the ContextProvider but I'm afraid this could be conflicting with my routing setup. Also used Daniel's answer to this question [Why does isomorphic-style-loader throw a TypeError: Cannot read property 'apply' of undefined when being used in unison with CSS-Modules
server index.js
app.get('/*', (req, res) => {
const matchingRoutes = matchRoutes(Routes, req.url);
let promises = [];
matchingRoutes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData());
}
});
// promise.then(data => {
Promise.all(promises).then(dataArr => {
// Let's add the data to the context
// const context = { data };
// const context = { dataArr };
const css = new Set()
const context = { insertCss: (...styles) => styles.forEach(style => css.add(style._getCss()))}
const app = React.renderToString(
<StaticRouter location={req.url}>
<ContextProvider context={context}>
<App/>
</ContextProvider>
</StaticRouter>
)
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, indexData) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
if (context.status === 404) {
res.status(404);
}
if (context.url) {
return res.redirect(301, context.url);
}
return res.send(
indexData
.replace('<style id="myStyle"></style>',`<style type="text/css" id="myStyle">${[...css].join('')}</style>`)
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'</body>',
`<script>window.__ROUTE_DATA__ = ${serialize(dataArr)}</script></body>`
)
);
});
});
});
Added on the server the ContextProvider in the renderToString(..) method, also I'm replacing the html body so the received CSS is attached to the HTML response.
ContextProvider.js
import React from 'react';
import PropTypes from 'prop-types'
import App from './App'
class ContextProvider extends React.Component {
static childContextTypes = {
insertCss: PropTypes.func,
}
getChildContext() {
return {
...this.props.context
}
}
render() {
return <App {
...this.props
}
/>
}
}
export default ContextProvider
Used the context provider from Daniel's answer (Reference above)
Client index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import ContextProvider from './ContextProvider';
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss());
return () => {
removeCss.forEach(f => f());
};
},
}
ReactDOM.hydrate(
<BrowserRouter>
<ContextProvider context={context} />
</BrowserRouter>,
document.getElementById('root')
);
Passing the context through the ContextProvider as supposed.
App.js used inside the ContextProvider
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';
import Routes from './routes';
export default props => {
return (
<div>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/todos">Todos</NavLink>
</li>
<li>
<NavLink to="/posts">Posts</NavLink>
</li>
</ul>
<Switch>
{renderRoutes(Routes)}
</Switch>
</div>
);
};
Home.js where I'm trying to test the custom style
import React from 'react';
import withStyles from '../../node_modules/isomorphic-style-loader/withStyles'
import styles from '../scss/Home.scss';
function Home(props, context) {
return (
<h1>Hello, world!</h1>
)
}
export default withStyles(styles)(Home);
routes.js describes the routes used.
import Home from './components/Home';
import Posts from './components/Posts';
import Todos from './components/Todos';
import NotFound from './components/NotFound';
import loadData from './helpers/loadData';
const Routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts')
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos')
},
{
component: NotFound
}
];
export default Routes;
Almost sure there is an easy fix for this issue but it doesn't seem so trivial to me. Thank you in advance.
Please try to use the built in StyleContext of isomorphic-style-loader instead of custom context provider.
server.js:
import StyleContext from 'isomorphic-style-loader/StyleContext';
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss());
return () => removeCss.forEach(dispose => dispose());
};
ReactDOM.render(
<StyleContext.Provider value={{ insertCss }}>
<Router>{renderRoutes(Routes)}</Router>
</StyleContext.Provider>,
document.getElementById('root')
);
client.js:
app.get('/*', function(req, res) {
const context = {};
const css = new Set(); // CSS for all rendered React components
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()));
const component = ReactDOMServer.renderToString(
<StyleContext.Provider value={{ insertCss }}>
<StaticRouter location={req.url} context={context}>
{renderRoutes(Routes)}
</StaticRouter>
</StyleContext.Provider>
);
if (context.url) {
res.writeHead(301, { Location: context.url });
res.end();
} else {
res.send(Html('React SSR', component));
}
});
You can see example project here: https://github.com/digz6666/webpack-loader-test/tree/ssr-2
Background:
I am practicing the idea of React/Redux. I would want to follow the flow of data.
axios dispatches action -> reducer setState to props -> Component render()
The problem may be more than 1 point. Because I am new to Frontend world.
Please feel free to re-design my app(if needed)
Problem:
company does not render out because this.props.companies is blank. But axios does fetch the array from backend.
action/index.js
//First experiment action returns promise instance
export function fetchCompanies(token) {
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return {
type: FETCH_COMPANIES,
payload: instance
}
}
export function getCompanies(token){
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return instance
.then(data=> store.dispatch('GET_COMPANIES_SUCCESS', data));
}
company_reducers.js
import {FETCH_COMPANIES, GET_COMPANIES_ERROR, GET_COMPANIES_SUCCESS} from "../actions/const";
export default function (state = {}, action) {
switch (action.type) {
case GET_COMPANIES_SUCCESS:
return {
...state,
companies: action.payload
};
case GET_COMPANIES_ERROR:
return {
...state,
err_msg: action.payload.text
};
default:
return state;
}
}
reducers/index.js
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import LoginReducer from './login_reducers';
import CompanyReducer from './company_reducers';
const rootReducer = combineReducers({
login: LoginReducer,
companies: CompanyReducer,
form: formReducer
});
export default rootReducer;
component/select_teams.js
import _ from 'lodash';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchCompanies, getCompanies} from "../actions";
import {Link} from 'react-router-dom';
class SelectTeam extends Component {
constructor(props) {
super(props);
const token = localStorage.getItem('token');
this.state = {
token,
companies: null,
err_msg: null
}
}
componentWillMount() {
const tmp = this.props.getCompanies(this.state.token);
tmp.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
};
renderErrors() {
return (
<div>{this.state.err_msg}</div>
);
}
renderCompanies() {
return _.map(this.props.companies, company => {
return (
<li className="list-group-item" key={company.id}>
<Link to={`/${company.id}`}>
{company.name}
</Link>
</li>
)
});
}
render() {
if (this.props.companies === null) {
return (
<div>Loading...</div>
);
}
console.log(this.props);
return (
<div>
<h3>❤ Select Team ❤</h3>
{this.renderErrors()}
{this.renderCompanies()}
</div>
);
}
}
function mapStateToProps(state){
return {companies: state.companies}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
fetchCompanies: fetchCompanies,
getCompanies: getCompanies
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
App.js
import React, {Component} from 'react';
import './App.css';
import SelectTeam from "./components/select_teams";
import reducers from './reducers/index';
import {Provider} from 'react-redux';
import promise from "redux-promise";
import {applyMiddleware, createStore} from 'redux';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import LoginPage from './components/loginPage';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const PrivateRoute = ({component: Component, isAuthorized, ...otherProps}) => (
<Route
{...otherProps}
render={props => (
isAuthorized() ? (<Component {...props} />) :
(
<Redirect to={
{
pathname: '/login',
state: {from: props.location},
}
}
/>
)
)}
/>
);
function PageNotFound() {
return (
<div>404 Page Not Found</div>
);
}
// TODO: I will add RESTful validation with backend later
function hasToken() {
const token = localStorage.getItem('token');
const isAuthenticated = !((token === undefined) | (token === null));
return isAuthenticated;
}
export const store = createStoreWithMiddleware(reducers);
class App extends Component {
//I will add security logic with last known location later.
//Get the features done first
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<PrivateRoute exact path="/select-teams" isAuthorized={hasToken} component={SelectTeam}/>
<Route path="/login" component={LoginPage}/>
<Route component={PageNotFound}/>
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
You should dispatch an action with the data fetched from the server.
Actions are pure functions that return an object (the object has at minimum a TYPE field).
If you have any async operations, you may use Redux-Thunk, which is an action creator that returns a function, and call the api fetch within it.
Here is the actions snippet:
// imports..
export const fetchCompaniesSuccess = (data) => {
retyrn {
type: FETCH_COMPANIES_SUCCESS,
data
}
}
export const fetchCompanies = (token) => dispatch => {
// ...
axios(...).then(dispatch(data => fetchCompaniesSuccess(data)))
}
In your company_reducers.js,
// Company Reducer Function, State here represents only the companies part of the store
case FETCH_COMPANIES_SUCCESS: // should match the the type returned by the action
return [
...state,
...action.data
]
// other cases & default
MAKE SURE to add redux-thunk as a middleware in your createStore, read Redux-Thunk doc for instructions.
then in you component:
componentDidMount(){
this.props.fetchCompanies(this.state.token);
}
Once companies data is added to the redux store, your component will rerender and the companies array will be available in props
You don't need to have a duplicate companies array in the component state.
You may want to Watch Dan Abramov introduction to redux, it is a free course.
Seems like your dispatch syntax is wrong. The parameter should be an object with type and payload.
return instance
.then(data=> store.dispatch({
type: 'GET_COMPANIES_SUCCESS',
payload: data
}));
I googled it but still don't know how to use import() along with react-router-dom (version 4.x) to achieve code-splitting/async-route effect.
I am using webpack2
The doc of react-router-dom use bundle-loader. But I thought import() is supported natively by webpack2. Is there a way to use import() directly?
Thanks
https://github.com/brotzky/code-splitting-react-webpack
https://brotzky.co/blog/code-splitting-react-router-webpack-2/
Checkout this out, I follow the steps and finish dynamic code require for my project.
Anyway, I figure it out myself.
It kind of works at the moment. But I've test it in real world. APPRECIATE to point out bugs in the snippet.
Thanks.
First have a component like this
//#flow
import React from "react";
import { Loading } from "../loading";
let map = {};
export function AsyncLoad(props: {
moduleKey: string,
loadedFrom: Promise<any>,
onLoaded: () => void
}) {
let Comp = map[props.moduleKey];
if (Comp) {
let passedProp = Object.assign({}, props);
delete passedProp.moduleKey;
delete passedProp.loadedFrom;
return <Comp {...props} />;
} else {
processModule();
return <Loading />;
}
async function processModule() {
const mod = await props.loadedFrom;
map[props.moduleKey] = mod.default;
props.onLoaded();
}
}
Then in my App.js, do like this:
//#flow
import React, { PureComponent } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { AsyncLoad } from "./asyncLoad";
import "./app.scss";
export class App extends PureComponent {
render() {
return (
<Router>
<div>
<Link to="/quiz">Show Quiz</Link>
<div className="content">
<Route
path="/quiz"
component={() =>
<AsyncLoad
moduleKey="quiz"
loadedFrom={import("./quiz")}
onLoaded={() => {
this.forceUpdate();
}}
/>}
/>
</div>
</div>
</Router>
);
}
}
very simple
<Route path="/page3" render={() => (
<AC loader={import('./Page3')} />
)} />
class AC extends Component {
state = {
_: null
}
async componentDidMount() {
const { default: _ } = await this.props.loader;
this.setState({
_: <_ />
});
}
render() {
return (
<div>{this.state._}</div>
);
}
}