Skip to content

Commit 81bcf03

Browse files
committed
Add support for universal fetching of data, fetch todos from server in TodoContainer
1 parent 5ebfc73 commit 81bcf03

File tree

9 files changed

+108
-23
lines changed

9 files changed

+108
-23
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# The main port to run the server on
33
APPLICATION_PORT=3000
44

5+
# The absolute URL to the application.
6+
APPLICATION_BASE_URL=http://localhost:3000
7+
58
# The primary asset host for JS, CSS, and images. This can be changed to a CDN
69
# URL. If serving assets locally, it should be the same as WEBPACK_OUTPUT_PATH
710
# below. This env variable only takes effect in production mode.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ component with the same name, for example:
7272
This project supports the awesome [Redux Devtools Extension](https://github.com/zalmoxisus/redux-devtools-extension). Install the
7373
Chrome or Firefox extension and it should just work.
7474

75+
## Server Side Rendering (SSR) and Asynchronous Data Fetching
76+
When rendering components on the server, you'll find that you may need to fetch
77+
some data before it can be rendered. The [server code](server/server.js) looks
78+
for a `fetchData` method on the container component and its child components,
79+
then executes all of them and only renders after the promises have all been
80+
resolved.
81+
82+
See the [TodosContainer](common/js/containers/Todos/index.js) for an example.
83+
7584
## Writing Tests
7685
The default testing framework is Mocha, though you can use whatever you want.
7786
Make sure you have it installed:

common/js/actions/todos.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1-
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from 'constants';
1+
import {
2+
ADD_TODO, REMOVE_TODO, TOGGLE_TODO,
3+
FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE
4+
} from 'constants/index';
5+
import { fetch } from 'lib/api';
26
import generateActionCreator from 'lib/generateActionCreator';
37

48
export const addTodo = generateActionCreator(ADD_TODO, 'text');
59
export const removeTodo = generateActionCreator(REMOVE_TODO, 'id');
610
export const toggleTodo = generateActionCreator(TOGGLE_TODO, 'id');
11+
12+
export const fetchTodosRequest = generateActionCreator(FETCH_TODOS_REQUEST);
13+
export const fetchTodosSuccess = generateActionCreator(FETCH_TODOS_SUCCESS, 'todos');
14+
export const fetchTodosFailure = generateActionCreator(FETCH_TODOS_FAILURE, 'error');
15+
16+
export const fetchTodos = () => {
17+
return (dispatch) => {
18+
dispatch(fetchTodosRequest());
19+
20+
return fetch('/api/todos', { method: 'GET' })
21+
.then(res => res.json())
22+
.then(todos => dispatch(fetchTodosSuccess(todos)))
23+
.catch(error => dispatch(fetchTodosFailure(error)));
24+
};
25+
};

common/js/constants/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export const ADD_TODO = 'ADD_TODO';
22
export const REMOVE_TODO = 'REMOVE_TODO';
33
export const TOGGLE_TODO = 'TOGGLE_TODO';
4+
5+
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
6+
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
7+
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';

common/js/containers/Todos/index.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { connect } from 'react-redux';
4-
import { addTodo, toggleTodo, removeTodo } from 'actions/todos';
4+
import { addTodo, toggleTodo, removeTodo, fetchTodos } from 'actions/todos';
55
import { Container, Header, Checkbox, List, Button, Form } from 'semantic-ui-react';
66
import classnames from 'classnames';
77
import css from './index.scss';
@@ -11,11 +11,19 @@ const cx = classnames.bind(css);
1111
class TodosContainer extends Component {
1212

1313
static propTypes = {
14-
todos: PropTypes.array.isRequired,
14+
todos: PropTypes.object.isRequired,
1515
dispatch: PropTypes.func.isRequired
1616
}
1717

18-
state = { todoText: null };
18+
state = { todoText: '' };
19+
20+
componentDidMount() {
21+
const { dispatch, todos: { isFetched } } = this.props;
22+
23+
if (!isFetched) {
24+
dispatch(fetchTodos());
25+
}
26+
}
1927

2028
submitTodo = (ev) => {
2129
const { dispatch } = this.props;
@@ -43,7 +51,7 @@ class TodosContainer extends Component {
4351
}
4452

4553
render() {
46-
const { todos } = this.props;
54+
const { todos: { todos } } = this.props;
4755
const { todoText } = this.state;
4856

4957
return (
@@ -63,7 +71,11 @@ class TodosContainer extends Component {
6371
/>
6472
</List.Content>
6573
<List.Content floated="left">
66-
<Checkbox type="checkbox" onChange={() => this.checkTodo(id)} />
74+
<Checkbox
75+
type="checkbox"
76+
checked={completed}
77+
onChange={() => this.checkTodo(id)}
78+
/>
6779
</List.Content>
6880
<List.Content className={cx(css.text, { [css.completed]: completed })}>
6981
{text}
@@ -76,7 +88,7 @@ class TodosContainer extends Component {
7688
<Form.Group>
7789
<Form.Input
7890
onChange={this.onTodoChange}
79-
value={todoText}
91+
value={todoText}
8092
type="text"
8193
placeholder="Add a todo..." />
8294
<Form.Button content="Add" icon="plus" />
@@ -87,8 +99,10 @@ class TodosContainer extends Component {
8799
}
88100
}
89101

90-
const mapStateToProps = (state) => ({
91-
todos: state.todos
92-
});
102+
TodosContainer.fetchData = ({ store }) => {
103+
return store.dispatch(fetchTodos());
104+
};
105+
106+
const mapStateToProps = ({ todos }) => ({ todos });
93107

94108
export default connect(mapStateToProps)(TodosContainer);

common/js/lib/api.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import isomorphicFetch from 'isomorphic-fetch';
2+
3+
const apiUrl = process.env.APPLICATION_BASE_URL || '';
4+
5+
// Overrides the fetch() method to add the base API url to the front.
6+
export const fetch = (url, ...rest) => isomorphicFetch(apiUrl + url, ...rest);

common/js/reducers/todos.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,50 @@
1-
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from 'constants';
1+
import {
2+
ADD_TODO, REMOVE_TODO, TOGGLE_TODO,
3+
FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE
4+
} from 'constants/index';
25

3-
const defaultState = [];
6+
const defaultState = {
7+
todos: [],
8+
isFetching: false,
9+
isFetched: false,
10+
error: null
11+
};
412

513
const todos = (state = defaultState, action) => {
614
switch (action.type) {
715
case ADD_TODO:
8-
return [...state, { id: Date.now(), text: action.text, completed: false }];
16+
return {
17+
...state,
18+
todos: [
19+
...state.todos, { id: Date.now(), text: action.text, completed: false }
20+
]
21+
};
922

1023
case REMOVE_TODO:
11-
return state.filter(todo => todo.id !== action.id);
24+
return {
25+
...state,
26+
todos: state.todos.filter(todo => todo.id !== action.id)
27+
};
1228

1329
case TOGGLE_TODO:
14-
return state.map(todo => {
15-
if (todo.id === action.id) {
16-
return { ...todo, completed: !todo.completed };
17-
}
18-
return todo;
19-
});
30+
return {
31+
...state,
32+
todos: state.todos.map(todo => {
33+
if (todo.id === action.id) {
34+
return { ...todo, completed: !todo.completed };
35+
}
36+
return todo;
37+
})
38+
};
39+
40+
case FETCH_TODOS_REQUEST:
41+
return { ...state, isFetching: true, isFetched: false };
42+
43+
case FETCH_TODOS_SUCCESS:
44+
return { ...state, todos: action.todos, isFetching: false, isFetched: true };
45+
46+
case FETCH_TODOS_FAILURE:
47+
return { ...state, isFetching: false, isFetched: false, error: action.error };
2048

2149
default:
2250
return state;

webpack/base.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import isomorphicConfig from './isomorphic';
55
import IsomorphicPlugin from 'webpack-isomorphic-tools/plugin';
66
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
77
import {
8-
ANALYZE, NODE_ENV, WEBPACK_OUTPUT_PATH, ASSET_URL, RESOLVE_PATHS
8+
ANALYZE, NODE_ENV, WEBPACK_OUTPUT_PATH, ASSET_URL, RESOLVE_PATHS,
9+
APPLICATION_BASE_URL
910
} from './constants';
1011

1112
const isDev = NODE_ENV === 'development';
@@ -16,7 +17,8 @@ const plugins = [
1617
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en|es/),
1718
new webpack.DefinePlugin({
1819
'process.env': {
19-
'NODE_ENV': JSON.stringify(NODE_ENV)
20+
'NODE_ENV': JSON.stringify(NODE_ENV),
21+
'APPLICATION_BASE_URL': JSON.stringify(APPLICATION_BASE_URL)
2022
}
2123
})
2224
];

webpack/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
export const {
66
NODE_ENV, DEV_SERVER_PORT, DEV_SERVER_HOSTNAME, WEBPACK_OUTPUT_PATH,
7-
ASSET_HOST, ASSET_PATH, ANALYZE
7+
ASSET_HOST, ASSET_PATH, ANALYZE, APPLICATION_BASE_URL
88
} = process.env;
99

1010
// The URL of the dev server including the hostname and port

0 commit comments

Comments
 (0)