Part 7 of the course
You already know redux, but reviewing some theory: We use Redux to manage the state. Because if we are managing the state without redux (like what we were doing previosuly), many state changes might be invoked in the same time and we might enter a hell of bugs.
With redux, components do not change the state directly, they change it by invoking functions (called actions) that behave in an orginized manner without interfering with each other. It is actually the function of the 'reducer' (invoked by actions) to change the state. Since only the Reducer changes the state, we remove the issue coming from several change attempts happening at the same time.
Make sure to install it. Go to your app directory and npm install redux
-- shouldn't take a lot of time.
Now go to Index and import {createStore} from 'redux'
We will have several Reducer groups (or State Collections), but for now we've only be dealing with posts, so we will only include a post reducer.
In your src folder, create a new folder called redux, and inside it, create a new file called reducer.js. We created a reducer function there (see later in code).
We then went to index.js, imported the rootReducer:
import rootReducer from '.redux/reducer'
and then created the store using it:
const store = createStore(rootReducer);
Now we would like to define our 'default state'. For now, we've always been dealing with a posts array that was placed in the state of Main. It is better to keep it alone somewhere (note that we will delete it altogether later on), but for now, create a new folder in src called data, inside it, create a new file name posts.js, and inside it, place the posts array (but declare it as a const here). Don't forget to export:
Posts.js
const posts = [{
id: 0,
description: "beautiful landscape",
imageLink: "https://image.jimcdn.com/app/cms/image/transf/none/path/sa6549607c78f5c11/image/i4eeacaa2dbf12d6d/version/1490299332/most-beautiful-landscapes-in-europe-lofoten-european-best-destinations-copyright-iakov-kalinin.jpg" +
"3919321_1443393332_n.jpg"
}, {
id: 1,
description: "Hello I am ghadir",
imageLink: "https://img.purch.com/rc/640x415/aHR0cDovL3d3dy5zcGFjZS5jb20vaW1hZ2VzL2kvMDAwLzA3Mi84NTEvb3JpZ2luYWwvc3BhY2V4LWlyaWRpdW00LWxhdW5jaC10YXJpcS1tYWxpay5qcGc=" +
"08323785_735653395_n.jpg"
}, {
id: 2,
description: "On a vacation!",
imageLink: "https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/2017/08/24/104670887-VacationExplainsTHUMBWEB.1910x1000.jpg"
}];
export default posts
We now want our reducer to return that data to the state as an intial state value. So we will import it to the reducer.js file and deal with it there (see code). We could have also just put it inside reducer.js directly but this just complicated stuff.
Reducer
import posts from '../data/posts'
const postReducer = (state = posts, action) => {
return state;
};
export default postReducer
Index
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Main from './components/Main';
import './styles/stylesheet.css';
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import {createStore} from 'redux'
import rootReducer from '.redux/reducer'
const store = createStore(rootReducer);
ReactDOM.render(<BrowserRouter><Main/></BrowserRouter>, document.getElementById('root'));
Install:
npm install react-redux
Now go to index.js and import:
import {Provider} from 'react-redux';
And then wrap your <BrowserRouter>
with <Provider>
tags.
Now we will create a new file called App.js in our Components folder. Our aim from creating App is to connect <Main/>
with the store (so we will replace <Main/>
in the original Index.js file by <App/>
after connection has been established). (It looks like it is easier at this point to create a new root component rather than just connect the store to Main, for organizational purposes). More explanation later.
Go to Main, and for now comment all the old state-related stuff (posts array, components that pass props in the render, etc).
Go back to App.js. We wrote the following inside it:
App.js
import {connect} from 'react-redux'
import Main from './Main'
function mapStateToProps(state){
return{
posts: state
//assume state had two properties: 'pictures' and 'names'.
//we could have said: "posts: state.names" - therefore only passing
//the 'names' part of the state. Here we wanted to pass all of state,
//and all we did was that we renamed it as posts.
}
}
const App = connect(mapStateToProps)(Main)
export default App
As the first argument passed in to connect
, mapStateToProps
is used for selecting the part of the data from the store that the connected component needs. It’s frequently referred to as just mapState for short.
[Note that in this case, it is passing only posts -> as that is kinda all our state. We specified that before using connect].
What does the above function [mapStateToProps] do? It will take our state, that lives in our store, and map it to props. These props can be accessed from within a component through the keyword posts (note it inside the function declaration).
Therefore, it allows us to specify which data from the store we want to pass (or: inject) to a certain React Component. Here we want to pass all of the state to Main, but we are renaming the state as posts (we currently only have posts in state anyway). Therefore, that's what we put inside the declaration, and what will passed throught the connect function below.
Note: it is the Provider (imported and used in Index.js, see code) that creates the ability to 'inject' a piece of state from the root component to any of its distant grandchildren, without having to pass props down the line. So Provider offers that functionality, and to use it, I have to use the connect function (to connect my component to the required piece of state/all of state if needed).
Here is the new code in Index.js:
ReactDOM.render(<Provider store={store}>
<BrowserRouter><Main/></BrowserRouter>
</Provider>, document.getElementById('root'));
So we actually want to pass the store from the <Provider/>
down to <Main/>
. Note that we will do this only once: we will not be injecting store into every other component, just the Main one(or highest level component). It's a good pattern to follow.
Okay, here is the idea: looks like we are still going to be passing the state down using props from Main to its children. Except: when we will need the data/state in a very deep nested subcomponent, then we are going to inject it there. But for now we are only injecting it in the Main (highest level component) and passing it down as props.
So we had to do three things for establishing the connection:
So now that Main is connected to our declared mapStateToProps, we can simply access posts from within Main.js by typing:
this.props.posts
Note: App.js has no declared component inside it, it only has the stateMapToProps function declaration. However, it exports 'App' which is kind of a pseudo component - or a container component - that is only there to contain Main and to establish the connection between Main and the state. App is the container component, while Main is the presentational components (concerns itself with UI and such).
We could have probably also put everything inside Main, (mapStateToProps function delcaration and all), and then at the end, put:
export default connect(mapStateToProps)(Main)
Here is an example from a different project that exports itself and has the mapStateToProps declared directly inside it:
Dashboard/different project
import React, { Component } from 'react'
import ProjectList from '../projects/ProjectList'
import Notifications from './Notifications'
import { connect } from 'react-redux'
class Dashboard extends Component {
render() {
console.log("hello this is Ghadir, " + this.props);
const { projects } = this.props;
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<ProjectList/> {/*was: <ProjectList projects={projects} />*/}
</div>
<div className="col s12 m5 offset-m1">
<Notifications />
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
projects: state.project.projects
}
}
export default connect(mapStateToProps)(Dashboard)
//note: we can still use <Dashboard/> as a normal export outside.
Kill me. We actually went ahead and did this exact thing to Main. However, he reverted it back and kept the 'App' for better understanding (separation of concerns). You can do it however you want, it doesn't matter.
Repeated note: now state exists in the props of Main, we can access it inside Main as
this.props.posts
. You can console.log it and see the pretty objects in your console.
Okay, now that we established everything, we are going to redo some stuff inside main.
As mentioned earlier, we are still going to pass stuff as props down to children because there is no deep level nesting in here (direct children and grandchildren are using the state, so pass it as props, no problem, it's only one level down). We will see later how to inject state into very far nested children.
Here is a thing: when you organize your app, you have to decide on certain 'logical' top level components, which you'll inject the state into, and then those components will pass that state into 1,2, and maybe 3 level components below them.
We have to try and preserve the 'top-down' flow of data. Data should be unidirectional in a React app. We can however break that rule when we have the scenario where a deep nested child (say, 5 level down) needs a part of the state, but none of the intermediaries actually use that part of the state. It then makes sense to inject it directly into that child component (this won't be a very common occurance).
Keep in mind, that in this app, it is useless to inject state into Photos since we are already easily passing the state to it from its parent and grandparent. This is just for demonstration.
Photo, before edit
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class Photo extends Component {
render(){
const post = this.props.post
return (
<figure className="figure">
<img className="photo" src={post.imageLink} alt={post.description}></img>
<figcaption><p>{post.description}</p></figcaption>
<div className = "button-container">
<button className="remove-button" onClick={() => {
this.props.onRemovePhoto(post)
}}>Remove</button>
</div>
</figure>
)
}
}
Photo.propTypes ={
post: PropTypes.object.isRequired,
}
export default Photo;
Photo, after edit
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux'
class Photo extends Component {
render(){
const post = this.props.post
return (
<figure className="figure">
<img className="photo" src={post.imageLink} alt={post.description}></img>
<figcaption><p>{post.description}</p></figcaption>
<div className = "button-container">
<button className="remove-button" onClick={() => {
this.props.onRemovePhoto(post)
}}>Remove</button>
</div>
</figure>
)
}
}
function mapStateToProps(state){
return{
posts: state
}
}
Photo.propTypes ={
post: PropTypes.object.isRequired,
}
export default connect(mapStateToProps)(Photo);
All of the current components and files:
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);
ReactDOM.render(<Provider store={store}><BrowserRouter><App/></BrowserRouter></Provider>, document.getElementById('root'));
App
import {connect} from 'react-redux'
import Main from './Main'
function mapStateToProps(state){
return{
posts: state
//assume state had two properties: 'pictures' and 'names'.
//we could have said: "posts: state.names" - therefore only passing
//the 'names' part of the state. Here we wanted to pass all of state,
//and all we did was that we renamed it as posts. (I guess).
}
}
const App = connect(mapStateToProps)(Main)
export default App
Main
import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';
import AddPhoto from './AddPhoto';
import {Route} from 'react-router-dom'
class Main extends Component {
constructor(){
console.log('constructor');
super();
}
render() {
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
PhotoWall
import React, {Component} from 'react';
import Photo from './Photo';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom'
class PhotoWall extends Component {
render(){
return(
<div>
<Link className="addIcon" to="/AddPhoto"></Link>
<div className="photoGrid">
{this.props.posts
.sort(function(x, y){
return y.id - x.id
})
.map((post, index) => <Photo key={index} post = {post} onRemovePhoto ={this.props.onRemovePhoto}/>)}
</div>
</div>
)
}
}
PhotoWall.propTypes ={
posts: PropTypes.array.isRequired,
}
export default PhotoWall;
Photo
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class Photo extends Component {
render(){
const post = this.props.post
return (
<figure className="figure">
<img className="photo" src={post.imageLink} alt={post.description}></img>
<figcaption><p>{post.description}</p></figcaption>
<div className = "button-container">
<button className="remove-button" onClick={() => {
this.props.onRemovePhoto(post)
}}>Remove</button>
</div>
</figure>
)
}
}
Photo.propTypes ={
post: PropTypes.object.isRequired,
}
export default Photo;
AddPhoto
import React, {Component} from 'react';
class AddPhoto extends Component {
//we are mostly declaring a constructor here just to bind 'this' to handleSubmit
constructor(){
super();
this.handleSubmit = this.handleSubmit.bind(this);
}
//note: on form submissions, we get a page refresh.
//we don't like page refresh.
//That's why we caught the event in the argument of handleSubmit and prevented it.
handleSubmit(event){
event.preventDefault();
//in the below, target refers to the submitted form7
//elements refer to elements inside the form
//we can access individual elements through their names
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); //post here is the object created above
}
}
render(){
return(
<div>
<h1>Photowall</h1>
<div className ="form">
<form onSubmit = {this.handleSubmit}>
<input type="text" placehoder="Link" name='link'></input>
<input type="text" placehoder="Description" name='description'></input>
<button>Post</button>
</form>
</div>
</div>
)
}
}
export default AddPhoto
Title
import React, {Component} from 'react';
class Title extends Component {
render(){
return <h1> {this.props.title} </h1>
}
}
export default Title
Reducer.js [in /redux]
import posts from '../data/posts'
const postReducer = (state = posts, action) => {
return state;
};
export default postReducer
posts.js [in /Data]
const posts = [{
id: 0,
description: "beautiful landscape",
imageLink: "https://image.jimcdn.com/app/cms/image/transf/none/path/sa6549607c78f5c11/image/i4eeacaa2dbf12d6d/version/1490299332/most-beautiful-landscapes-in-europe-lofoten-european-best-destinations-copyright-iakov-kalinin.jpg" +
"3919321_1443393332_n.jpg"
}, {
id: 1,
description: "Hello I am ghadir",
imageLink: "https://img.purch.com/rc/640x415/aHR0cDovL3d3dy5zcGFjZS5jb20vaW1hZ2VzL2kvMDAwLzA3Mi84NTEvb3JpZ2luYWwvc3BhY2V4LWlyaWRpdW00LWxhdW5jaC10YXJpcS1tYWxpay5qcGc=" +
"08323785_735653395_n.jpg"
}, {
id: 2,
description: "On a vacation!",
imageLink: "https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/2017/08/24/104670887-VacationExplainsTHUMBWEB.1910x1000.jpg"
}];
export default posts
Recap: we created a store inside index.js, and used a reducer (created in redux folder) to populate the store with state.
We injected that state to our Main component (using Provider + connect + a container called App to separate concerns), and from Main we passed pieces of state as props to its children components.