I have an application that uses the same layout for all routes... except one.
One route will be completely different than all others.
So the entire application will have a menu, body, footer, etc.
The one-off route will not have any of that and be a completely separate thing.
How should I set this kinda thing up in a react app? Everything I've ever seen/done always has one main wrapping element that has the routes rendered as children.
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import configureStore from './store'
import App from './components/App'
// import registerServiceWorker from './registerServiceWorker'
import { unregister } from './registerServiceWorker'
const preloadedState = window.__PRELOADED_STATE__ ? window.__PRELOADED_STATE__ : {}
// console.log('window.__PRELOADED_STATE__', window.__PRELOADED_STATE__)
delete window.__PRELOADED_STATE__
const Store = configureStore(preloadedState)
const rootEl = document.getElementById('root')
ReactDOM.hydrate(
<Provider store={Store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
rootEl
)
if(module.hot){
module.hot.accept('./components/App', () => {
ReactDOM.hydrate(
<Provider store={Store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
rootEl
)
})
}
// registerServiceWorker()
unregister()
App.js
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
// Components
import AppHelmet from './AppHelmet'
import Notices from './Notices'
import Header from './Header'
import Body from './Body'
import Footer from './Footer'
// Site state
import { getSiteInfo } from '../store/actions/siteInfo'
import { REACT_APP_SITE_KEY } from '../shared/vars'
// CSS
import '../css/general.css'
class App extends Component {
initialAction() {
this.props.getSiteInfo(REACT_APP_SITE_KEY)
}
componentWillMount() {
// On client and site info has not been fetched yet
if(this.props.siteInfo.site === undefined){
this.initialAction()
}
}
render() {
return (
<div>
<AppHelmet {...this.props} />
<Notices />
<div className="body">
<Header />
<Body />
</div>
<Footer />
</div>
)
}
}
const mapStateToProps = (state) => {
return {
siteInfo: state.siteInfo,
user: state.user
}
}
const mapDispatchToProps = (dispatch) => {
return {
getSiteInfo: (siteKey) => dispatch(getSiteInfo(siteKey))
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App))
Body.js
import React, { Component } from 'react'
import { Switch, Route } from 'react-router-dom'
import routes from '../shared/routes'
class Body extends Component {
render() {
return (
<Switch>
{routes.map((route, i) => <Route key={i} {...route} />)}
</Switch>
)
}
}
export default Body
So, as you can see the index.js entry point will render <App />. <App /> will render the main layout, including <Body />, which renders all routes and content.
Cool.
But seeing as I don't want this one-off to render the <App /> layout, I'm not sure how to set this up from index.js. I'm sure it's simple and I'm just not seeing the answer.
One way to achieve what you want is to listen to the router.
You can add the listener into the components you want to hide.
When the listener detects you're on a view where you do not want the components to show, simply don't render them for that view.
Related
I have information in the state (true or false) that I want to display if is true this Navbar component, but when I use the hook, I get an error message:
hook error
My code:
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import store, { history } from './reduxStore';
import AppRouterContainer from './pages/AppRouterContainer';
import Feedback from './pages/feedback/Feedback';
import Navbar from './components/Navbar/Navbar';
import { useTypedSelector } from '../src/hooks/useTypedSelector';
const isAuth = useTypedSelector((state) => state.auth.isAuth);
const App = () => (
<BrowserRouter>
<Provider store={store}>
<ConnectedRouter history={history}>
<AppRouterContainer />
{isAuth && (
<Navbar />
)}
<Feedback />
</ConnectedRouter>
</Provider>
</BrowserRouter>
);
export default App;
You need to create a wrapper component to have access to store in your context (I think your useTypedSelector() hook needs that access).
You can use hooks only inside a function, not just inside a module.
Check out this example:
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { useTypedSelector } from '../src/hooks/useTypedSelector';
import Navbar from './components/Navbar/Navbar';
import AppRouterContainer from './pages/AppRouterContainer';
import Feedback from './pages/feedback/Feedback';
import store, { history } from './reduxStore';
const NavbarWrapper = () => {
const isAuth = useTypedSelector((state) => state.auth.isAuth);
if (!isAuth) {
return null;
}
return <Navbar />;
};
const App = () => (
<BrowserRouter>
<Provider store={store}>
<ConnectedRouter history={history}>
<AppRouterContainer />
<NavbarWrapper />
<Feedback />
</ConnectedRouter>
</Provider>
</BrowserRouter>
);
export default App;
Also, I think you should move the NavbarWrapper component to a separate file.
I used to have Provider context wrapped around my top React component: App. I then decided to move the context directly to ReactDOM.render so that I can use redux connect in App. I want to use connect so that I can use a loader (LinearProgress) as the top component in my app. So I will import loading from props and display a loader accordingly (loading could be toggled from some inner component).
However I keep getting this error:
Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
The page is still rendered normally but I have the above error in the console. I checked the line which causes the error is the following one (inside App):
{loading && <LinearProgress color='secondary'/>}
If I remove the line then no errors are reported in the console. Why conditional display of loader inside App causes the error?
This is the code of the App component:
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import LinearProgress from '#material-ui/core/LinearProgress';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import SearchAppBar from './components/layout/MaterialHeader.js';
import AppInfo from './components/appOperations/AppInfo.js';
import About from './components/layout/About.js';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import constants from './constants';
class App extends Component {
render() {
const { loading } = this.props;
return (
<Router>
<div className="App">
{loading && <LinearProgress color='secondary'/>}
<SearchAppBar/>
<Switch>
<Route exact path="/" />
<Route exact path={constants.CLIENT_ROUTES.APP} component={AppInfo}/>
<Route exact path="/about" component={About}/>
</Switch>
</div>
</Router>
);
}
}
App.propTypes = {
loading: PropTypes.bool.isRequired
};
const mapStateToProps = state => ({
loading: state.app.loading
});
export default connect(mapStateToProps, null)(App);
This is the code of the parent and top-most component of App:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import MuiThemeProvider from '#material-ui/core/styles/MuiThemeProvider';
import App from './App';
import MaterialBlueTheme from './components/layout/MaterialBlueTheme.js';
import * as serviceWorker from './serviceWorker';
import store from './store.js';
ReactDOM.render(
<Provider store={store}>
<MuiThemeProvider theme={MaterialBlueTheme}>
<App />
</MuiThemeProvider>
</Provider>
, document.getElementById('root')
);
serviceWorker.unregister();
Look for a one of your components that is trying to change the state within the render method. You can't do this:
render() {
this.setState({
...
});
return (...);
}
I created a simple react application. It has a header and three other components called welcome, feature 1 and feature 2.
index.js
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Route } from 'react-router-dom'
import App from './App';
render((
<BrowserRouter>
<Route path="/" component={App} />
</BrowserRouter>
), document.getElementById('root'));
App.js
import React from 'react'
import Header from './Header'
import Main from './Main'
const App = () => (
<div>
<Header />
<Main />
</div>
)
export default App
Header.js
import React, { Component } from 'react';
export default class Header extends Component {
componentDidMount() {
APICall('/user')
}
render() {
return (
<div>I am header</div>
)
}
}
Main.js
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import Welcome from './Welcome'
import Feature1 from './Feature1'
import Feature2 from './Feature2'
export default class Main extends Component {
render() {
return (
<div>
<Route exact path="/" component={Welcome} />
<Route path="/feature1" component={Feature1} />
<Route path="/feature2" component={Feature2} />
</div>
)
}
}
welcome.js
import React, { Component } from 'react';
export default class Welcome extends Component {
render() {
return (
<div>Welcome!</div>
)
}
}
Feature1.js
import React, { Component } from 'react';
export default class Feature1 extends Component {
render() {
return (
<div>I am Feature1</div>
)
}
}
Feature2.js
import React, { Component } from 'react';
export default class Feature2 extends Component {
render() {
return (
<div>I am Feature2</div>
)
}
}
Welcome, Feature1 and Feature2 are in different routes where as Header is common in all the routes. Say I have a user and I want to show the username on the header. I will make an API call to get the username in componentDidMount() life-cycle hook of header.
Now if I change the route, I don't want the API call to be made again as the username is not going to change. And I thought that is how this was going to behave. As Header component is same in all the routes, I thought Header won't re-mount when I change the route. But that is not what is happening. It is remounting and making the API call again. How can I make sure that the API call is made only once?
I think in this case considering your Header is aware of what User is logged, i.e. App.js state passed down as props, you could use shouldComponentUpdate():
shouldComponentUpdate(nextProps, nextState) {
// see if user changed
if(nextProps.user !== nextState.user){
return true;
}
// returning false will prevent re-rendering
return false;
}
I hope this is somehow useful.
Have a good day!
change to this in index.js:-
<BrowserRouter>
<App />
</BrowserRouter>
It seems you messed up the routing then, because in your router you have only one route registered, which seems to trigger on any child route change. Also, routes should be wrapped in a Switch component.
Check out this structure
/** index.js */
import React from 'react'
import { render } from 'react-dom'
import App from './App';
render(<App />, document.getElementById('root'));
/** App.jsx */
import React from 'react'
import Header from './Header'
import Main from './Main'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
const App = () => (
<div>
<Header />
<BrowserRouter>
<Switch>
<Route exact path="/" component={Homepage} />
<Route exact path="/f1" component={Feature1} />
<Route exact path="/f2" component={Feature2} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</div>
)
export default App
index.js
import React, { Component } from 'react';
import { AppRegistry } from 'react-native';
import Login from './src/screens/Login';
import Secured from './src/screens/Secured';
import NewPass from './src/screens/NewPass';
import { Provider } from 'react-redux';
import store from './store'; //Import the store
class ReactNativeStormpath extends Component {
state = {
isLoggedIn: false
}
render() {
if (this.state.isLoggedIn)
return (
<Provider store={store}>
<Secured
onLogoutPress={() => this.setState({isLoggedIn: false})}
/>
</Provider>
)
else
return (
<Provider store={store}>
<Login
onLoginPress={() => this.props.navigation.navigate('NewPass')}
/>
</Provider>
)
}
}
AppRegistry.registerComponent('ReactNativeStormpath', () => ReactNativeStormpath);
I receive "undefined is not an object", or the button doesn´t do anything,
i've followed almost all post and couldn't find a way to navigate between views in react native, this can't be that difficult.
I am unable to make the store available to children components.
The setup is a SPA with Symfony as back-end, though this should not make a difference for this matter.
The entry point for Webpack is the file:
/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import ReduxPromise from 'redux-promise';
import Root from './App';
import registerServiceWorker from './registerServiceWorker';
import reducers from './pages/combine_reducers';
let composeEnhancers = typeof(window) !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
reducers,
composeEnhancers(
applyMiddleware(ReduxPromise)
)
)
ReactDOM.render(
<Root store={store} />
, document.querySelector('#root')
);
registerServiceWorker();
The apps as such is at:
/client/App.js
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import {
BrowserRouter as Router,
Route,
Link,
Switch
} from 'react-router-dom';
import HomePage from './pages/home/';
import AccountPage from './pages/account/';
const Root = ({ store }) => {
return(
<Provider store={store}>
<div className="App">
<header className="App-header">
<h1 className="App-title">Welcome to React</h1>
</header>
<Router>
<div>
<Link to="/account">Account</Link>
<Link to="/">Home</Link>
<div>
<Switch>
<Route path="/account" component={AccountPage} />
<Route path="/" component={HomePage} />
</Switch>
</div>
</div>
</Router>
</div>
</Provider>
)
}
Root.propTypes = {
store: PropTypes.object.isRequired
}
export default Root;
So far so good. The store is available in App.js.
But that's not the case at the next level. As you can see I'm attempting to make the store available using connect().
/client/pages/home/index.js
import React from 'react';
import { connect } from 'react-redux';
import Register from '../common/register/';
import PropTypes from 'prop-types';
class Home extends React.Component {
constructor(props){
super(props)
console.log(props);
}
render() {
return (
<div>
<h1> Hello World from home! </h1>
<Register />
</div>
);
}
}
Home.propTypes = {
store: PropTypes.object.isRequired
}
const mapStateToProps = (state) => {
return {
store: state.store,
}
}
export default connect(mapStateToProps)(Home)
At the lower level, the Register component, I'm able to submit the form, but the store not being available, I am unable to capture the response coming from the server.
/client/pages/common/register/index.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import RegisterForm from './containers/register';
import { actionSubmitRegister } from './actions/';
import PropTypes from 'prop-types';
class Register extends React.Component{
constructor (props) {
super(props);
this.state = {
registerResponse: '',
}
this.onSubmitRegister = this.onSubmitRegister.bind(this);
}
onSubmitRegister (event) {
event.preventDefault();
let submitForm = new Promise((resolve, reject) => {
actionSubmitRegister(this.props.form.RegisterForm.values);
});
submitForm.then((response) => {
console.log('response',response);
this.setState({registerResponse: this.props.submit_register.data});
console.log('registerResponse', this.state.registerResponse);
}).catch((error) => {
console.log(error);
});
}
render(){
return (
<div>
<div>
<RegisterForm
submitRegister={this.onSubmitRegister}
/>
<h3>{this.state.registerResponse}</h3>
</div>
</div>
)
}
}
/*
Register.propTypes = {
store: PropTypes.object.isRequired
}
*/
const mapStateToProps = (state) => {
return {
form: state.form,
submit_register: state.submit_register,
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({actionSubmitRegister}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Register);
In mapStateToProps you map store: state.store but in general you use this method to map single props from your state to props in your component, not map the entire store (if this is even possible).
Eg:
form: state.form
The reason you are not able to access the store object in props is because you are not passing it down via props.
Provider from the react-redux library, makes it available to all children down the element tree. Store is made available via React's context API, NOT via props.
"Context is designed to share data that can be considered “global” for a tree of React components."
So in a child component of Provider, we can now do something like
render() {
const { store } = this.context;
console.log(store)
return(
...
)
}
This is the same way that react-redux's connect HOC is able to access the store and subsequently mapStateToProps or utilise the store's dispatch method to mapDispatchToProps.
Also I think Provider requires that it’s child element is a React component.
Check out this tutorial for a more in-depth explanation.
After the input I received above, I reviewed my code and got it to work.
Actually the main issue was on the /client/pages/common/register/index.js file, but I am posting the whole chain for reference:
/client/index.js
nothing to change
/client/App.js
The references to propTypes do not seem to be necessary, so I took them out.
import React from 'react';
import { Provider } from 'react-redux';
import {
BrowserRouter as Router,
Route,
Link,
Switch
} from 'react-router-dom';
import HomePage from './pages/home/';
import AccountPage from './pages/account/';
const Root = ({ store }) => {
return(
<Provider store={store}>
<div className="App">
<header className="App-header">
<h1 className="App-title">Welcome to React</h1>
</header>
<Router>
<div>
<Link to="/account">Account</Link>
<Link to="/">Home</Link>
<div>
<Switch>
<Route path="/account" component={AccountPage} />
<Route path="/" component={HomePage} />
</Switch>
</div>
</div>
</Router>
</div>
</Provider>
)
}
export default Root;
/client/pages/home/index.js
Here both propTypes and connect() do not seem to be required.
import React from 'react';
import Register from '../common/register/';
class Home extends React.Component {
constructor(props){
super(props)
}
render() {
return (
<div>
<h1> Hello World from home! </h1>
<Register />
</div>
);
}
}
export default Home;
/client/pages/common/register/index.js
The main issue here was the onSubmitRegister() method. The promise was not properly setup and I was referencing the action directly instead of using this.props. React do not seem to like that.
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import RegisterForm from './containers/register';
import { actionSubmitRegister } from './actions/';
class Register extends React.Component{
constructor (props) {
super(props);
this.state = {
registerResponse: '',
}
this.onSubmitRegister = this.onSubmitRegister.bind(this);
}
onSubmitRegister (event) {
event.preventDefault();
let submitForm = new Promise((resolve) => {
resolve(this.props.actionSubmitRegister(this.props.form.RegisterForm.values));
});
submitForm.then((result) => {
let data = result.payload.data;
this.setState({registerResponse: data.message});
}).catch((error) => {
console.log(error);
});
}
render(){
return (
<div>
<div>
<RegisterForm
submitRegister={this.onSubmitRegister}
/>
<h3>{this.state.registerResponse}</h3>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
form: state.form,
submit_register: state.submit_register,
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({actionSubmitRegister}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Register);