Part 7 of the course, video 6.
Create a new file inside of redux folder called actions.js
Actions.js
//remove
export function removePost(index){ //takes index of the post to be removed
//actions are just javascript objects
//I am returning an action here.
//the whole function is an action creator
//we use action creators to be make actions portable
return {
type: 'REMOVE_POST',
index: index
}
}
//note: in the above, we could have used post instead of index,
//but index is better since we don't want to send large pieces
//of data through our events.
Note that we are using ES5 function syntax, not ES6 (arrow functions). This is because the guy of the tutorial is trying to make things easier for us to learn. However, you usually won't use a named function for the action creator, and will probably just use its type name to dispatch (below).
So now, since Main is connected to the Redux store, I can dispatch my removePost action (or action creator) from there.
We did it based on a lifecycle method: componentDidMount
. Make sure to import your action into Main to use it.
Main
import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';
import AddPhoto from './AddPhoto';
import {Route} from 'react-router-dom'
import {removePost} from '../redux/actions'
class Main extends Component {
constructor(){
console.log('constructor');
super();
}
componentDidMount(){
this.props.dispatch(removePost(1));
}
render() {
console.log(this.props)
return (
<div>
<Route exact path = '/' render={() => (
<div>
<Title title={'PhotoWall'}/>
<PhotoWall posts = {this.props.posts} />
</div>
)} />
{/* <Route path = '/AddPhoto' render={({history}) => (
<div>
<AddPhoto onAddPhoto={(addedPost) =>{
this.addPhoto(addedPost);
history.push('/');
}}/>
</div>
)} /> */}
</div>
)
}
}
export default Main
[note that when we dispatch it, it won't do anything because the reducer code does not handle it yet].
Note that our action removePost(index) takes an argument, index. So when dispatching it above, we gave it a random payload of '1' (payload = data in an action).
We console.log(action.index) in the reducer:
import posts from '../data/posts'
const postReducer = (state = posts, action) => {
console.log(action.index)
return state;
};
export default postReducer
It first gave a 'undefined' in the console (because it gets called at first at store creation, and we're trying to print action.index but nothing of that sort is being emmited at that time).
Then it gets properly called again when we dispatch the action on the lifecylce (didMount), and that's when it will print a '1' (the random index we gave it).
(and probaly only works when you've already created a 'concern separation' system where you have an App container.
Now, there is a more concise way to call an action dispatcher. To implement it, go to App.js (where our Main is connected to the store) and create the function mapDispatchToPros
. This function is normally used to provide a shorthand inside our component, such that, instead of writing this:
componentDidMount(){
this.props.dispatch(removePost(1));
}
We can directly write:
componentDidMount(){
this.props.removePost(1);
}
//sooooo - just removed the dispatch? Ok.
This is implemented to keep separate parts dealing with separate functionality (so that our Main stay focused on UI on so on). Even though it still looks like I will be calling the function from Main (?).
Now go to the container commponent (App), and import:
import {bindActionCreator} from redux
import {removePost} from '../redux/actions
Now change App code as follows:
App
import {connect} from 'react-redux'
import Main from './Main'
import {removePost} from '../redux/actions'
import { bindActionCreators } from 'redux'
function mapStateToProps(state){
return{
posts: state
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({removePost}, dispatch)
}
const App = connect(mapStateToProps, mapDispatchToProps)(Main)
export default App
What we did here with the mapDispatchToProps(dispatch)
:
Whenever the function 'removePost' is called, mapDispatchToProps
will bind a dispatch to it (or will dispatch it). That is why now I can call it from main without dispatching it. (Again, this is optional).
Main
//...
componentDidMount(){
this.props.removePost(1);
//calling function without the dispatch
}
//...
Now remove everything from your components related to 'onRemovePhoto' and so on. We removed those instances from PhotoWall and Photo.
Now, the action of 'removePost' need to be dispatched from within Photo (on button click). When dispatched, we need to pass information to it from the state, to update the state.
But currently, Photo does no have the state. So we are going to pass down the state from Main -> PhotoWall -> Photo through props (because they are not deep enough).
They are actually being passed down currently. Here is how it looks in Main:
<PhotoWall posts = {this.props.posts} />
But we also need to pass the dispatch method removePost, which is a part of the same props. (So it seems dispatch methods are part of the state - ?).
Note: We need to pass it down so that we can emit actions from Photo.
Now, we could create a new prop and pass down the dispatch method in it, as follows:
<PhotoWall posts = {this.props.posts} removePost = {this.props.removePost} />
Or, since we are virtually passing everything from the state, pass the whole state down using the spread operator:
<PhotoWall {this.props} />
//now we are not passing posts alone and dispatch alone, we are passing the whole source
The last piece of code is equivalent to the one before it.
Now, to PhotoWall:
In the case of PhotoWall, we didn't remove previously passed props (post for ex.), just added one with a spread operator (don't know why):
PhotoWall - relevant line
.map((post, index) => <Photo key={index} post = {post} {this.props}/>)}
Now to Photo:
Photo - relevant line
<button className="remove-button" onClick={() => {
this.props.removePost(1);
}}>Remove</button>
We haven't yet implemented the functionality, but so far for now, when the button is clicked, we would only get the console log in the console.
Now we're going to make the reducer actually do something when the action is called.
First, we need to tell the reducer which post we want to remove. We are currently passing a useless '1' in the action parameter. We want to change that '1' into useful information.
If we check PhotoWall, we are already passing the index to the Photo componets as a Key. So we are going to go ahead and pass it as an indes too:
.map((post, index) => <Photo key={index}
post = {post} {this.props} index={index}/>)}
Now go to Photo, and in the argument of removePost(), call the specific index of that post:
<button className="remove-button" onClick={() => {
this.props.removePost(this.props.index);
}}>Remove</button>
Now our reducer, that hasn't yet been prepared to deal with the actions, will console.log the index into the console.
Now, go to Main and remove the componentDidMount method that calls removePost().
Now go to the reducer and update it as follows:
import posts from '../data/posts'
const postReducer = (state = posts, action) => {
console.log(action.index)
switch(action.type){
case 'REMOVE_POST': return [state.slice(0, action.index), state.slice(action.index + 1)]
default: return state
}
};
export default postReducer
What we did: In case there were no actions dispatched, then return the default state.
But in case the action 'REMOVE_POST' has been dispatched, then we want to return a new altered state that skips the post that has the index specified by action.index (so we are passing info through action properties).
We did this by using the spread operator: inside array bounds ([]), first put all the elements from index zero up the index of action.index [not included], then put all the elemets from index action.index + 1 up till end of array.
We can use slice because it doesn't modify original array (mutability).
We put a comma between the two spread opreators because at the end of the day, they are only returning elements, and those elements will be separated by commas in first place.
Everything is working beautifully.
Go to Main and uncomment the second <Route>
stuff, and remove no longer used stuff:
Main
<Route path = '/AddPhoto' render={({history}) => (
<AddPhoto/>
)} />
Now, if we clicked on the (+) button in the UI, it is supposed to change to the /AddPhoto URL. But it doesn't. This is because of how Redux kind of messes up the system.
To make it work again, you have to go to App, import
import {withRouter} from 'react-router'
And then wrap your connect function with the withRouter function:
App
import {connect} from 'react-redux'
import Main from './Main'
import {removePost} from '../redux/actions'
import { bindActionCreators } from 'redux'
import {withRouter} from 'react-router'
function mapStateToProps(state){
return{
posts: state
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({removePost}, dispatch)
}
const App = withRouter(connect(mapStateToProps, mapDispatchToProps)(Main))
export default App
Now, go to AddPhoto.js and remove previous logic.
Now alter the actions.js file:
//remove
export function removePost(index){
console.log(`I am removePost, I've been called with index ${index}`);
return {
type: 'REMOVE_POST',
index: index //we could have just put one index here. See below.
}
}
//adding post
export function addPost(post){
return{
type: 'ADD_POST',
post //this is same as saying "post: post", but it is a shorthand notation in ES6
}
}
Now go to App.js and import this action (actually, instead of importing action by action, you can import all of them together at the same time):
App
import {connect} from 'react-redux'
import Main from './Main'
// import {removePost} from '../redux/actions' //replaced by all actions below
import * as actions from ../redux/actions
import { bindActionCreators } from 'redux';
import {withRouter} from 'react-router'
function mapStateToProps(state){
return{
posts: state
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actions, dispatch) //we had {removePost} instead of actions here previously
}
const App = withRouter(connect(mapStateToProps, mapDispatchToProps)(Main))
export default App
Now we need to pass those actions from Main (where store is injected) down to AddPhoto to be used.
In Main, alter AddPhoto as follows:
Main (part)
<Route path = '/AddPhoto' render={({history}) => (
<AddPhoto {this.props}/>
)} />
Now go to AddPhoto and alter handleSubmit as follows:
handleSubmit(event){
event.preventDefault();
const imageLink = event.target.elements.link.value;
const description = event.target.elements.description.value;
const post = {
id: Number(new Date()),
description: description,
imageLink: imageLink
}
if (description && imageLink){
//this.props.onAddPhoto(post); //no longer used
this.props.addPost(post)
}
}
Now alter the Reducer so that it handles ADD_POST:
Reducer
import posts from '../data/posts'
const postReducer = (state = posts, action) => {
console.log(action.index)
switch(action.type){
case 'REMOVE_POST': return [state.slice(0, action.index), state.slice(action.index + 1)]
case 'ADD_POST': return [state, action.post]
default: return state
}
};
export default postReducer
Note how we just added the action.post as an element next to our state array. That is why the spread operator is so useful (and we kept everything immutable).
Now everything in the app should work smoothly like before, except that when we submit a new photo, it doesn't go back directly to the main page. This is because in this new setup we didn't use history.
We need to use it but in a different way now: we will pass it as a prop to AddPhoto:
Main
<Route path = '/AddPhoto' render={({history}) => (
<AddPhoto {this.props} onHistory={history}/>
)} />
And now inside AddPhoto, we access it via props:
AddPhoto - only handleSubmit
handleSubmit(event){
event.preventDefault();
const imageLink = event.target.elements.link.value;
const description = event.target.elements.description.value;
const post = {
id: Number(new Date()),
description: description,
imageLink: imageLink
}
if (description && imageLink){
this.props.addPost(post)
this.props.onHistory.push('/');
}
}
Now we want to make the title of the app clickable. We will remove the title from both Main and PhotoWall (Main has it as a component, while PhotoWall has it as a simple h1 element).
Now go back to Main and add a new <h1>
element containing <Link>
, and having the path of '/':
Main - render method
render() {
console.log(this.props)
return (
<div>
<h1><Link to="/">Photowall</Link></h1>
<Route exact path = '/' render={() => (
<div>
<PhotoWall {this.props} />
</div>
)} />
<Route path = '/AddPhoto' render={({history}) => (
<AddPhoto {this.props} onHistory={history}/>
)} />
</div>
)
}
Now style the thing in CSS. Done.
Note from the tutorial guy:
In the last tutorial, we passed down {onHistory=history } from the main component to the addPhoto component. And then inside of addPhoto we called this.props.onHistory.push('/') . This is redundant.
Why?
The main component is connected to our store such that it's wrapped with <BrowserRouter>
. Therefore inside of it's props, Main has access to the history prop as this.props.history , and since we're passing down all of Main's props down to AddPhoto with {...this.props} , then inside of AddPhoto, you can simply call this.props.history . Therefore you do not need to pass in the props onHistory=history down to AddPhoto.
Try it out, remove{onHistory=history} , and inside of handleSubmit, simply call this.props.history.push('/') , you should get the same results.
Note: also remove history from the argument of the the function inside Route in Main (the one of AddPhoto) because it is no longer used.
Worked beautifully.
We are going to add a devtool to chrome for redux (I am not sure how helpful this is). After adding it, you need to add the following code to your store:
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
As such:
Index
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import './styles/stylesheet.css';
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import {createStore} from 'redux'
import rootReducer from './redux/reducer'
import {Provider} from 'react-redux';
import App from './components/App'
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>, document.getElementById('root'));
Now, you can open the dev tools, and open redux tab. Under the 'filter', you will see all actions dispatched by your application.
So if you click remove you will see the remove action being called and so on (great for debugging).
You can look at the details of each action in the right hand pane. In Action
, you can see all about the action. In State
, you can see what was included in the state after selected action was completed. Diff
tells us the difference that was caused by the action (so for example, it will show us the post that was removed or the post that was added). You can view this details as 'Tree' (concise) or as 'Raw' (more code like) in the different tab options.
There is also a slider at the bottom that helps us go back in time and see what actions where preformed when. If we go back to the beginning, our UI will look like the initial state, before any action was done. Or, if you want to focus on an action that happened earlier, you can click 'jump' next to that action, and the UI will be presented the way it was for that action.