State Management

Video 6 of part 5

Going slowly into state:

For now, we have an array of posts declared inside the Main folder, and it is passed down in props to PhotoWall (see part2).

However, if we updated that array, the UI will not be re-rendered. That's why we need state: to re-render the UI whenever a change in the data inside the state happens.

A simple state management in Main would be as follows:

Create a constructore inside the Main class, add to it the words super() [so that we can use the keyword this], and then create the state and move the whole posts array there. And pass down the props starting from state. Now, whenever a state change happens, it will automatically re-render the UI:

Main

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://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"
                }]
        }
    }
    render() {
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} />
            </div>
        )
    }
}

export default Main

Note how posts is now a part of the state (kind of like a property of state), so we no longer define it with const, and it is assigned a value through the ':' operator not the "=" operator.

Insider the render, we now access that piece of state by using this.state.posts, not just posts like we had earlier.

Removing a post when a button is clicked

Note: this will all be probalby done different once we get into redux.

We have a Main component with an array of posts in its state (each post having an id, description image link...) - there are currently 3 posts inside the array in total.

Main passes this array of posts to PhotoWall as props, and PhotoWall in turn passes that array (again, as props) to its child: Photo component.

Photo Component contains a remove button, that is supposed to remove a whole Photo group once it's clicked.

However, for a Photo group to be really removed, it should be removed from the original array of posts present in the state of Main, which would trigger a rerender producing a UI without that particular post.

So how to do that? How can the Photo component alter its grandfather, Main?

This is done through methods that can also be passed in props. We will create a method in Main, that will passed through props to PhotoWall, who will in turn pass it through props to each individual child Photo component (so all children Photo components will have the ability to call that method).

The method will update the state through setState. See code below.

Main

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: "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"
                }]
        }

        this.removePhoto = this.removePhoto.bind(this);
    }


    removePhoto(postRemoved){
        console.log(postRemoved.description);
        this.setState((state) => ({          //set state taking state as parameter because it needs to access it inside
            posts: state.posts.filter( post => post !== postRemoved)
            //the new state will have an array called posts, which is the same as that in the old state, but now,
            //it filters through the posts in the old state, and only returns those which are not equal to removed post.
        }))
    }
    render() {
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto}/>
            </div>
        )
    }
}

export default Main

PhotWall

import React, {Component} from 'react';
import Photo from './Photo';

class PhotoWall extends Component {
    render(){
        return(
            <div className="photoGrid">
                {this.props.posts.map((post, index) => <Photo key={index} post = {post}  onRemovePhoto ={this.props.onRemovePhoto}/>)}
            </div>
        )
    }
}

export default PhotoWall;

Photo

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" onClick={() => {
                        this.props.onRemovePhoto(post)
                    }}>Remove</button>
                </div>
            </figure>
        )

    }
}

export default Photo;

Note when you are calling props inside a functional component, you start directly with props..., but if you are dealing with class components, you have to do this.props....

Note regarding 'binding' this:

Note that, inside Main, we had to bind this keyword to our removePhoto method, so that we can use this inside the body of the method (else, we will get an error). Whenever we have similar methods, we should always make sure that this kewyord is bound to the method inside the constructor.

Prop Types

Why are prop types useful: sometimes we might end up passing props that are empty, or empty projects, etc... And that will throw a lot of errors and might cause a headache with debugging it.

We can save ourself that hassle by deciding early on what exact prop types we will passing.

To be able to use prop types, we should first install it:

npm install --save prop-types

Now let's go to PhotoWall and specify the prop types we want (see code below).

PhotoWall

import React, {Component} from 'react';
import Photo from './Photo';
import PropTypes from 'prop-types';

class PhotoWall extends Component {
    render(){
        return(
            <div className="photoGrid">
                {this.props.posts.map((post, index) => <Photo key={index} post = {post}  onRemovePhoto ={this.props.onRemovePhoto}/>)}
            </div>
        )
    }
}

PhotoWall.propTypes ={
    posts: PropTypes.array.isRequired,
    onRemovePhoto: PropTypes.func.isRequired
}

export default PhotoWall;

Part changed in Main for testing:

//original
 render() {
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto}/>
            </div>
        )
    }

//changed to
 render() {
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} onRemovePhoto ={{}}/>
            </div>
        )
    }

You will note that when we pass {{}} in onRemovePhoto (in the changed return of Main), we will get a browser error telling us we passed the wrong type of props (because {} means an empty project, and React is expecting a function here). Nice.

Note: we could have skipped the 'isRequired' part in the prop types. What it means is that it not only expects a certain type, but expects that it will receive a certain props. So if the props is not passed at all, it will also through an error.

We also specified the prop types for 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,
    onRemovePhoto: PropTypes.func.isRequired
}

export default Photo;

LifeCycle Methods

Constructor is one of the lifecycle methods. It gets triggered even before the component is mounted into the UI, that's why we should do nothing inside it but initialize state and bind this to our functions.

Also notice that constructor and render are being automatically called/invoked without us explicitly calling them, as opposed to our own defined functions/methods. Life cycle methods all work that way.

Now, suppose that we want to retrieve data from a database and use it in a component. We should do that after a component has alredy been mounted, and this is where one lifecycle method is extremely useful: componentDidMount().

componentDidMount() is immediately invoked once our component has been mounted into the DOM.

So assume we will simulate a call to a database to fetch data (will not deal with a real fetch right now because we just want to simulate it).

Let's create a fake function [simulateFetchFromDatabase()] in the Main component that acts as if it is fetching data, and put our posts array inside it. Then remove the data from posts array in the state, and just keep an empty posts array.

Now use the componentDidMount() function to get the data reaturned by simulateFetchFromDatabase(), and then update the state with that data.

Here is the complete code:

Main

import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';

class Main extends Component {

    constructor(){
        super();
        this.state = {
            posts: []
        }

        this.removePhoto = this.removePhoto.bind(this);
    }


    removePhoto(postRemoved){
        console.log(postRemoved.description);
        this.setState((state) => ({          //set state taking state as parameter because it needs to access it inside
            posts: state.posts.filter( post => post !== postRemoved)
            //the new state will have an array called posts, which is the same as that in the old state, but now,
            //it filters through the posts in the old state, and only returns those which are not equal to removed post.
        }))
    }

    componentDidMount(){
        const data = simulateFetchFromDatabase();
        this.setState({
            posts: data
        })

    }

    render() {
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto}/>
            </div>
        )
    }
}

function simulateFetchFromDatabase(){

    return [{
        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 Main

Now how does this code works chronologically?

The first thing ever called will be the constructor. It will set the initial state to an empty state (because posts is initially empty). Then render will be called - which calls all children components, but nothing will appear in front end because the array we are iterating through is empty. But as soon as the components is mounted (very briefly after render), the componentDidMount() function will be called, it will get the data from the database and update state with it, which will trigger a rerender [meaning, the render method will be called again] and now everything works beautifully.

So place api calls (to fetch data, etc) inside componentDidMount(). Do not get confused and place them inside compnentWillMount(). The latter one is called just before a component is mounted.

Another lifecycle method that might be useful is componentDidUpdate() - which gets called whenver we have a rerender (called directly after every other render() except the initial one).

The above was more or less a trial with lifecycle methods. Now put your code back to the way it was (keep the lifecycles empty for later use).

Main

import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';

class Main extends Component {

    constructor(){
        console.log('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: "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"
                }]
        }

        this.removePhoto = this.removePhoto.bind(this);
    }


    removePhoto(postRemoved){
        console.log(postRemoved.description);
        this.setState((state) => ({     
            posts: state.posts.filter( post => post !== postRemoved)
        }))
    }

    componentDidMount(){
        console.log('componentDidMount')
    }

    componentDidUpdate(){
        console.log('componentDidUpdate')
    }

    render() {
        console.log('render');
        return (
            <div>
                <Title title={'PhotoWall'}/>
                <PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto}/>
            </div>
        )
    }
}

export default Main