React Js and Redux
Objective:
To understand React and redux
Prerequisite:
- Use Visual studio code editor
- install Node.js --> which helps in running javascript code on your server
- node -v, npm -v --> to check the versions
ES6 (Javascript)
- Use this console for practice - https://es6console.com/
- var variables are function scoped not block scoped (like if block).
- let variables are block scoped (prefer let over var, as var allows duplicate names, which causes confusion)
- const is also block scoped like let, the only difference is let allows you to update whereas const doesn't.
const person = {
fname: 'Vinay',
lname: 'Raghumanda'
}
person = {
fname: 'Vinay',
lname: 'Raghumanda'
}
console.log(person.fname);
console.log(person.lname);
- person is an object with properties fname and lname, if we assign person with different values, it fails, if let is used no error, as it allows update.
- Arrow functions
without and with arrow function
const integers = [1,2,3];
const updatedIntegers = integers.map(function(number){ //number is simply the current element in the loop.
return number += 1;
});
const arrowUpdated = integers.map((number)=>{
return number+=2;
});
const arrowUptSimplified = integers.map(number => number+=2); //simplify further, which is implicit return
const arrowUptSimplified = integers.map(() => 'bob'); // updates every element in the array to bob
const useOfFilter = integers.filter(number => number>1);
console.log(updatedIntegers);
console.log(arrowUpdated);
- template strings - easily we can insert variables into a string without breaking the flow
const fname = 'vinay';
const lname = 'raghumanda';
const fullname = `My first name is ${fname}, and last name is ${lname}`;
console.log(fullname);
- prototype methods which are created from class objects
function Person(name, age){
this.name = name;
this.age = age;
};
const person = new Person('vinay', 24); //Person is a constructor
console.log(person);
Person.prototype.speak = function(){ //adding speak to Person object using prototype
console.log(`this is ${this.name}`);
}
person.speak();
- ES6 classes
replicating above functionality with classes
class Person{
constructor(name, age, children){
this.name=name;
this.age=age;
this.children = children;
}
speak(){
console.log(`this is ${this.name}`);
}
birth(child){
this.children.push(child);
}
}
const person = new Person('Vinay', 25, ['arunodaya', 'kaushik']); // initialise object
console.log(person);
person.speak();
person.birth('pavan'); // pushing children to array
console.log(person.children);
- Spread operator --> expand arrays and objects using spread operator
const names = ['sai', 'pavan', 'kishore']
const moreNames = ['vinod', 'vijay', 'rakesh']
// 1st way
let allNames = [];
allNames = allNames.concat(names);
allNames.push('Gio');
allNames = allNames.concat(moreNames);
console.log(allNames);
// 2nd way - using spread operator
let fullList = [...names, 'Gio', ...moreNames];
console.log(fullList);
React Intro
babel compiles ES6 (jsx) code to js5 code, as js5 is understood by most browsers.
- create react app from https://github.com/facebook/create-react-app
- npx create-react-app my-app
- cd my-app
- npm start
- Application will be up and running in http://localhost:3000/
- you can edit src/App.js for any code changes.
- to get started, just delete all files from src, except index.js
- remove all the code from index.js
Elements:
- import react and react-dom modules, and write below code
import React from 'react';
import ReactDOM from 'react-dom';
const element = React.createElement("h1", null, 'Hello!');
ReactDOM.render(element, document.getElementById('root'));
- index.html with body having id as root
- under create element, you can give different types of html tags like h1, p, strong, ol etc.
- using ol --> orderedlist
import React from 'react';
import ReactDOM from 'react-dom';
const element = React.createElement('ol', null,
React.createElement('li', null, 'Take out trash'),
React.createElement('li', null, 'shovel the driveway'),
React.createElement('li', null, 'walk the dog')
);
ReactDOM.render(element, document.getElementById('root'));
- let's make it better by using arrays and arrow function
import React from 'react';
import ReactDOM from 'react-dom';
const tasks = ['Take out the trash', 'shovel the driveway', 'walk the dog'];
const element = React.createElement('ol', null, tasks.map(task => React.createElement('li', null, task)));
ReactDOM.render(element, document.getElementById('root'));
- with above implementation it works but throws a warning like "Each child in a list should have a unique "key" prop"
- which can be addressed by defining key prop, we use index as key
import React from 'react';
import ReactDOM from 'react-dom';
const tasks = ['Take out the trash', 'shovel the driveway', 'walk the dog'];
const element = React.createElement('ol', null, tasks.map((task, index) => React.createElement('li', {key:index}, task)));
ReactDOM.render(element, document.getElementById('root'));
JSX:
- Let's make it even more better by using JSX
import React from 'react';
import ReactDOM from 'react-dom';
const tasks = ['Take out the dust', 'shovel the driveway', 'walk the dog'];
const element = <ol>
{tasks.map((task, index) => <li key={index}>{task}</li>)}
</ol>
ReactDOM.render(element, document.getElementById('root'));
- Let's try inserting header and have a look at how it's enclosed inside a div
import React from 'react';
import ReactDOM from 'react-dom';
const tasks = ['Take out the dust', 'shovel the driveway', 'walk the dog'];
const element =
<div>
<h1>Task List</h1>
<ol>
{tasks.map((task, index) => <li key={index}>{task}</li>)}
</ol>
</div>
ReactDOM.render(element, document.getElementById('root'));
Components
- we will combine all the components and prepare a main component. Main use of components is resusability
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const tasks = ['Take out the dust', 'shovel the driveway', 'walk the dog'];
class List extends Component {
render() {
return (<ol>
{tasks.map((task, index) => <li key={index}>{task}</li>)}
</ol>)
}
}
class Title extends Component {
render() {
return (<h1>Task List</h1>)
}
}
class Main extends Component {
render() {
return (<div>
<Title />
<List />
</div>)
}
}
ReactDOM.render(<Main />, document.getElementById('root'));
- let's try passing different data from props rather than hard coding array, observe props like tasks, title and look at how they are being referred.
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class List extends Component {
render() {
return (<ol>
{this.props.tasks.map((task, index) => <li key={index}>{task}</li>)}
</ol>)
}
}
class Title extends Component {
render() {
return (<h1>{this.props.title}</h1>)
}
}
class Main extends Component {
render() {
return (<div>
<Title title={'Todos'}/>
<List tasks={['wake up at 6 am', 'get to sleep by 9 pm']}/>
<List tasks={['go to office by 11 am', 'come to home by 8 pm']}/>
</div>)
}
}
ReactDOM.render(<Main />, document.getElementById('root'));
- Refactoring code, just keeping classes separately
List.js
import React, { Component } from 'react';
class List extends Component {
render() {
return (<ol>
{this.props.tasks.map((task, index) => <li key={index}>{task}</li>)}
</ol>)
}
}
export default List;
Title.js
import React, { Component } from 'react';
class Title extends Component {
render() {
return (<h1>{this.props.title}</h1>)
}
}
export default Title;
Main.js
import React, { Component } from 'react';
import List from './List';
import Title from './Title';
class Main extends Component {
render() {
return (<div>
<Title title={'Todos'} />
<List tasks={['wake up at 6 am', 'get to sleep by 9 pm']} />
<List tasks={['go to office by 11 am', 'come to home by 8 pm']} />
</div>)
}
}
export default Main;
index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Main from './Components/Main'
ReactDOM.render(<Main />, document.getElementById('root'));
React - State management
we are building a photowall app, with components like below
Main.js
import React, { Component } from 'react';
import Title from './Title';
import Photowall from './Photowall';
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: "Aliens???",
imageLink: "https://s3.india.com/wp-content/uploads/2017/12/rocket.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"
}]
class Main extends Component {
render() {
return (<div>
<Title title={'Photowall'} />
<Photowall posts={posts}/>
</div>)
}
}
export default Main;
Photowall.js
import React, { Component } from 'react';
import Photo from './Photo';
class Photowall extends Component{
render(){
return(
<div className="photo-grid">
{this.props.posts.map((post,index) => <Photo key={index} post={post}/>)}
</div>
)
}
}
export default Photowall;
Photo.js
import React, {Component} from 'react';
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"> Remove</button>
</div>
</figure>
}
}
export default Photo;
Sample style sheet:
html{
font-size: 10px;
font-family: sans-serif;
}
p{
font-size: 1.6rem;
line-height: 1.5;
}
h1{
font-family: cursive;
text-align: center;
font-size: 13rem;
font-weight: 100;
margin: 2rem 0;
letter-spacing: -1px;
color: black;
}
.photo-grid{
max-width: 1000px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
}
.figure{
flex-basis: calc(33.333% - 4rem);
border: 1px solid #d3d3d3;
padding: 2rem;
flex-grow: 1 ;
margin: 0 2rem 2rem 2rem;
}
.photo {
width: calc(100% + 4rem);
margin-left: -2rem;
margin-top: -2rem;
max-width: none;
}
.button-container {
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.remove-button{
flex-basis: 48%;
color: red;
font-size: 15px;
border: 2px solid #f2f2f2;
line-height: 2;
padding: 5px;
}
Lets convert Photowall class to function, and have a look at how props is configured
import React, { Component } from 'react';
import Photo from './Photo';
function Photowall(props){
return(
<div className="photo-grid">
{props.posts.map((post,index) => <Photo key={index} post={post}/>)}
</div>
)
}
export default Photowall;
Similarly, let's do it for others
import React, {Component} from 'react';
function Photo(props){
const post = 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"> Remove</button>
</div>
</figure>
}
export default Photo;
Title.js
import React, { Component } from 'react';
function Title(props){
return (<h1>{props.title}</h1>)
}
export default Title;
state management
- inside constructor state of the components is added
- whenever state method is triggered, render method will retrigger and update UI.
- let's modify Main.js with above changes
import React, { Component } from 'react';
import Title from './Title';
import Photowall from './Photowall';
class Main extends Component {
constructor(){
super();
this.state={
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: "Aliens???",
imageLink: "https://s3.india.com/wp-content/uploads/2017/12/rocket.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"
}]
}
}
render() {
return (<div>
<Title title={'Photowall'} />
<Photowall posts={this.state.posts}/>
</div>)
}
}
export default Main;
- Let's try deleting a post
in Main.js observe removePhoto function and prop that is passed as onRemovePhoto.
removePhoto(postRemoved){
console.log(postRemoved.description);
}
render() {
return (<div>
<Title title={'Photowall'} />
<Photowall posts={this.state.posts} onRemovePhoto={this.removePhoto}/>
</div>)
}
in Photowall.js, observe the prop
{props.posts.map((post,index) => <Photo key={index} post={post} onRemovePhoto={props.onRemovePhoto}/>)}
in Photo.js, observe onClick method
<div className="button-container">
<button className="remove-button" onClick={() => {
props.onRemovePhoto(post);
}}> Remove</button>
</div>
now lets add actual login in Main.js, observe the binding of removephoto to this
}
this.removePhoto = this.removePhoto.bind(this);
}
removePhoto(postRemoved){
console.log(postRemoved.description);
this.setState((state) => {
posts : state.posts.filter(post => post !== postRemoved)
});
}
prop types
- have it installed in your project npm install --save prop-types
- it is like telling the what parameters are expected for the attributes
import React from 'react';
import Photo from './Photo';
import PropTypes from 'prop-types';
function Photowall(props){
return(
<div className="photo-grid">
{props.posts.map((post,index) => <Photo key={index} post={post} onRemovePhoto={props.onRemovePhoto}/>)}
</div>
)
}
PropTypes.propTypes = {
posts: PropTypes.array.isRequired,
onRemovePhoto: PropTypes.func.isRequired
}
export default Photowall;
have a look at how we are setting prop types for posts and onRemovePhoto function.
life cycle methods
constructor --> render --> componentDidMount --> render
componentDidMount(){
const data = simulateDataFromDatabase()
this.setState({
posts: data
})
}
componentDidUpdate is one more method that will be called when state is updated
componentDidUpdate(prevProps, prevState){
console.log(prevState.posts)
console.log(this.state)
}
output:
import React, {Component} from 'react'
class AddPhoto extends Component {
constructor() {
super()
this.handleSubmit = this.handleSubmit.bind(this)
}
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)
}
}
render() {
return (
<div>
<h1> Photowall </h1>
<div className="form">
<form onSubmit={this.handleSubmit}>
<input type ="text" placeholder="Link" name="link"/>
<input type ="text" placeholder="Desciption" name="description"/>
<button> Post </button>
</form>
</div>
</div>
)
}
}
export default AddPhoto
Main.js
<div>
<Route exact path = "/" render={() => (
<div>
<Title title={'Photowall'}/>
<PhotoWall posts={this.state.posts} onRemovePhoto={this.removePhoto} onNavigate = {this.navigate}/>
</div>
)}/>
<Route path= "/AddPhoto" render = {({history}) => (
<AddPhoto onAddPhoto={(addedPost) => {
this.addPhoto(addedPost)
history.push('/')
}}/>
)}/>
</div>
)
}
addPhoto method
addPhoto(postSubmitted) {
this.setState(state => ({
posts: state.posts.concat([postSubmitted])
}))
}
Redux
- helps in managing state
- reducer updates state in redux, component will just emit the action, but action will be done by reducer.
- store maintains global state of the app
- install redux on to your machine - npm install redux
- Action --> Store --> Reducer
- Create a folder redux, under it reducer.js
const postReducer = function posts(state, action){
return state;
}
export default postReducer;
- now import root reducer in index.js
import { createStore } from 'redux';
import rootReducer from './redux/reducer';
const store = createStore(rootReducer)
- Create data folder and post.js, where we keep our 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: "Aliens???",
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;
- import this posts in reducer.js and assign state with posts
import posts from '../data/posts';
const postReducer = function posts(state = posts, action)
- install react-redux to get access to Provider --> npm install react-redux
- provider can pass state to deeply nested component.
- import Provider in index.js and wrap around with <provider> tag and add attribute store
import {Provider} from 'react-redux'
const store = createStore(rootReducer)
ReactDOM.render(<Provider store={store}>
<BrowserRouter>
<Main/>
</BrowserRouter>
</Provider>,
document.getElementById('root'));
- Create App.js with
import { connect } from 'react-redux';
import Main from './Main'
function mapStateToProps(state, ownProps) {
return {
posts: state
}
}
const App = connect(mapStateToProps)(Main);
export default App;
- import this App in index.js
import App from './Components/App'
ReactDOM.render(<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>,
document.getElementById('root'));
- in Main.js, we can pass
<PhotoWall posts={this.props.posts}/>
- injected our store from main.js, from main.js to Photowall using props.
- Note: configure your store in top level component
- Let's see how we can connect to deeply nested component like Photo, just have mapStateToProps function and export with connect
import { connect } from 'react-redux';
function mapStateToProps(state, ownProps) {
return {
posts: state
}
}
export default connect(mapStateToProps)(Photo);
which is not recommended, but in case.
Let's dispatch Actions
- create action.js in redux
export function removePicture(index) {
return {
type: 'REMOVE_PICTURE',
index: i
}
}
- Add one more function in App.js and looks at imports and removepost method.
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {removePost} from '../redux/action'
function mapDispachToProps(dispatch) {
return bindActionCreators({removePost}, dispatch);
}
const App = withRouter(connect(mapStateToProps, mapDispachToProps)(Main));
export default App;
- From main.js you can pass props using spread operator, have a look at component did mount, we were accessing remove picture from action.js
import {removePicture} from '../redux/actions';
ComponentDidMount(){
this.props.removePicture(1);
}
<PhotoWall {...this.props}/>
- in Photo.js, you can simply props.removePicture(1)
<button className="Remove" onClick={() => {
this.props.removePicture(i)
}}>Remove</button>
- lets pass index as a prop in Photowall.js
<Photo key={i} post={post} index={index} {...this.props}/>
in photo.js
this.props.removePicture(props.index);
create action in reducer
import {combineReducers } from 'redux';
import _posts from '../data/posts';
const postReducer = function posts(state = _posts, action) {
console.log(action.type)
switch(action.type) {
case 'REMOVE_PICTURE' :
return [...state.slice(0,action.i),...state.slice(action.i + 1)]
}
}
Let's see how to add a post
use withRouter to have Routes working properly
const App = withRouter(connect(mapStateToProps, mapDispachToProps)(Main));
Let's create a action in action.js, we can pass value directly like "post"
export function addPicture(post) {
return {
type: 'ADD_PICTURE',
post
}
}
Let's pass props in Main.js
<Route path='/AddPhoto' render={() => (<AddPost {...this.props}/>)}/>
in AddPhoto.js, let's add addPicture
this.props.addPost(post);
in reducer.js, add, simply it's adding post to the state.
case 'ADD_PICTURE' :
return [...state, action.post]
Note:
react dev tools extension in chrome
redux dev tools extension from - https://github.com/zalmoxisus/redux-devtools-extension
Add code in createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
which will allow to debug further and shows a detailed view of store variables.
Lets add Comments
- Create Single.js for view a photo on a separate screen
- Create Comments.js for comment funtionality
Comment.js
import React, {Component} from 'react'
import Photo from './Photo'
import Comments from './Comments'
class Single extends Component {
render() {
const {match, posts} = this.props
const id = Number(match.params.id)
const post = posts.find((post) => post.id === id)
const comments = this.props.comments[match.params.id] || []
const index = this.props.posts.findIndex((post) => post.id === id)
return <div className='single-photo'>
<Photo post={post} {...this.props} index={index}/>
<Comments addComment={this.props.addComment} comments={comments} id = {id}/>
</div>
}
}
export default Single
Single.js
import React, {Component} from 'react'
class Comments extends Component {
constructor() {
super()
this.handleSubmit = this.handleSubmit.bind(this)
}
render() {
return <div className="comment">
<form className="comment-form" onSubmit={this.handleSubmit}>
<input type="text" placeholder="comment" name="comment"/>
<input type="submit" hidden/>
</form>
</div>
}
}
export default Comments
Add them in Main.js
<Route path="/single/:id" render = {(params) => (
<Single {...this.props} {...params}/>
)}/>
Add this in Photo.js
<Link to={`/single/${post.id}`}> <img className= "photo" src={post.imageLink} alt={post.description}/></Link>
Let's try adding comments
First create a fucntion comment in reducer.js, use combineReducers and export root reducer
Create AddComment function in action.js
we will implement count of comments as well
Photo.js
import React from 'react';
import PropTypes from 'prop-types'
import {Link} from 'react-router-dom'
function Photo(props) {
const post = props.post
return <figure className="figure">
<Link to={`/single/${post.id}`}> <img className= "photo" src={post.imageLink} alt={post.description}/></Link>
<figcaption> <p> {post.description} </p> </figcaption>
<div className = "button-container">
<button onClick = {() => {
props.removePost(props.index)
props.history.push('/')
}}> Remove </button>
<Link className="button" to={`/single/${post.id}`}>
<div className="comment-count">
<div className="speech-bubble"> </div>
{props.comments[post.id] ? props.comments[post.id].length : 0 }
</div>
</Link>
</div>
</figure>
}
Photo.propTypes = {
post: PropTypes.object.isRequired,
}
export default Photo
Single.js
import React, {Component} from 'react'
import Photo from './Photo'
import Comments from './Comments'
class Single extends Component {
render() {
const {match, posts} = this.props
const id = Number(match.params.id)
const post = posts.find((post) => post.id === id)
const comments = this.props.comments[match.params.id] || []
const index = this.props.posts.findIndex((post) => post.id === id)
return <div className='single-photo'>
<Photo post={post} {...this.props} index={index}/>
<Comments addComment={this.props.addComment} comments={comments} id = {id}/>
</div>
}
}
export default Single
Comment.js
import React, {Component} from 'react'
class Comments extends Component {
constructor() {
super()
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(event) {
event.preventDefault()
const comment = event.target.elements.comment.value
this.props.addComment(comment, this.props.id)
event.target.elements.comment.value = ''
}
render() {
return <div className="comment">
{
this.props.comments.map((comment, index) => {
return (
<p key={index}> {comment} </p>
)
})
}
<form className="comment-form" onSubmit={this.handleSubmit}>
<input type="text" placeholder="comment" name="comment"/>
<input type="submit" hidden/>
</form>
</div>
}
}
export default Comments
action.js
//remove
export function removePost(index) {
return {
type: 'REMOVE_POST',
index
}
}
export function addPost(post) {
return {
type: 'ADD_POST',
post
}
}
export function addComment(comment, postId) {
return {
type: 'ADD_COMMENT',
comment,
postId
}
}
//adding post
reducer.js
import _posts from '../data/posts'
import {combineReducers} from 'redux'
function comments(state={}, action) {
switch (action.type) {
case 'ADD_COMMENT':
if (!state[action.postId]) {
return {...state, [action.postId]: [action.comment]}
} else {
return {...state, [action.postId]: [...state[action.postId], action.comment] }
}
default: return state
}
return state
}
function posts(state = _posts, action) {
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
}
}
const rootReducer = combineReducers({posts, comments})
export default rootReducer
App.js
import Main from './Main'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as actions from '../redux/actions'
import {withRouter} from 'react-router'
function mapStateToProps(state) {
return {
posts: state.posts,
comments: state.comments
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch)
}
const App = withRouter(connect(mapStateToProps, mapDispatchToProps)(Main))
export default App
Main.js
import React, {Component} from 'react'
import PhotoWall from './PhotoWall'
import AddPhoto from './AddPhoto'
import {Route, Link} from 'react-router-dom'
import Single from './Single'
class Main extends Component {
constructor() {
super()
}
render() {
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}/>
)}/>
<Route path="/single/:id" render = {(params) => (
<Single {...this.props} {...params}/>
)}/>
</div>
)
}
}
export default Main
Firebase and Redux
Firebase is a real time database, where we save all our data which resides inside a redux store.
set up firebase by going to firebase.google.com
go to console
Create your project
dashboard looks like below
Click on Firestore database, which is nosql db and everything we represent in nodes.
Click on rules, enable read and write permissions to true and publish
Add Firebase to your webapp, copy the code which is generated by google
or perform npm install firebase from your terminal
create database folder, under config.js
npm install redux-thunk
/AddPhoto
- import React, {Component} from 'react'
- class AddPhoto extends Component {
- constructor() {
- super()
- this.handleSubmit = this.handleSubmit.bind(this)
- }
- 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.startAddingPost(post)
- this.props.onHistory.push('/')
- }
- }
- render() {
- return (
- <div>
- <div className="form">
- <form onSubmit={this.handleSubmit}>
- <input type ="text" placeholder="Link" name="link"/>
- <input type ="text" placeholder="Desciption" name="description"/>
- <button> Post </button>
- </form>
- </div>
- </div>
- )
- }
- }
- export default AddPhoto
/App
- import Main from './Main'
- import {connect} from 'react-redux'
- import {bindActionCreators} from 'redux'
- import * as actions from '../redux/actions'
- import {withRouter} from 'react-router'
- function mapStateToProps(state) {
- return {
- posts: state.posts,
- comments: state.comments
- }
- }
- function mapDispatchToProps(dispatch) {
- return bindActionCreators(actions, dispatch)
- }
- const App = withRouter(connect(mapStateToProps, mapDispatchToProps)(Main))
- export default App
/Comments
- import React, {Component} from 'react'
- class Comments extends Component {
- constructor() {
- super()
- this.handleSubmit = this.handleSubmit.bind(this)
- }
- handleSubmit(event) {
- event.preventDefault()
- const comment = event.target.elements.comment.value
- this.props.startAddingComment(comment, this.props.id)
- event.target.elements.comment.value = ''
- }
- render() {
- return <div className="comment">
- {
- this.props.comments.map((comment, index) => {
- return (
- <p key={index}> {comment} </p>
- )
- })
- }
- <form className="comment-form" onSubmit={this.handleSubmit}>
- <input type="text" placeholder="comment" name="comment"/>
- <input type="submit" hidden/>
- </form>
- </div>
- }
- }
- export default Comments
/Main
- import React, {Component} from 'react'
- import PhotoWall from './PhotoWall'
- import AddPhoto from './AddPhoto'
- import {Route, Link} from 'react-router-dom'
- import Single from './Single'
- class Main extends Component {
- state = { loading: true }
- componentDidMount() {
- this.props.startLoadingPost().then(() => {
- this.setState({loading: false})
- })
- this.props.startLoadingComments()
- }
- render() {
- 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}/>
- )}/>
- <Route exact path="/single/:id" render = {(params) => (
- <Single loading={this.state.loading} {...this.props} {...params}/>
- )}/>
- </div>
- )
- }
- }
- export default Main
/Photo
- import React from 'react';
- import PropTypes from 'prop-types'
- import {Link} from 'react-router-dom'
- function Photo(props) {
- const post = props.post
- return <figure className="figure">
- <Link to={`/single/${post.id}`}> <img className= "photo" src={post.imageLink} alt={post.description}/></Link>
- <figcaption> <p> {post.description} </p> </figcaption>
- <div className = "button-container">
- <button onClick = {() => {
- props.startRemovingPost(props.index, post.id)
- props.history.push('/')
- }}> Remove </button>
- <Link className="button" to={`/single/${post.id}`}>
- <div className="comment-count">
- <div className="speech-bubble"> </div>
- {props.comments[post.id] ? props.comments[post.id].length : 0 }
- </div>
- </Link>
- </div>
- </figure>
- }
- Photo.propTypes = {
- post: PropTypes.object.isRequired,
- }
- export default Photo
- /Photowall
- import React from 'react'
- import Photo from './Photo'
- import PropTypes from 'prop-types'
- import {Link} from 'react-router-dom'
- //anchor tag, href attribute
- function PhotoWall(props) {
- return <div>
- <Link className = "addIcon" to="/AddPhoto"> </Link>
- <div className="photoGrid" >
- {props.posts
- .sort(function(x,y) {
- return y.id - x.id
- })
- .map((post, index) => <Photo key={index} post={post} {...props} index={index}/>)}
- </div>
- </div>
- }
- PhotoWall.propTypes = {
- posts: PropTypes.array.isRequired,
- }
- export default PhotoWall
/Single
- import React, {Component} from 'react'
- import Photo from './Photo'
- import Comments from './Comments'
- class Single extends Component {
- render() {
- const {match, posts} = this.props
- const id = Number(match.params.id)
- const post = posts.find((post) => post.id === id)
- const comments = this.props.comments[match.params.id] || []
- const index = this.props.posts.findIndex((post) => post.id === id)
- if (this.props.loading === true) {
- return <div className="loader"> ...loading </div>
- } else if (post) {
- return <div className='single-photo'>
- <Photo post={post} {...this.props} index={index}/>
- <Comments startAddingComment={this.props.startAddingComment} comments={comments} id = {id}/>
- </div>
- } else {
- return <h1> ...no post found </h1>
- }
- }
- }
- export default Single
/Title
- import React from 'react'
- function Title(props) {
- return <h1> {props.title} </h1>
- }
- export default Title
/Posts
- const posts = []
- export default posts
/Config
- import * as firebase from 'firebase'
- var config = {
- apiKey: "AIzaSyAWZObGQDfGbplDsJ0QXZGfZ22Ph9vlkgc",
- authDomain: "photowall-45333.firebaseapp.com",
- databaseURL: "https://photowall-45333.firebaseio.com",
- projectId: "photowall-45333",
- storageBucket: "photowall-45333.appspot.com",
- messagingSenderId: "586411564955"
- }
- firebase.initializeApp(config)
- const database = firebase.database()
- export {database}
/actions
- import {database} from '../database/config'
- export function startAddingPost(post) {
- return (dispatch) => {
- return database.ref('posts').update({[post.id]: post}).then(() => {
- dispatch(addPost(post))
- }).catch((error) => {
- console.log(error)
- })
- }
- }
- export function startLoadingPost() {
- return (dispatch) => {
- return database.ref('posts').once('value').then((snapshot) => {
- let posts = []
- snapshot.forEach((childSnapshot) => {
- posts.push(childSnapshot.val())
- })
- dispatch(loadPosts(posts))
- }).catch((error) => {
- console.log(error)
- })
- }
- }
- export function startRemovingPost(index, id) {
- return (dispatch) => {
- return database.ref(`posts/${id}`).remove().then(() => {
- dispatch(removePost(index))
- }).catch((error) => {
- console.log(error)
- })
- }
- }
- export function startAddingComment(comment, postId) {
- return (dispatch) => {
- return database.ref('comments/'+postId).push(comment).then(() => {
- dispatch(addComment(comment, postId))
- }).catch((error) => {
- console.log(error)
- })
- }
- }
- export function startLoadingComments() {
- return (dispatch) => {
- return database.ref('comments').once('value').then((snapshot) => {
- let comments = {}
- snapshot.forEach((childSnapshot) => {
- comments[childSnapshot.key] = Object.values(childSnapshot.val())
- })
- dispatch(loadComments(comments))
- })
- }
- }
- export function loadComments(comments) {
- return {
- type: 'LOAD_COMMENTS',
- comments
- }
- }
- export function removePost(index) {
- return {
- type: 'REMOVE_POST',
- index
- }
- }
- export function addPost(post) {
- return {
- type: 'ADD_POST',
- post
- }
- }
- export function addComment(comment, postId) {
- return {
- type: 'ADD_COMMENT',
- comment,
- postId
- }
- }
- export function loadPosts(posts) {
- return {
- type: 'LOAD_POSTS',
- posts
- }
- }
/reducer
- import _posts from '../data/posts'
- import {combineReducers} from 'redux'
- function comments(state={}, action) {
- switch (action.type) {
- case 'ADD_COMMENT':
- if (!state[action.postId]) {
- return {...state, [action.postId]: [action.comment]}
- } else {
- return {...state, [action.postId]: [...state[action.postId], action.comment] }
- }
- case 'LOAD_COMMENTS': return action.comments
- default: return state
- }
- return state
- }
- function posts(state = _posts, action) {
- switch (action.type) {
- case 'REMOVE_POST': return [...state.slice(0, action.index), ...state.slice(action.index + 1)]
- case 'ADD_POST': return [...state, action.post]
- case 'LOAD_POSTS': return action.posts
- default: return state
- }
- }
- const rootReducer = combineReducers({posts, comments})
- export default rootReducer
/index
- import React from 'react'
- import ReactDOM from 'react-dom'
- import './styles/stylesheet.css'
- import {BrowserRouter} from 'react-router-dom'
- import {createStore, applyMiddleware} from 'redux'
- import rootReducer from './redux/reducer'
- import {Provider} from 'react-redux'
- import App from './Components/App'
- import thunk from 'redux-thunk'
- import {database} from './database/config'
- const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), applyMiddleware(thunk))
- ReactDOM.render(<Provider store={store}><BrowserRouter><App/></BrowserRouter></Provider>, document.getElementById('root'))
Wrapping Up
Final Project -
Comments
Post a Comment