My Movielist component looks a bit like:
componentDidMount() {
fetch('http://localhost:3335/movies')
.then(function(response) {
return response.json()
}).then((movies) => {
this.setState({ movies });
}).catch(function(ex) {
console.log('parsing failed', ex)
})
}
renderMovie(movie) {
return (
<Movie movie={movie} key={movie.id}></Movie>
);
}
render() {
return (
<div className="movies columns is-multiline">
{ this.state.movies.map(this.renderMovie) }
</div>
);
}
My Movie component has a <Link> :
shouldComponentUpdate() {
debugger;
if (this.props.params) {
let activeMovie = find(this.state.movies, {'id': this.props.params.id});
debugger;
this.setState({ movies: activeMovie });
}
}
render() {
return (
<Link to={`/movies/${this.props.movie.id}`}/>
...
In my index.js I've setup the following routes:
ReactDOM.render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="/movies/(:id)" component={Movie}/>
</Route>
</Router>
),
document.getElementById('root')
);
I would like every time I click on The <Link>, to get the param.id and show only that Movie (with something like lodash find):
let activeMovie = find(this.state.movies, {'id': this.props.params.id});
Unfortunately this.props.params is undefined.
basically just have only one instance of the Movie component loaded in memory. (ideally without losing the previous movies so avoiding a new call everytime I go to the list view)
That's like a Todo app but with Movies instead of Todos..
Your route params are not passed to your Movie Component and you are trying to access it in Movie Component. The params only passed to Movielist Component.
MovieList
renderMovie(movie) {
return (
<Movie
params={this.props.params}
movie={movie}
key={movie.id}
></Movie>
);
}
You should remove the function shouldComponentUpdate in your Movie Component. Because when the url changed. The params will be passed to your MovieList Component and componentWillReceiveProps will receive the params. So you can directly update the MovieList there and you don't need to pass the params to your Movie Component.
MovieList
componentWillReceiveProps(nextProps) {
let activeMovie = find(this.state.movies, {'id': nextProps.params.id});
this.setState({ movies: activeMovie, }
}
But this will lose your movies state. So I think you need to add a nested route like below and have a separate page for Movie Details.
ReactDOM.render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="/movies" component={MovieList}>
<Route path="/:id" component={Movie}>
</Route>
</Route>
</Router>
),
document.getElementById('root')
);
const {
Router,
Route,
IndexRoute,
Redirect,
Link,
IndexLink,
browserHistory
} = ReactRouter;
const {
Component,
} = React;
class App extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="container">
{ React.Children.toArray(this.props.children) }
</div>
)
}
}
class MovieList extends Component {
constructor(props) {
super(props);
this.state = {
movies: []
};
this.renderMovies = this.renderMovies.bind(this);
}
componentDidMount() {
this.setState({
movies: [
{ id: '1', name: 'Fantastic Beasts And Where To Find Them'},
{ id: '2', name: 'Ouija: Origin Of Evil'},
{ id: '3', name: 'Marvel\'s Doctor Strange'}]
})
}
renderMovies() {
return this.state.movies.map(movie => <li key={movie.id}><Link to={`/js/${movie.id}`}>{movie.name}</Link></li>)
}
render() {
return (
<div>
<h1>Movies</h1>
<ul>
{this.renderMovies()}
</ul>
</div>
)
}
}
class Movie extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>{this.props.params.movieId}</div>
);
}
}
ReactDOM.render((
<Router history={browserHistory}>
<Route path="/js" component={App}>
<IndexRoute component={MovieList} />
<Route path="/js/:movieId" component={Movie} />
</Route>
</Router>
), document.getElementById('root'))
I have created a working jsbin example. Hope it helps.
Related
I am new to react and trying to route the user to a new page (within and external) if a certain condition is not met. My snippet below works but I not confident that it is the correct approach.
Could you please have a look and let me know if there is a more efficient way to do it. I want to redirect user automatically when the URL does not have an argument or has an invalid argument.
Thanks
export default class extends Component {
static getInitialProps({ query: { city, state } }) {
return { city: city, state: state };
}
componentDidMount() {
if (!this.props.city) {
Router.push('INTERNAL URL')
} else {
if (this.props.city === "augusta") {
window.location = 'https://www.google.com'
}
}
}
render() {
if (!this.props.city ||this.props.city === "augusta"){
return null
}else{
return(
<div>...</div>
)
}
}
}
You can try to use react-router. react-router. Try if this works
import { Route } from 'react-router';
export default YourPageComponent extends React.Component {
//no need to use static method as react-router provides a props called 'match' allows you get back param from URL
static getInitialProps({ query: { city, state } }) {
return { city: city, state: state };
}
componentDidMount = () => {
this.handleRedirect();
}
//put this function in the related component in componentDidMount
handleRedirect = () => {
const {match: { params }} = this.props;
console.log(params.state, params.city);
//do your logic here
if (params.city !== "augusta"){
window.location.href = 'https://google.com';
return null;
}
}
}
In app.js
render() {
<Switch>
{/* take URL path as a condition to go to which page */}
<Route path='/:city/:state' component={YourPageComponent}/>
<Route path='/' component={/* put your default component here */}/>
</Switch>
}
You can use react-router-dom. Example from https://reacttraining.com/react-router/web/guides/quick-start:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
{/* A <Switch> looks through its children <Route>s and
renders the first one that matches the current URL. */}
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
I'd suggest you to use react-router-dom.
Once you set up the router on the top of your application you can use the Router methods in your child components.
<Router>
<Switch>
<Route exact path='/' render={props=>(<Component {...props}/>)}/>
</Switch>
</Router>
Inside the child component you can use the history.props.push() method to avoid re-rendering.
export default class extends Component {
static getInitialProps({ query: { city, state } }) {
return { city: city, state: state };
}
componentDidMount() {
if (!this.props.city) {
this.props.history.push('to your route')
} else {
if (this.props.city === "augusta") {
window.location = 'https://www.google.com'
}
}
}
render() {
if (!this.props.city ||this.props.city === "augusta"){
return null
}else{
return(
<div>...</div>
)
}
}
}
I am having trouble with the Route path <Route path="customers/:id" render={(props) => <CusDataForm {...props}/>}/> in the code below:
import CusDataCtrl from './cusdata/CusDataCtrl'
import CusDataForm from './cusdata/CusDataForm'
class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/customers" component={CusDataCtrl} />
<Route path="customers/:id" render={(props) => <CusDataForm {...props}/>}/>
</Switch>
</BrowserRouter>
);
}
}
export default App;
if I use <Route exact path="/customers/:id" component={CusDataForm} /> the component does render correctly; however, I need to pass some props down to this component.
My calling component is defined like so:
class CusDataGrid extends Component {
constructor(props) {
super(props)
this.state = {data: []}
}
componentDidMount() {
let me = this;
dbFetch("customers",data => me.setState({data:data}));
}
callEdit = e => {
let recid = e.target.getAttribute("data")
this.props.history.push("/customers/"+recid);
}
render() {
const rows = this.state.data.map((row, ndx) => {
return (
<div key={ndx}><button data={row.recordid} className="waves-effect waves-light btn-small" onClick={this.callEdit}>Edit</button></div>
);
});
return (
<div id="cusdata"><div className="data-scrollable">{rows}</div></div>
);
}
};
export default CusDataGrid;
and my target component is:
class CusDataForm extends Component{
componentDidMount = () =>{
this.setState({id: this.props.id ? this.props.id : ""});
}
render(){
return(<div>HELLO</div>)
}
}
export default CusDataForm;
Please let me know what I am doing incorrectly. Thanks!
you can use hook useParams for it
<Switch>
<Route path="/:id" children={<Child />} />
</Switch>
function Child() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
let { id } = useParams();
return (
<div>
<h3>ID: {id}</h3>
</div>
);
}
official documentation
My problem is when I change a state inside a redux store and based on this state I mount or unmount a component. The Code looks like this:
class Main extends Component {
render() {
const { dropdownState } = this.props;
return (
<div>
<SecondHeadBar />
<div className="main">
<Switch>
<Route exact path='/' component={withRouter(WebsiteIndex)}/>
<Route path='/track/:trackid' component={withRouter(MssTrack)}/>
<Route path='/album/:albumid' component={withRouter(Container.AlbumContainer)}/>
<Route path='/profile/:userName' component={withRouter(MssUser)}/>
<Route path='/upload/:albumid' component={withRouter(MssUploadTemplate)}/>
<Route path='/upload' component={withRouter(MssUploadTemplate)}/>
<Route path='/admin' component={withRouter(ControlCenter)}/>
<Route path='/kategorie' component={withRouter(Category)} exact/>
<Route path='/kategorie/:catName' component={withRouter(Folder)}/>
<Route path='/notFound' component={withRouter(NotFound)}/>
<Route path='/meine-eintraege' component={withRouter(Container.MyEntriesContainer)}/>
</Switch>
</div>
{dropdownState ? <DownloadDropdown /> : ''}
</div>
);
}
}
function mapStateToProps(state) {
return {
dropdownState: state.collection.dropdownState
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Main);
Whenever the prop dropdownState changes. And the Component DownloadDropdown gets mounted then everything in the Main Component gets rerendered. So the content flashes.
Simplest solution would be to have <DownloadDropdown /> be a container component that is connected to Redux and will always stay mounted although not visible. Then you can utilize a HOC or something that's always mounted and visible (like <SecondHeadBar />) and have it connected to a Redux action creator that toggles DownloadDropdown's visiblity. In other words, isolate Redux to two components, instead of over your entire route tree.
Working example: https://codesandbox.io/s/yw4m7yz8r1 (navigate around the routes and click the 'Download Schedule' link at the top!)
I'm not sure how you are triggering the mount/unmount, but let's stay it's being toggled by a button:
SecondHeadBar.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { handleDropdown } from '../actions';
class SecondHeadBar extends Component {
state = {...}
componentDidMount = () => { ... }
render = () => (
<div>
...
<button onClick={this.props.handleDropdown}>Toggle Dropdown</button>
...
</div>
)
}
export default connect(null, { handleDropdown })(SecondHeadBar)
DownloadDropdown.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class DownloadDropdown extends Component {
state = { ... }
componentDidMount = () => { ... }
render = () => (
this.props.isVisible
? <div>I'm visible!</div>
: null
)
}
export default connect(state => ({ isVisible: state.dropdown }))(DownloadDropdown)
actions.js
import { TOGGLE_DROPDOWN } from '../types'
export const handleDropdown = () => ({
type: TOGGLE_DROPDOWN
})
reducers.js
import { TOGGLE_DOWN } from '../types';
...
const dropdownReducer = (state=false, { type, payload }) => {
switch(type) {
case TOGGLE_DROPDOWN: return !state
default: return state
}
}
export default = combineReducer({
...
dropdown: dropdownReducer
...
})
routes.js
const Main = () => (
<div>
<SecondHeadBar />
<div className="main">
<Switch>
<Route exact path='/' component={withRouter(WebsiteIndex)}/>
<Route path='/track/:trackid' component={withRouter(MssTrack)}/>
<Route path='/album/:albumid' component={withRouter(Container.AlbumContainer)}/>
<Route path='/profile/:userName' component={withRouter(MssUser)}/>
<Route path='/upload/:albumid' component={withRouter(MssUploadTemplate)}/>
<Route path='/upload' component={withRouter(MssUploadTemplate)}/>
<Route path='/admin' component={withRouter(ControlCenter)}/>
<Route path='/kategorie' component={withRouter(Category)} exact/>
<Route path='/kategorie/:catName' component={withRouter(Folder)}/>
<Route path='/notFound' component={withRouter(NotFound)}/>
<Route path='/meine-eintraege' component={withRouter(Container.MyEntriesContainer)}/>
</Switch>
</div>
<DownloadDropdown/>
</div>
);
export default Main;
Now, when the user clicks the "Toggle Dropdown" button in <SecondHeadBar/>, it'll update <DownloadDropdown/>'s visibility without affecting your route tree.
I think you can use this lifecycle methods to check.
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.name !== prevState.name) {
return { name: nextProps.name};
}
}
or for older version check in componentwillreceiveProps and stops re render.
In my App.js I have the following code
class App extends Component {
constructor(props) {
super(props);
this.state = {
url: config.Url,
tables : [],
};
}
componentWillMount() {
axios.get(this.state.url + '/tables')
.then(response => {
if (response.data.status === '200') {
const tables = response.data;
this.setState({ tables });
}
})
.catch((error) => {
console.log('error ' + error);
});
}
render() {
return (
<Router>
<div className="App">
<Header tables={this.state.tables}/>
<Route exact path='/' component={Login} />
<Route exact path='/dashboard' component={Dashboard}/>
</div>
</Router>
);
}
}
export default App;
While calling the Header component the header state is not set and hence the values are not receiving inside the header. What should I change to get the table state values in my Header component? I tried with componentWillMount and componentDidMount functions and both gives same response.
Your code looks fine other than you need to use componentDidMount instead of componentWillMount. setState will have no effect in componentWillMount, which is why you aren't seeing any changes.
Also, take a look at this article: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
So, independent of componentDidMount and componentWillMount, on the first step you need validade table with is empty array with if (!this.state.tables.length) {, see full code:
import React from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
url: config.Url,
tables : [],
};
}
componentWillMount() {
axios.get(this.state.url + '/tables')
.then(response => {
if (response.data.status === '200') {
const tables = response.data;
this.setState({ tables });
}
})
.catch((error) => {
console.log('error ' + error);
});
}
render() {
if (!this.state.tables.length) {
return <div>Loading</div>;
}
return (
<Router>
<div className="App">
<Header tables={this.state.tables}/>
<Route exact path='/' component={Login} />
<Route exact path='/dashboard' component={Dashboard}/>
</div>
</Router>
);
}
}
export default App;
What you should do is to set initial state of tables to null and in render method check whether 'tables' is null or not. Because you are fetching remote data and by the time React renders the page, data may not be fetched and assigned to tables.
render() {
let tablesToProps = <p>Loading..</p>
if(this.state.tables) {
tablesToProps = <Header tables={this.state.tables} />
}
return (
<Router>
<div className="App">
{tablesToProps}
<Route exact path='/' component={Login} />
<Route exact path='/dashboard' component={Dashboard}/>
</div>
</Router>
);
}
My application use react-router to manage user navigation and now I need add unit tests bu I'm stuck on how to change route.
My <App /> is (simplified):
class AppUser extends Component {
render() {
return (
<div className="layout">
{this.props.children}
</div>
);
}
}
class Initial extends Component {
render() {
return (
<div className="initial" />
);
}
}
export default class App extends Component {
render() {
let masterPageBase = (props) => (
<AppUser>
{props.children}
</AppUser>
);
let notFound = () => (
<div>
<h1>Not found!</h1>
</div>
);
<Router history={browserHistory}>
<Route path="/" component={masterPageBase}>
<IndexRoute component={Initial} />
<Route path="*" component={notFound} />
</Route>
</Router>
}
}
And my test is:
describe('<App />', () => {
it('user', () => {
const wrapper = shallow(<App />);
// FIXME This fails
expect(wrapper.find('AppUser').length).toEqual(1);
});
});
How can I change the route so that will be an existing child.
This is how you can fake a route in your tests:
There is a module called history which you can use to create a fake browser history in your tests. In order to apply it, you need to make your router parametric in the history it uses, like this:
export default class App extends Component {
render() {
createRouter(browserHistory);
}
}
export function createRouter(history) {
let masterPageBase = (props) => (
<AppUser>
{props.children}
</AppUser>
);
let notFound = () => (
<div>
<h1>Not found!</h1>
</div>
);
return <Router history={history}>
<Route path="/" component={masterPageBase}>
<IndexRoute component={Initial} />
<Route path="*" component={notFound} />
</Route>
</Router>
}
In your tests, you can then use the history module to create a fake history:
import { useRouterHistory } from "react-router";
import createMemoryHistory from "history/lib/createMemoryHistory";
function navigatingTo(path) {
return mount(createRouter(useRouterHistory(createMemoryHistory)(path)));
}
describe('Router', () => {
it('user', () => {
expect(navigatingTo("/").find('AppUser').length).toEqual(1);
});
});
PS: If you run these tests in node.js then you need to make use of jsdom in order for enzyme's mount() to work.