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;