Learning Full-Stack JavaScript Development: MongoDB, Node and React


6. Routing on Client and Server


023 Handling the contest click event


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';

class App extends React.Component {
  
 constructor(props){
super(props);
  
 this.state = {
pageHeader: 'Naming Contests',
contests: this.props.initialContests
};
}
  
 componentDidMount() {

    }

    componentWillUnmount() {

    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.state.pageHeader} />
                <ContestList contests={this.state.contests} />
            </h2>
        );
    }

};

export default App;


ContestList.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ContestPreview from './ContestPreview';
import PropTypes from 'prop-types';

const ContestList = ({ contests }) => (
<div className="ContestList">  
 {contests.map(contest =>  
 <ContestPreview key={contest.id} {...contest} />
)}
</div>
);

ContestList.propTypes = {
contests: PropTypes.array
}

export default ContestList;


ContestPreview.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class ContestPreview extends Component {
  
 constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}

    handleClick(){
        console.log(this.props.id);
    };

    render() {
        return (
            <div className="link ContestPreview" onClick={this.handleClick}>
                <div className="category-name">
                    {this.props.categoryName}
                </div>
                <div className="contest-name">
                    <div>{this.props.contestName}</div>
                </div>
            </div>
        );
    }

}

ContestPreview.propTypes = {
categoryName: PropTypes.string.isRequired,
contestName: PropTypes.string.isRequired
}

export default ContestPreview;


024 Navigating to a contest


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

class App extends React.Component {
  
 constructor(props){
super(props);
  
 this.state = {
pageHeader: 'Naming Contests',
contests: this.props.initialContests
};
}
  
 componentDidMount() {

    }

    componentWillUnmount() {

    }

    fetchContest(contestId){
        pushState(
            { currentContestId: contestId },
            `/contest/${contestId}`
        )
    };

    render() {

        return (
            <h2 className="App">
                <Header message= {this.state.pageHeader} />
                <ContestList
                    onContestClick={this.fetchContest}
                    contests={this.state.contests} />
            </h2>
        );
    }

};

export default App;


ContestList.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import ContestPreview from './ContestPreview';
import PropTypes from 'prop-types';

const ContestList = ({ contests, onContestClick }) => (
<div className="ContestList">  
 {contests.map(contest =>  
 <ContestPreview
key={contest.id}
onClick={ onContestClick }
{...contest} />
)}
</div>
);

ContestList.propTypes = {
contests: PropTypes.array,
onContestClick: PropTypes.func.isRequired
}

export default ContestList;


ContestPreview.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class ContestPreview extends Component {
  
 constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}

    handleClick(){
        // console.log(this.props.id);
        this.props.onClick(this.props.id);
    };

    render() {
        return (
            <div className="link ContestPreview" onClick={this.handleClick}>
                <div className="category-name">
                    {this.props.categoryName}
                </div>
                <div className="contest-name">
                    <div>{this.props.contestName}</div>
                </div>
            </div>
        );
    }

}

ContestPreview.propTypes = {
id: PropTypes.number.isRequired,
categoryName: PropTypes.string.isRequired,
contestName: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
}

export default ContestPreview;


025 Looking up the contest on route change


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';
import Contest from './Contest';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

class App extends React.Component {
  
 constructor(props){
super(props);
this.fetchContest = this.fetchContest.bind(this);
this.currentContent = this.currentContent.bind(this);
  
 this.state = {
pageHeader: 'Naming Contests',
contests: this.props.initialContests
};
}
  
 componentDidMount() {

    }

    componentWillUnmount() {

    }

    fetchContest(contestId){

        pushState(
            { currentContestId: contestId },
            `/contest/${contestId}`
        );

        this.setState({
            pageHeader: this.state.contests[contestId].contestName,
            currentContestId: contestId
        });
    };

    currentContent() {
        if (this.state.currentContestId) {
            return <Contest {...this.state.contests[this.state.currentContestId]} />;
        }

        return <ContestList
            onContestClick={this.fetchContest}
            contests={this.state.contests} />;
    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.state.pageHeader} />
                {this.currentContent()}
            </h2>
        );
    }

};

export default App;


Contest.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component} from 'react';
import PropTypes from 'prop-types';

class Contest extends React.Component {
render() {
return (
<div className="Contest">
{this.props.id}
</div>
);
  
 }
}

Contest.propTypes = {
id: PropTypes.number.isRequired
}

export default Contest;


ContestList.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import ContestPreview from './ContestPreview';
import PropTypes from 'prop-types';

const ContestList = ({ contests, onContestClick }) => (
<div className="ContestList">  
 {Object.keys(contests).map(contestId =>  
 <ContestPreview
key={contestId}
onClick={ onContestClick }
{...contests[contestId]} />
)}
</div>
);

ContestList.propTypes = {
contests: PropTypes.object,
onContestClick: PropTypes.func.isRequired
}

export default ContestList;


026 Fetching contest information from the API


api/index.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import express from 'express';
import data from '../src/testData';

const router = express.Router();

const contests = data.contests.reduce((obj, contest) => {
obj[contest.id] = contest;
return obj;
}, {});

router.get('/contests', (req, res) => {
res.send({ contests: contests });
});

router.get('/contests/:contestId', (req, res) => {
let contest = contests[req.params.contestId];
contest.description = 'Any Text';
res.send({ contest });
});

export default router;


api.js


1
2
3
4
5
6
import axios from 'axios';

export const fetchContest = contestId => {
return axios.get(`/api/contests/${contestId}`)
.then( resp => resp.data.contest )
};


Contest.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component} from 'react';
import PropTypes from 'prop-types';

class Contest extends React.Component {
  
 render() {
  
 return (
<div className="Contest">
{this.props.description}
</div>
);
}
}

Contest.propTypes = {
description: PropTypes.string.isRequired
}

export default Contest;


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';
import Contest from './Contest';
import \* as api from '../api';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

class App extends React.Component {
  
 constructor(props){
  
 super(props);
  
 this.fetchContest = this.fetchContest.bind(this);
this.currentContent = this.currentContent.bind(this);
  
 this.state = {
pageHeader: 'Naming Contests',
contests: this.props.initialContests,
};
}
  
 componentDidMount() {

    }

    componentWillUnmount() {

    }

    fetchContest(contestId){

        pushState(
            { currentContestId: contestId },
            `/contest/${contestId}`
        );


        api.fetchContest(contestId).then(contest => {

            this.setState({
                pageHeader: contest.contestName,
                currentContestId: contest.id,
                contests: {
                    [contest.id]: contest
                }
            });
        });
    };

    currentContent() {

        if (this.state.currentContestId) {
            return <Contest {...this.state.contests[this.state.currentContestId]} />;
        }

        return <ContestList
            onContestClick={this.fetchContest}
            contests={this.state.contests} />;
    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.state.pageHeader} />
                {this.currentContent()}
            </h2>
        );
    }

};

export default App;

http://localhost/api/contests/4


027 A bit of refactoring


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';
import Contest from './Contest';
import \* as api from '../api';
import PropTypes from 'prop-types';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

class App extends React.Component {
  
 constructor(props){
  
 super(props);
  
 this.fetchContest = this.fetchContest.bind(this);
this.currentContent = this.currentContent.bind(this);
  
 // static propTypes = {
// initialData: PropTypes.object.isRequired
// }

        this.state = this.props.initialData;
    }

    componentDidMount() {

    }

    componentWillUnmount() {

    }

    fetchContest(contestId){

        pushState(
            { currentContestId: contestId },
            `/contest/${contestId}`
        );


        api.fetchContest(contestId).then(contest => {

            this.setState({
                currentContestId: contest.id,
                contests: {
                    [contest.id]: contest
                }
            });
        });
    };


    currentContest(){
        return this.state.contests[this.state.currentContestId]
    }

    pageHeader() {
        if(this.state.currentContestId){
            return this.currentContest().contestName;
        }

        return 'Naming Contests';

    }

    currentContent() {

        if (this.state.currentContestId) {
            return <Contest {...this.currentContest()} />;
        }

        return <ContestList
            onContestClick={this.fetchContest}
            contests={this.state.contests} />;
    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.pageHeader()} />
                {this.currentContent()}
            </h2>
        );
    }

};

export default App;


index.js


1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDOM from 'react-dom';

import App from './components/App';

ReactDOM.render(
<App initialData={window.initialData} />,
document.getElementById('root')
);


serverRender.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import ReactDOMServer from 'react-dom/server';

import App from './src/components/App';

import config from './config';
import axios from 'axios';

const serverRender = () =>
axios.get(`${config.serverUrl}/api/contests`)
.then(resp => {
return {
initialMarkup: ReactDOMServer.renderToString(
<App initialData={resp.data} />
),
initialData: resp.data
};
});

export default serverRender;


028 Server-side routing for a contest



server.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import config from './config';
import apiRouter from './api';
import sassMiddleware from 'node-sass-middleware';
import path from 'path';
import serverRender from './serverRender';

import express from 'express';
const server = express();

server.use(sassMiddleware({
src: path.join(**dirname, 'sass'),
dest: path.join(**dirname, 'public')
}));

server.set('view engine', 'ejs');

server.get(['/', '/contest/:contestId'], (req, res) => {  
 serverRender(req.params.contestId)
.then(( {initialMarkup, initialData}) => {
res.render('index', {
initialMarkup,
initialData
});
})
.catch(console.error);
});

server.get('/about.html', (req, res) => {
res.send('The about page');
});

server.use('/api', apiRouter);
server.use(express.static('public'));

server.listen(config.port, config.host, () => {
console.info('Express listening on port ', config.port);
});


serverRender.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react';
import ReactDOMServer from 'react-dom/server';

import App from './src/components/App';

import config from './config';
import axios from 'axios';

const getApiUrl = contestId => {
  
 console.log('contestId1');
console.log(contestId);
  
 if (contestId) {
return `${config.serverUrl}/api/contests/${contestId}`;
}
return `${config.serverUrl}/api/contests`;
}

const getInitialData = (contestId, apiData) => {  
 if (contestId){
return {
currentContestId: apiData.contest.id,
contests: {
[apiData.contest.id]: apiData.contest
}
};
}
  
 return {
contests: apiData.contests
}
};

const serverRender = (contestId) =>
axios.get(getApiUrl(contestId))
.then(resp => {

            const initialData = getInitialData(contestId, resp.data);

            return {
                initialMarkup: ReactDOMServer.renderToString(
                <App initialData={initialData} />
                ),
                initialData
            };
        });

export default serverRender;


029 Navigating to a list of contests


api.js


1
2
3
4
5
6
7
8
9
10
11
import axios from 'axios';

export const fetchContest = contestId => {
return axios.get(`/api/contests/${contestId}`)
.then( resp => resp.data.contest )
};

export const fetchContestList = () => {
return axios.get(`/api/contests`)
.then( resp => resp.data.contests )
};


Contest.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component} from 'react';
import PropTypes from 'prop-types';

class Contest extends React.Component {
  
 render() {
  
 return (
<div className="Contest">
<div className="contest-description">
{this.props.description}
</div>
<div className="home-link link"
                    onClick={this.props.contestListClick}
                    >
Contest List
</div>
</div>
);
}
}

Contest.propTypes = {
description: PropTypes.string.isRequired,
contestListClick: PropTypes.func.isRequired
}

export default Contest;


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';
import Contest from './Contest';
import \* as api from '../api';
import PropTypes from 'prop-types';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

class App extends React.Component {
  
 constructor(props){
  
 super(props);
  
 this.fetchContest = this.fetchContest.bind(this);
this.fetchContestList = this.fetchContestList.bind(this);
this.currentContent = this.currentContent.bind(this);
  
 // static propTypes = {
// initialData: PropTypes.object.isRequired
// }

        this.state = this.props.initialData;
    }

    componentDidMount() {

    }

    componentWillUnmount() {

    }

    fetchContest(contestId) {

        pushState(
            { currentContestId: contestId },
            `/contest/${contestId}`
        );


        api.fetchContest(contestId).then(contest => {

            this.setState({
                currentContestId: contest.id,
                contests: {
                    [contest.id]: contest
                }
            });
        });
    };


    fetchContestList() {

        pushState(
            { currentContestId: null },
            `/`
        );

        api.fetchContestList().then(contests => {

            this.setState({
                currentContestId: null,
                contests
            });
        });
    };


    currentContest(){
        return this.state.contests[this.state.currentContestId]
    }

    pageHeader() {
        if(this.state.currentContestId){
            return this.currentContest().contestName;
        }

        return 'Naming Contests';

    }

    currentContent() {

        if (this.state.currentContestId) {
            return <Contest
                contestListClick={this.fetchContestList}
                {...this.currentContest()} />;
        }

        return <ContestList
            onContestClick={this.fetchContest}
            contests={this.state.contests} />;
    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.pageHeader()} />
                {this.currentContent()}
            </h2>
        );
    }

};

export default App;


030 Handling the browser’s back button


App.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import React from 'react';
import axios from 'axios';
import Header from './Header';
import ContestList from './ContestList';
import Contest from './Contest';
import \* as api from '../api';
import PropTypes from 'prop-types';

const pushState = (obj, url) => window.history.pushState(obj, '', url);

const onPopState = handler => {
window.onpopstate = handler;
}

class App extends React.Component {
  
 constructor(props){
  
 super(props);
  
 this.fetchContest = this.fetchContest.bind(this);
this.fetchContestList = this.fetchContestList.bind(this);
this.currentContent = this.currentContent.bind(this);
  
 this.state = this.props.initialData;
}
  
 componentDidMount() {
onPopState((event) => {
this.setState({
currentContestId: (event.state || {}).currentContestId
})
});
}
  
 componentWillUnmount() {
onPopState(null);
}
  
 fetchContest(contestId) {
  
 pushState(
{ currentContestId: contestId },
`/contest/${contestId}`
);
  
  
 api.fetchContest(contestId).then(contest => {
  
 this.setState({
currentContestId: contest.id,
contests: {
[contest.id]: contest
}
});  
 });
};
  
  
 fetchContestList() {
  
 pushState(
{ currentContestId: null },
`/`
);
  
 api.fetchContestList().then(contests => {
  
 this.setState({
currentContestId: null,
contests
});  
 });
};
  
  
 currentContest(){
return this.state.contests[this.state.currentContestId]
}
  
 pageHeader() {
if(this.state.currentContestId){
return this.currentContest().contestName;
}
  
 return 'Naming Contests';

    }

    currentContent() {

        if (this.state.currentContestId) {
            return <Contest
                contestListClick={this.fetchContestList}
                {...this.currentContest()} />;
        }

        return <ContestList
            onContestClick={this.fetchContest}
            contests={this.state.contests} />;
    }

    render() {

        return (
            <h2 className="App">
                <Header message= {this.pageHeader()} />
                {this.currentContent()}
            </h2>
        );
    }

};

export default App;