I'm trying to complete my app, have learned react, redux, react router all in one, now I'm just confused a bit when it comes to putting it all together.
Say I have a Nav component that's included in a header that's included globally on all pages and it calls a redux action which then runs a reducer and returns some search results.
When one searches from the navigation bar, how do I get it to redirect a search page that then returns the search results?
Nav component
class Nav extends React.Component {
render() {
const { search } = this.props;
return (
<header>
<SearchBox search={search} />
</header>
)
}
}
that includes a search component
class SearchBox extends React.Component {
constructor() {
super();
this.state = {
name: ''
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault();
this.props.search(JSON.stringify({name: this.state.name}))
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" id="name" onChange={this.handleChange} placeholder="Search" />
<button type="submit">Search</button>
</form>
)
}
}
my layouts are like
index.js
const Index = () => {
return (
<Nav />
... Home Content
)
}
profile.js
const Profile = () => {
return (
<Nav />
... Profile Content
)
}
search.js
const Users = (props) => {
let list = null;
list = props.users.map((user, index)=> {
const { name } = user.profile;
const { username } = user;
return (
<li key={index}>
<h3><a href={'/'+username}>{name}</a></h3>
</li>
)
});
return <section id="search"><ul>{list}</ul></section>;
}
class Search extends React.Component {
render() {
const { searchResults } = this.props;
return (
<Nav />
<div>
{
/* show 'No results when no results found' */
searchResults !== ''
? seachResults.length == 0
? 'No results found'
: <Users users={searchResults} />
: null
}
</div>
)
}
}
const mapStateToProps = state => ({
searchResults: state.store.searchResults,
});
the user action is
export const search = (name) => dispatch => {
...
dispatch({
type: SEARCH_USER,
payload: res
})
the reducer is
const initialState = {
searchResults: ''
};
case SEARCH_USER:
return {
...state,
searchResults: action.payload.search
}
}
index.js
class App extends React.Component {
render() {
return (
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/profile" component={Profile} />
<Route path="/search" component={Search} />
</Switch>
</Router>
)
}
}
Related
I am using this approach to make some routes in my application only accessible after log in.
So in App.js I have some ProtectedRoutes.
<ProtectedRoute path='/projects' auth={this.props} component={Projects} />
<ProtectedRoute path='/search' auth={this.props} component={Search} />
<ProtectedRoute path='/admin' auth={this.props} component={Admin} />
And my ProtectedRoute function is like this:
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={props => {
return isAuthenticated ? (
<Comp {...sendprops} />
) : (
<Redirect to='/' />
);
}}
/>
);
};
export default ProtectedRoute;
But having done that, I've totally broken my Search component, which relies on being able to see and push to the history property.
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery}&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
When I try to console.log out the properties in the Search component, I can see location still, but I've lost history. So in what feels like a somewhat ham fisted, ignorant fashion, I've wrapped my Search export in withRouter.
export default withRouter(Search);
And now I can see the history again. But I have no idea why I lost it in the first place? Insights would be greatly appreciated.
As requested, here is the Search component.
import React, { Component } from 'react';
import ResultList from './SearchResultList';
import { withRouter } from 'react-router'
class Search extends Component {
constructor(props) {
super(props);
console.log(props);
let url_query = '';
let url_search_type = '';
let url_search_page = 1;
let loading = false;
if(this.props.location.search) {
const params = new URLSearchParams(this.props.location.search);
url_query = params.get('query');
url_search_type = params.get('search_type');
url_search_page = params.get('page') || 1;
loading = true;
}
this.state = {
searchQuery: url_query,
searchType: url_search_type,
searchPage: url_search_page,
searchResult: {
results: [],
pages_left: '',
pages_right: '',
page: '',
type: ''
},
isLoading: loading
}
}
componentDidMount() {
if(this.state.isLoading){
this.doSearch();
}
}
handleChange = (e) => {
const search_name = e.target.name;
const search_value = e.target.value.trim();
this.setState({
[search_name]: search_value,
})
}
handleSubmit = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
isLoading: true,
searchPage: 1
}, this.doSearch);
}
changePage = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
searchPage: e,
isLoading: true
}, this.doSearch);
}
doSearch = () => {
//console.log(this.state);
const endpoint = 'http://urlurlurllll/search?query=' + this.state.searchQuery + '&search_type=' + this.state.searchType + '&page=' + this.state.searchPage;
//console.log(endpoint);
fetch(endpoint)
.then(data => data.json())
.then(jdata => {
this.setState({
searchResult: {
results: jdata.results,
pages_left: jdata.pages_left,
pages_right: jdata.pages_right,
page: jdata.page,
type: this.state.searchType
},
isLoading: false
})
})
.catch(error => console.log(error));
}
render (){
const isLoading = this.state.isLoading;
const searchResult = this.state.searchResult;
return (
<React.Fragment>
<div>
<h1>Search</h1>
<form>
<label>Search Term:</label>
<input type="text" id="input-search" name="searchQuery" value={this.state.searchQuery} onChange={ event => this.handleChange(event) } />
<label>Search Type:</label>
<select id="select-type" name="searchType" value={this.state.searchType} onChange={ event => this.handleChange(event) }>
<option value=''>Select Type</option>
<option value='hashtag'>hashtag</option>
<option value='handle'>handle</option>
<option value='keyword'>keyword</option>
</select>
<button type="button" id="btn-search" onClick={event => this.handleSubmit(event)}>Search</button>
</form>
</div>
<div>
{ isLoading ? (
<div>
<p>Loading...</p>
</div>
) : (
<React.Fragment>
<h2>Search Results</h2>
{searchResult.results.length ? (
<ResultList searchResult={searchResult} changePage={this.changePage} />
) : (
<p>Use the little form up there to start searching!</p>
)}
</React.Fragment>
)}
</div>
</React.Fragment>
);
}
}
export default withRouter(Search);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.3.1/umd/react-dom.production.min.js"></script>
Issue
I think the issue is the route props from render aren't passed along to the component. (Though it is unclear to me how the location route prop was still working)
Solution
Spread the route props (match, location, and history) provided from render into the component being rendered.
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={(props) => // <-- route props need to be
isAuthenticated ? (
<Comp {...sendprops} {...props} /> // <-- passed to component
) : (
<Redirect to="/" />
)
}
/>
);
};
I just checked that tutorial/lesson you linked to see what they were doing and they pass the route props through as well.
I'm trying to do this.props.history.push... in my component, but even after making sure that I'm exporting it using withRouter I still get this error:
Uncaught TypeError: Cannot read property 'push' of undefined
I also made sure that the parent component that's using this is wrapped inside of a ProtectedRoute as well:
// my component:
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
class UserIndexItem extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.play = this.play.bind(this);
}
handleClick(e) {
if (!e.target.classList.contains("triangle")) {
this.props.history.push(`/playlist/${this.props.playlist.id}`);
}
}
handleTrack(playlist) {
// still going forward one, then back one, and then it plays normally...
if (!playlist.payload.tracks) return;
let tracks = Object.values(playlist.payload.tracks);
let currentTrack = tracks[0];
let nextTrack = tracks[1];
this.props.receiveCurrentTrack(currentTrack);
this.props.receiveNextTrack(nextTrack);
this.props.receiveTitle(currentTrack.title);
this.props.receiveArtist(currentTrack.artist);
this.props.receiveAlbumId(currentTrack.album_id);
}
play() {
const { playlist } = this.props;
this.props.requestSinglePlaylist(this.props.playlist.id).then(playlist => this.handleTrack(playlist));
this.props.receivePlaylistId(playlist.id);
}
render() {
const { playlist } = this.props;
return (
<li>
<div className="playlist-image" onClick={ this.handleClick }>
<div className="play-button" onClick={ this.play }>
<div className="triangle right"></div>
<div className="circle"></div>
</div>
<div className="overlay"></div>
<img src={playlist.photo_url} alt="Playlist thumbnail" onClick={ this.handleClick }/>
</div>
<div className="playlist-name">
<Link to={`/playlist/${playlist.id}`}>{ playlist.title}</Link>
</div>
</li>
);
}
}
export default withRouter(UserIndexItem);
// my parent component:
import React from 'react';
import UserIndexItem from './user_index_item';
import { selectTracksFromPlaylist } from '../../reducers/selectors';
class UserIndex extends React.Component {
constructor(props) {
super(props);
}
render() {
const { user, playlists } = this.props;
return(
<div className="user-index-container">
<div className="header">
<h1>{ user.username }</h1>
<h2>Public Playlists</h2>
</div>
<div className="playlists">
<ul>
{ playlists.map(playlist =>
<UserIndexItem
key={ playlist.id }
playlist={ playlist }
requestSinglePlaylist={ this.props.requestSinglePlaylist }
receiveCurrentTrack={ this.props.receiveCurrentTrack }
receiveNextTrack = { this.props.receiveNextTrack }
receiveTitle={ this.props.receiveTitle }
receiveArtist={ this.props.receiveArtist }
receivePlaylistId={ this.props.receivePlaylistId }
receiveAlbumId={ this.props.receiveAlbumId }
/>)
}
</ul>
</div>
</div>
);
}
}
export default UserIndex;
// my route that's using the parent component:
<ProtectedRoute path="/users/:userId" component={UserIndex} />
// my ProtectedRoute implementation:
const Protected = ({ component: Component, path, loggedIn, exact }) => (
<Route path={ path } exact={ exact } render={ (props) => (
loggedIn ? (
<Component {...props} />
) : (
<Redirect to="/welcome" />
)
) }/>
);
You can try like this:
<ProtectedRoute path="/users/:userId" component={props => <UserIndex {...props} />} />
Please let me know if this is working.
Thanks.
I think that {...props} need to call inside UserIndexItem as well.
According to my understand inside the App.js you need to pass {...props} to child component otherwise it don't have parent properties
// this ProtectedRoute should change according to your requirement. I just put sample code
const ProtectedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
? <Component {...props} />
: <Redirect to="/Login"/>
)} />
)
<ProtectedRoute path="/users/:userId" component={UserIndex} />
// my parent component:
<UserIndexItem
key={ playlist.id }
playlist={ playlist }
requestSinglePlaylist={ this.props.requestSinglePlaylist }
receiveCurrentTrack={ this.props.receiveCurrentTrack }
receiveNextTrack = { this.props.receiveNextTrack }
receiveTitle={ this.props.receiveTitle }
receiveArtist={ this.props.receiveArtist }
receivePlaylistId={ this.props.receivePlaylistId }
receiveAlbumId={ this.props.receiveAlbumId }
{...this.props}
/>
When clicking on the back button , the url gets changed, but the component that should be rendered for this url, is not rendered. Rerendering seems to be blocked. I am using connected-react-router. Before implementing connected-react-router, the back button worked fine. I tried a suggested solution , wrapping my connect function with the redux store with withRouter, but still see no result.
index.js
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'),
);
App.js
class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch(getInitialBookData());
}
render() {
return (
<div>
<Navigation />
</div>
);
}
}
export default connect()(App);
Navigation.js
const Navigation = () => {
return (
<Fragment>
<Navbar color="light" light expand="md">
<Collapse className="navbar-collapse">
<Nav className="ml-sm-auto navbar-nav">
<NavItem className="p-2">
<NavLink activeClassName="active" to="/">Dashboard</NavLink>
</NavItem>
<NavItem className="p-2">
<NavLink activeClassName="active" to="/addbook">Add Book</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
<Container className="content mt-8">
<Switch>
<Route exact path="/" component={Dashboard} />
<Route exact path="/editbook/:id" component={CreateEditBookContainer} />
<Route exact path="/addbook" component={CreateEditBookContainer} />
<Route component={Page404} />
</Switch>
</Container>
</Fragment>
);
};
Dashboard.js
const Dashboard = ({ loading, error, success }) => {
return (
<Fragment>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
<Row className="mt-3">
<h2 className="mb-5">Welcome to Dashboard</h2>
{ loading === true
? null
: <BookListContainer /> }
</Row>
</Fragment>
);
};
const mapStateToProps = ({fetching, errors, success}) => {
return {
loading: fetching.status,
error: errors,
success: success,
};
}
Dashboard.propTypes = {
loading: PropTypes.bool.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(Dashboard));
BookListContainer.js
class BookListContainer extends Component{
state = {
navigateToEditBook: false,
};
idBookToEdit = null;
goToEditComponent = (id) => {
this.idBookToEdit = id;
this.setState({ navigateToEditBook: true });
}
render() {
let idBookToEdit = this.idBookToEdit;
if (this.state.navigateToEditBook === true) {
return <Redirect push to={{
pathname:`/editBook/${idBookToEdit}`,
state: { referrer: '/', bookId: idBookToEdit }
}} />
}
const { books } = this.props;
return(
<Row>
{ books.map((book) => {
return (
<Book
key={book._id}
book={book}
handleEditBook={this.goToEditComponent}
/>
)
}) }
</Row>
)};
};
const mapStateToProps = (props) => {
return {
books: props.books.books,
location: props.router.location,
};
}
BookListContainer.propTypes = {
books: PropTypes.array.isRequired,
};
export default withRouter((connect(mapStateToProps)(BookListContainer)));
CreateEditBookContainer.js
class CreateEditBookContainer extends Component {
render() {
const bookId = this.props.location.state
? this.props.location.state.bookId
:null
const { books, error, success } = this.props;
let book = null;
if(bookId){
book = books.filter(book => book._id === bookId)
}
return(
<Col sm="12" md="6" className="m-auto pt-5">
<CreateEditBookForm
{ ...book ? book = { ...book[0] } : null }
/>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
</Col>
)}
}
const mapStateToProps = ({ books, errors, success }) => {
return {
books: books.books,
error: errors,
success: success,
}
}
CreateEditBookContainer.propTypes = {
books: PropTypes.array.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(CreateEditBookContainer));
When clicking on EditBook Button, CreateEditBookContainer.js gets correctly rendered, but when clicking on the back button, this components remains, while the Dashboard component should actually get rendered since it corresponds to the path '/', that is correctly set in the url. When clicking on the Route links in specified in Navigation, all components are rendered correctly. It ONLY fails on the back button. Thank you for any suggestions.
I have a comment form and I can add comments, but I have to refresh the page for them to display
the comment form looks like this
class CommentForm extends PureComponent {
constructor(props) {
super(props);
this.state = {
state: this.props.state
};
this.handleChange = this.handleChange.bind(this);
}
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit(this.state);
};
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
render() {
return (
<div>
<Paper className="styles" elevation={4}>
<form onSubmit={this.handleSubmit}>
<div>
<TextField
label="Comment"
name="comment"
value={this.state.comment || ""}
onChange={this.handleChange}
/>
</div>
<br />
<Button type="submit">Save</Button>
</form>
</Paper>
</div>
);
}
}
const mapStateToProps = state => {
return {
state: state
};
};
export default connect(
mapStateToProps,
{ getTicket }
)(CommentForm);
And this component is for displaying the comments that are saved in the database
displayComments
class DisplayComments extends PureComponent {
componentWillMount() {
this.props.getAllComments();
}
getComment(commentId) {
this.props.getComment(commentId);
}
render() {
const { comments } = this.props;
const commentsOnTicket = this.props.data.match.params.id
const filterComments = comments.filter(comment => comment.tickets.id == commentsOnTicket);
const commentsList = filterComments.sort((a, b) => {
return a.id - b.id;
});
return (
<div>
<Paper className="styles" elevation={4}>
<h1>comments</h1>
<table>
<thead />
<tbody>
{commentsList.map(comment => (
<tr key={comment.id}>
<td style={{ border: "2px solid black" }}>
{comment.comment}
</td>
<td />
</tr>
))}
</tbody>
</table>
<br />
<br />
</Paper>
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
comments: state.comments,
comment: state.comment,
users: state.users === null ? null : state.users,
};
};
export default connect(
mapStateToProps,
{
getAllComments,
getComment,
}
)(DisplayComments);
My knowledge on React is limited since I just started learning about it
So I'm guessing it should be done with lifecyclehooks
but I dont really understand them yet.
this is the Parent component rendering the commment form and the displaying of the comments
class TicketDetails extends PureComponent {
componentWillMount() {
this.props.getAllComments();
}
addComment = comment => {
this.props.addComment(comment);
};
render() {
const { ticket, tickets, comments } = this.props;
const { users } = ticket;
return <div>
<Card className="outer-card">
<h1>Ticket: {ticket.id}</h1>
<h2>Price: €{ticket.price}</h2>
<p>Description: {ticket.description}</p>
<h2>Image: {ticket.image}</h2>
<p>Risk: {getRiskfactor(tickets, ticket, users, comments)} %</p>
<hr />
</Card>
<DisplayComments data={this.props} />
<CommentForm onSubmit={this.addComment} />
</div>;
}
}
const mapStateToProps = state => {
return {
ticket: state.ticket,
users: state.users,
tickets: state.tickets,
comments: state.comments
};
};
export default connect(
mapStateToProps,
{
addComment,
getAllComments
}
)(TicketDetails);
the TicketDetails is rendered in the App component
class App extends Component {
render() {
return (
<Router>
<div>
<TopBar />
<br/><br/>
<Route exact path="/login" component={LoginPage} />
<Route exact path="/logout" component={LogoutPage} />
<Route exact path="/signup" component={SignupPage} />
<Route exact path="/events" component={EventsList} />
<Route exact path="/addEvent" component={AddEvent} />
<Route exact path="/events/:id" component={TicketsList} />
<Route exact path="/tickets/:id" component={TicketDetails} />
<Route exact path="/addTicket" component={AddTicket} />
<Route exact path="/" render={() => <Redirect to="/events" />} />
</div>
</Router>
);
}
}
Action/comments.js
import * as request from "superagent";
import {baseUrl} from "../constants";
import {logout} from "./users";
import {isExpired} from "../jwt";
export const GET_ALL_COMMENTS = "GET_ALL_COMMENTS";
export const GET_COMMENT = "GET_COMMENT";
export const ADD_COMMENT = "ADD_COMMENT";
export const getAllComments = () => (dispatch, getState) => {
const state = getState();
if (!state.currentUser) return null;
const jwt = state.currentUser.jwt;
if (isExpired(jwt)) return dispatch(logout());
request
.get(`${baseUrl}/comments`)
.set("Authorization", `Bearer ${jwt}`)
.then(response =>
dispatch({
type: GET_ALL_COMMENTS,
payload: response.body.comments
})
)
.catch(err => alert(err));
};
export const getComment = commentId => (dispatch, getState) => {
const state = getState();
if (!state.currentUser) return null;
const jwt = state.currentUser.jwt;
if (isExpired(jwt)) return dispatch(logout());
request
.get(`${baseUrl}/comments/${commentId}`)
.set("Authorization", `Bearer ${jwt}`)
.then(response =>
dispatch({
type: GET_COMMENT,
payload: response.body
})
)
.catch(err => alert(err));
};
export const addComment = comment => (dispatch, getState) => {
const state = getState();
const jwt = state.currentUser.jwt;
if (isExpired(jwt)) return dispatch(logout());
request
.post(`${baseUrl}/comments`)
.set("Authorization", `Bearer ${jwt}`)
.send({
comment: comment.comment,
tickets: state.ticket
})
.then(response =>
dispatch({
type: ADD_COMMENT,
payload: response.body
})
);
};
reducers/comments.js
import { GET_ALL_COMMENTS } from "../actions/comments";
export default function(state = [], action) {
switch (action.type) {
case GET_ALL_COMMENTS:
return action.payload;
default:
return state;
}
}
reducers/comment.js
import { GET_COMMENT,ADD_COMMENT } from "../actions/comments";
const comment = {};
export default function(state = comment, action) {
switch (action.type) {
case GET_COMMENT:
return action.payload;
case ADD_COMMENT:
return action.payload;
default:
return state;
}
}
Although your question seems to be incomplete, but still there are few mistakes in implementation.
You have not used your dispatch to props properly. It is meant to connect dispatch to your function so that you donot have to call this.props.dispatch(func()) explicitly in your function
const mapDispatchToProps = (dispatch) => {
return {
addComment: (comment) => dispatch(addComment(comment))
}
}
It doesnot seems a good practice to pass your props down to two to three level. Since your class is statful class you can easily get it in your component and even call it with quite a ease.
Why would one have a action to get data from store. Actions and reducers are meant to be making changes in store like add, deleting and updating, not for fetching data. For fetching data you are connecting state to props.
UPDATE****
// Add import of the action here like import {addComment} from '../.whatever';
class TicketDetails extends PureComponent {
componentWillMount() {
this.props.getAllComments();
}
addComment = comment => {
this.props.addComment(comment);
};
render() {
const { ticket, tickets, comments } = this.props;
const { users } = ticket;
return <div>
<Card className="outer-card">
<h1>Ticket: {ticket.id}</h1>
<h2>Price: €{ticket.price}</h2>
<p>Description: {ticket.description}</p>
<h2>Image: {ticket.image}</h2>
<p>Risk: {getRiskfactor(tickets, ticket, users, comments)} %</p>
<hr />
</Card>
<DisplayComments data={this.props} />
<CommentForm onSubmit={this.addComment} />
</div>;
}
}
const mapStateToProps = state => {
return {
ticket: state.ticket,
users: state.users,
tickets: state.tickets,
comments: state.comments
};
};
const mapDispatchToProps = (dispatch) => {
return {
addComment: (comment) => dispatch(addComment(comment)),
getAllComments: () => dispatch(getAllComments())
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TicketDetails);
I have a simple app. It has two components:
Items - it just renders items from the store
AddItem - it just add an item to the store
The question is: why when I submit the form of AddItem component this component is mounted again? If I don't invoke addItem function there is no rerender of AddItem. Any thoughts?
Live example on WebpackBin
Here is the app
import React, { Component } from 'react';
import { BrowserRouter, Match, Link, Redirect, browserHistory } from 'react-router';
import { Provider, connect } from 'react-redux';
import { createStore, bindActionCreators } from 'redux';
import { render } from 'react-dom';
// ACTIONS
const add = (item) => {
return {
type: 'ADD',
payload: item
}
};
// REDUCER
const initialState = {
items: [
'hello',
'world'
]
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD': {
return { items: [...state.items, action.payload] };
}
default : {
return state;
}
}
}
// STORE
const store = createStore(reducer);
// ADD ITEM
class AddItem extends Component {
constructor(props) {
super(props);
this.state = {
foo: 'bar'
};
}
componentDidMount() {
console.log('mount AddItem');
}
onSubmit(event) {
event.preventDefault();
this.props.addItem(this.itemTitle.value);
this.form.reset();
this.setState({
foo: 'asd'
});
}
render() {
return (
<div>
<form onSubmit={::this.onSubmit} ref={node => this.form = node}>
<div>
<label htmlFor="item-title">Item</label>
</div>
<div>
<input id="item-title" type="text" defaultValue="baz" ref={node => this.itemTitle = node}/>
</div>
<input type="submit" value="Add" />
<div>
{this.state.foo}
</div>
</form>
<button>Click</button>
</div>
);
}
}
// ITEMS
class Items extends Component {
render() {
return (
<ul>
{
this.props.items.map(item => {
return <li key={item}>{item}</li>;
})
}
</ul>
);
}
}
// LAYOUT
class Layout extends Component {
render() {
const MatchWithProps = ({component:Component, ...rest}) => {
<Match {...rest} render={() => (
<Component {...this.props} />
)}/>
};
return (
<BrowserRouter>
<div>
Reload
<br />
<Link activeOnlyWhenExact activeClassName="active" to="/">Items</Link>
<Link activeClassName="active" to="/add">Add Item</Link>
<hr />
<Match exactly pattern="/" render={() => <Items {...this.props} /> } />
<Match exactly pattern="/add" component={() => <AddItem addItem={this.props.itemActions.add} />} />
</div>
</BrowserRouter>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items
};
}
const itemActions = { add };
const mapDispatchToProps = (dispatch) => {
return {
itemActions: bindActionCreators(itemActions, dispatch)
};
}
const ConnectedLayout = connect(mapStateToProps, mapDispatchToProps)(Layout);
// PROVIDER
const provider = (
<Provider store={store}>
<ConnectedLayout />
</Provider>
);
render(provider, document.querySelector('#react-container'));