I have a hard time understanding how to use redux together with react-router.
index.js
[...]
// Map Redux state to component props
function mapStateToProps(state) {
return {
cards: state.cards
};
}
// Connected Component:
let ReduxApp = connect(mapStateToProps)(App);
const routes = <Route component={ReduxApp}>
<Route path="/" component={Start}></Route>
<Route path="/show" component={Show}></Route>
</Route>;
ReactDOM.render(
<Provider store={store}>
<Router>{routes}</Router>
</Provider>,
document.getElementById('root')
);
App.js
import React, { Component } from 'react';
export default class App extends React.Component {
render() {
const { children } = this.props;
return (
<div>
Wrapper
{children}
</div>
);
}
}
Show.js
import React, { Component } from 'react';
export default class Show extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<ul>
{this.props.cards.map(card =>
<li>{card}</li>
)}
</ul>
);
}
}
This throws
Uncaught TypeError: Cannot read property 'map' of undefined
The only solution I've found is to use this instead of {children}:
{this.props.children &&
React.cloneElement(this.props.children, { ...this.props })}
Is this really the proper way to do it?
Use react-redux
In order to inject any state or action creators into the props of a React component you can use connect from react-redux which is the official React binding for Redux.
It is worth checking out the documentation for connect here.
As an example based on what is specified in the question you would do something like this:
import React, { Component } from 'react';
// import required function from react-redux
import { connect } from 'react-redux';
// do not export this class yet
class Show extends React.Component {
// no need to define constructor as it does nothing different from super class
render() {
return (
<ul>
{this.props.cards.map(card =>
<li>{card}</li>
)}
</ul>
);
}
}
// export connect-ed Show Component and inject state.cards into its props.
export default connect(state => ({ cards: state.cards }))(Show);
In order for this to work though you have to wrap your root component, or router with a Provider from react-redux (this is already present in your sample above). But for clarity:
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route } from 'react-router';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from './some/path/to/reducers';
const store = createStore(reducers);
const routes = <Route component={ReduxApp}>
<Route path="/" component={Start}></Route>
<Route path="/show" component={Show}></Route>
</Route>;
ReactDOM.render(
// Either wrap your routing, or your root component with the Provider so that calls to connect have access to your application's state
<Provider store={store}>
<Router>{routes}</Router>
</Provider>,
document.getElementById('root')
);
If any components do not require injection of any state, or action creators then you can just export a "dumb" React component and none of the state of your app will be exposed to the component when rendered.
I solved it by explicitly mapping the state with connect in every component:
export default connect(function selector(state) {
return {
cards: state.cards
};
})(Show);
This way I can decide what properties of the state the component should have access to as well, polluting the props less. Not sure if this is best practice though.
Related
This question is specifically for react-router-dom version 5.
Here's my App component following the structures of the quick start guide:
import React, { Component } from "react";
import ProductDetails from "./components/productDetails";
class App extends Component {
render() {
return (
<Switch>
<Route path="/products/:id">
<ProductDetails />
</Route>
</Switch>
);
}
}
And here's my productDetails.jsx:
import React, { Component } from "react";
class ProductDetails extends Component {
render() {
return (
<h1>Product Details - {this.props.match.params.id}</h1>
);
}
}
export default ProductDetails;
which
How to get the this.props.match.params.id in class component way, not function way.
Also I found that the corresponding props empty:
but, those values are in Router.Provider:
So, how to get all the Route component's props from it's children using class?
Add withRouter HOC to your class component then you will have access to the this.props.match.params
import it from react-router
import { withRouter } from "react-router";
Then wrap your component with it like this
export default withRouter(ProductDetails);
I'm building an e-commerce website with React (my first ever React project) and I'm using React router to manage my pages.
I've got the following component tree structure:
<Router>
<BrowserRouter>
<Router>
<withRouter(Base)>
<Route>
<Base>
<BaseProvider>
<Context.Provider>
<Header>
<PageContent>
The standard React Router structure basically, and withRouter I've got the following:
Base.js
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { Header } from './Header';
import { Footer } from './Footer';
import Provider from '../../BaseProvider';
class Base extends Component {
render() {
return (
<Provider>
<Header/>
<div className="container">{this.props.children}</div>
<Footer />
</Provider>
);
}
}
BaseProvider.js
import React, { Component, createContext } from 'react';
const Context = createContext();
const { Provider, Consumer } = Context;
class BaseProvider extends Component {
state = {
cart: [],
basketTotal: 0,
priceTotal: 0,
};
addProductToCart = product => {
const cart = { ...this.state.cart };
cart[product.id] = product;
this.setState({ cart, basketTotal: Object.keys(cart).length });
};
render() {
return (
<Provider
value={{ state: this.state, addProductToCart: this.addProductToCart }}
>
{this.props.children}
</Provider>
);
}
}
export { Consumer };
export default BaseProvider;
This gives me a template essentially, so I just the children pages without having to include Header and Footer each time.
If I want to use my global context I'm having to import it each time, and it seems like I've done something wrong as surely I should be able to use this on any page since it's exported in BaseProvider?
If I was to visit the About page, I'd get the same component structure, but no access to the consumer without using:
import { Consumer } from '../../BaseProvider';
Why do I have to do this for each file even though it's exported and at the top level of my BaseProvider? It just seems such a bad pattern that I'd have to import it into about 20 files...
Without importing it, I just get:
Line 67: 'Consumer' is not defined no-undef
I tried just adding the contextType to base but I get: Warning: withRouter(Base): Function components do not support contextType.
Base.contextType = Consumer;
I feel like I've just implemented this wrong as surely this pattern should work a lot better.
I'd recommend using a Higher Order Component - a component that wraps other components with additional state or functionality.
const CartConsumer = Component => {
return class extends React.Component {
render() {
return (
<MyContext.Consumer>
<Component />
</MyContext.Consumer>
)
}
}
}
Then in any component where you'd like to use it, simply wrap in the export statement:
export default CartConsumer(ComponentWithContext)
This does not avoid importing completely, but it's far more minimal than using the consumer directly.
I'm designing a website that just like facebook or anyother website that I seen I assume works (disclosure: I'm new to web programing), what I want to do is to route to different routes but in one of my routes or possibly even more I need to pass info to the next screen route when I to the new page for example: (I'm in
www.website.com/page1 then move to www.website.com/page1/page2) whilst passing data through the state say I want to pass a date or a name but I would not want it to be shown in url. So I found that react can pass with:
<Link {to={pathname:"/page2", state:{statetopass:datatopass}}}>
However,when I do pass the state once I'm in (www.website.com/page1/page2) I'm unable to read the data only when I refresh which i find weird will I ever see the data passed, I read that history is mutable but I can't really understand what that means its probably something to do with what my problem is.
The code that I have tried so far is here:
<-------------------- APP--------------------------->
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, IndexRoute, HashRouter } from 'react-router-dom';
import Page1 from './Page1'
import Page2 from './Page2'
import { createBrowserHistory } from "history";
const history = createBrowserHistory();//idk if history should be here seems
class App extends Component {//to make no difference
constructor(props) {
super(props);
this.state = {
}
}
render() {
return (
<Router>
<Switch>
<Route exact path='/hom/Page1' component={({ match }) => { return (<Page1 />) }} />
<Route exact path='/hom/Page1/Page2' component={({ match }) => { return (<Page2 />) }} />
</Switch>
</Router>
)
}
}
export default App;
<--------------------Page1----------------->
import React, { Component } from 'react';
import { Link } from 'react-router-dom'
class Page1 extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
return (
<div>
<Link to={{
pathname: `Page1/Page2`,
state: { dataneed: "testme" }
}}><button>Check </button>
</Link>
</div>
)
}
}
export default Page1;
<-------------------------------Page2----------------------------->
import React, { Component } from 'react';
import { createBrowserHistory } from "history";
const history = createBrowserHistory();
class Page2 extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
console.log(history.location.state.dataneed)
return (
<div>
<h1>{history.location.state.dataneed}</h1>
</div>
)
}
}
export default Page2;
So you will see that at first you get an error but then once you refresh you see the text being displayed. If anyone could suggest the best way to go about doing whta I'm trying and if anyone could help me shed some light on the matter I would greatly appreciate this.
PS: I'm using 4.3.1 version there are videos out there but those seem to use version lower than 4.0 and completely different.
I believe the issue here is the mix of React Router and the History package. React Router uses History and has a history built in to its routers, so there is no need to explicitly use createBrowserHistory or anything from History directly. Specifically the issue is that the state is passed to the Link, from React Router, but then you attempt to access the data from the createBrowserHistory() object.
What you can do to resolve this issue and keep your code a bit cleaner is basically not use createBrowserHistory directly and instead rely on the built-in history from React Router. The routing data can be accessed through props.location, props.history, and props.match, which are injected into any component wrapped in the higher-order component withRouter from React Router.
What this will look like:
In index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.render((
<BrowserRouter>
<App/>
</BrowserRouter>
), document.getElementById('root'));
In App.js:
import React, { Component } from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import Page1 from './Page1'
import Page2 from './Page2'
class App extends Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
return (
{/* Note no need for a Router because we wrapped the App component in the BrowserRouter in index.js */}
<Switch>
{/* Note the component attribute can be simplified */}
<Route exact path='/hom/Page1' component={ Page1 } />
<Route exact path='/hom/Page1/Page2' component={ Page2 } />
</Switch>
)
}
}
export default withRouter(App);
Page1 is fine.
In Page2:
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
class Page2 extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
const { dataneed } = this.props.location.state;
return (
<div>
<h1>{ dataneed }</h1>
</div>
)
}
}
export default withRouter(Page2);
Hopefully this helps, let me know if you have questions!
I'm using the following:
react v16.2.0,
react-redux v5.0.7,
react-router-dom v4.2.2,
redux v3.7.2
What I am trying to achieve is to update some props from the Auth component, and when the user navigates to /user (Which loads the Userpage component), the modified props should be displayed.
Here is a simplified version of my code:
In App.js:
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Store from './components/Store';
import Home from './components/Home';
import Auth from './components/auth';
import Userpage from './components/Userpage';
import { BrowserRouter as Router, Route } from 'react-router-dom';
class App extends Component {
render() {
return (
<Provider store={Store}>
<Router>
<div>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Auth}/>
<Route path="/user" component={Userpage}/>
</div>
</Router>
</Provider>
);
}
}
export default App;
In Store.js:
import { createStore } from 'redux';
const reducer = (state,action) => {
if(action.type == 'TEST'){
return Object.assign({},state,{test:action.payload.test});
} else
return state;
}
export default createStore(reducer,{
test:'DOES NOT WORK',
})
In Auth.js :
import React from 'react';
import { connect } from 'react-redux';
import Userpage from './Userpage';
class Auth extends React.Component {
componentWillMount() {
this.props.update('TEST',{test:'WORKS'});
}
render() {
return(
<div>
<Userpage/>
</div>
);
}
}
export default connect(
(store) => {
return store;
},
(dispatch) => {
return {
update:(dispatchType, dispatchPayload) => {
dispatch({type:dispatchType,payload:dispatchPayload});
}
}
}
)(Auth);
In Userpage.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Userpage extends Component{
componentDidMount(){
console.log(this.props.test);
}
render(){
return null;
}
}
export default connect(
(store) => {
return store;
}
)(Userpage);
Now, when I navigate to /login, the store is updated and test is set to "WORKS". But if I navigate to /userpage, console.log(this.props.test) prints "DOES NOT WORK", instead of the updated prop.
Just to test, I included Userpage in the render function of Auth and it console logs "WORKS" correctly.
So why is the redux store apparently being reset to default values on navigation to another page using react-router? How can I fix this?
Found the solution. Apparently the whole application re-renders when you manually navigate (by typing in the URL in the browser), hence resetting the redux store.
Using Link as mentioned in react-router docs or redirecting using this.props.history.push('/user') works without any issues.
I am trying to programmatically change pages using browserHistory.push. In one of my components, but not in a component that I embedded inside of that one.
Does anyone know why my project is throwing the error below only for the child component but not for the parent component?
Cannot read property 'push' of undefined
Parent Component
import React, {Component} from 'react';
import { browserHistory } from 'react-router';
import ChildView from './ChildView';
class ParentView extends Component {
constructor(props) {
super(props);
this.addEvent = this.addEvent.bind(this);
}
changePage() {
this.props.history.push("/someNewPage");
}
render() {
return (
<div>
<div>
<button onClick={this.changePage}>Go to a New Page!</button>
</div>
<ChildView /> // this is the child component where this.props.history.push doesn't work
</div>
);
}
}
function mapStateToProps(state) {
return {
user: state.user
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
setName: setName
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ParentView);
Child Component
import React, {Component} from 'react';
import { browserHistory } from 'react-router';
class ChildView extends Component {
constructor(props) {
super(props);
this.addEvent = this.addEvent.bind(this);
}
changePage() {
this.props.history.push("/someNewPage");
}
render() {
return (
<div>
<div>
<button onClick={this.changePage}>Go to a New Page!</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
user: state.user
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
setName: setName
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ChildView);
Router
// Libraries
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { browserHistory } from 'react-router';
// Components
import NotFound from './components/NotFound';
import ParentView from './components/ParentView';
import ChildView from './components/ChildView';
import SomeNewPage from './components/SomeNewPage';
// Redux
import { Provider } from 'react-redux';
import {createStore} from 'redux';
import allReducers from './reducers';
const store = createStore(
allReducers,
window.devToolsExtension && window.devToolsExtension()
);
const routes = (
<Router history={browserHistory}>
<div>
<Provider store={store}>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/parentView" component={ParentView} />
<Route path="/someNewPage" component={SomeNewPage} />
<Route path="/childView" component={ChildView} />
<Route component={NotFound} />
</Switch>
</Provider>
</div>
</Router>
);
export default routes;
As you can see, the components are virtually exactly the same except that the child one is inside the parent one.
Note I have tried these approaches but they do not resolve the issue for me:
Uncaught TypeError: Cannot read property 'push' of undefined with correct import being available
React browserHistory.push giving Uncaught TypeError: Cannot read property 'push' of undefined
redirect to a page programmatically in react-router 2
You answered your question in your question.
As you can see, the components are virtually exactly the same except
that the child one is inside the parent one.
The very fact that the component is nested one further is your issue. React router only injects the routing props to the component it routed to, but not to the components nested with in.
See the accepted answer to this question. The basic idea is that you now need to find a way to inject the routing props to the child component.
You can do that by wrapping the child in a HOC withRouter.
export default withRouter(connect(mapStateToProps, matchDispatchToProps)(ChildView));
I hope this helps.
Using withRouter is fine, an alternative option is to pass the history as a prop from parent to child (without using withRouter), e.g:
Parent
<SignupForm history={this.props.history}/>
Child
this.props.history.push('/dashboard');
You Can try this
this.context.history.push('/dashboard')
or pass the history as a prop from parent to child component like
parent
<SignupForm history={this.props.history}/>
child
this.props.history.push('/dashboard');
or withRouter
import { withRouter } from "react-router";
export default withRouter(connect(mapStateToProps, {
...
})(Header));
This is the problem of Nested Components.
I was facing this issue in Header component.
I wanted to redirect the page to home page on clicking the logo.
this.props.history.push won't work in nested components.
So Solution is to use withRouter .
To Use withRouter just copy paste Below 2 lines in Header(You can use it in your component) :
import { withRouter } from "react-router";
export default withRouter(connect(mapStateToProps, {
...
})(Header));
If anyone is still facing similar issues please try to change the onClick to this
onClick={() => this.props.history.push("/dashboard")}
also if you are in a child component please use the <Redirect to="/dashboard"/> component