Skip to content

Commit

Permalink
Feature: SSR + Code Splitting 🚀 (#43)
Browse files Browse the repository at this point in the history
* multi: Prepare for code splitting

chore: Add react-loadable to dependencies
chore: Move babel options to package.json so they can be retrieved more easily in webpack config
chore: Remove webpack patch (https://github.com/gaearon/react-hot-loader/tree/next\#no-patch-required)
chore: Update dependencies, move appropriate to devDependencies

* feat: split up todos into separate components

* chore: move templates to server folder instead

* feat: SSR + code splitting 🚀

- Add support for import() syntax in babel and use only on the server
(it is already supported on the client with webpack 2)
- Update webpack config for import() code splitting
- Fix an issue with static fetching for server data

* chore: update readme/comments
  • Loading branch information
calvinl authored Feb 3, 2018
1 parent cb183a4 commit a32b05f
Show file tree
Hide file tree
Showing 36 changed files with 2,776 additions and 1,104 deletions.
20 changes: 0 additions & 20 deletions .babelrc

This file was deleted.

6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ APPLICATION_PORT=3000
APPLICATION_BASE_URL=http://localhost:3000

# The output path of server and client files built by webpack and babel.
OUTPUT_PATH=/dist
PUBLIC_OUTPUT_PATH=/dist/public
OUTPUT_PATH=dist
PUBLIC_OUTPUT_PATH=dist/public

# Settings for webpack-dev-server.
DEV_SERVER_PORT=3001
DEV_SERVER_HOSTNAME=localhost
DEV_SERVER_HOST_URL=http://localhost:3001

# The primary asset path. Can be changed to be a CDN URL.
PUBLIC_ASSET_PATH=/assets
PUBLIC_ASSET_PATH=http://localhost:3001/assets/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ npm-debug.log*
# ignore built static files
/dist
/webpack-assets.json
/webpack-stats.json
/react-loadable.json
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ A universal React/Redux boilerplate with sensible defaults. Out of the box, this
boilerplate comes with:

- Server-side rendering with Express
- Code splitting with Webpack's dynamic `import()`s and [react-loadable](https://github.com/thejameskyle/react-loadable)
- Sane [webpack configurations](webpack/)
- JS hot reloading with `react-hot-loader` and `webpack-dev-server`
- CSS, SASS and `css-modules` support with hot reloading and no [flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) (`css-hot-loader`)
- Routing with `react-router-v4`
- Full production builds that do not rely on `babel`.
- Full production builds that do not rely on `babel-node`.

## Get started

Expand Down Expand Up @@ -161,13 +162,19 @@ Check the `.eslintignore` file for directories excluded from linting.

## Changing the public asset path

By default, assets are built into `/dist/public`. This path is then served by
express under the path `/assets`. This is the public asset path. In a production
By default, assets are built into `dist/public`. This path is then served by
express under the path `assets`. This is the public asset path. In a production
scenario, you may want your assets to be hosted on a CDN. To do so, just change
the `PUBLIC_ASSET_PATH` environment variant.

If you're using Heroku:
Example using Heroku, if serving via CDN:

```
heroku config:set PUBLIC_ASSET_PATH=https://my.cdn.com
```

Example using Heroku, if serving locally:

```
heroku config:set PUBLIC_ASSET_PATH=/assets
```
24 changes: 15 additions & 9 deletions client/index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import 'babel-polyfill';
import 'css/base/index.scss';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import configureStore from 'store';
import App from 'containers/App';
import Loadable from 'react-loadable';

import './styles';

// Hydrate the redux store from server state.
const initialState = window.__INITIAL_STATE__;
const history = createHistory();
const store = configureStore(initialState, history);

// Render the application
ReactDOM.hydrate(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
window.main = () => {
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
});
};
5 changes: 5 additions & 0 deletions client/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Include all Vendor CSS here
import 'semantic-ui-css/semantic.min.css';

// Base styles
import 'css/base/index.scss';
2 changes: 0 additions & 2 deletions client/vendor.js

This file was deleted.

35 changes: 35 additions & 0 deletions common/js/components/common/Loading/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';

const Loading = (props) => {
const { error, timedOut, pastDelay } = props;

console.log(props);

if (error) {
// When the loader has errored
return (
<div>Error!</div>
);
} else if (timedOut) {
// When the loader has taken longer than the timeout
return (
<div>Taking a long time...</div>
);
} else if (pastDelay) {
// When the loader has taken longer than the delay
return (
<div>Loading...</div>
);
}

return null;
};

Loading.propTypes = {
error: PropTypes.bool,
timedOut: PropTypes.bool,
pastDelay: PropTypes.bool
};

export default Loading;
1 change: 1 addition & 0 deletions common/js/components/common/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as ErrorPage } from './ErrorPage';
export { default as Footer } from './Footer';
export { default as Header } from './Header';
export { default as Loading } from './Loading';
export { default as RouteWithSubRoutes } from './RouteWithSubRoutes';
54 changes: 54 additions & 0 deletions common/js/components/todos/TodoForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form } from 'semantic-ui-react';
import classnames from 'classnames/bind';
import css from './index.scss';

class TodoForm extends Component {
static propTypes = {
onSubmit: PropTypes.func,
className: PropTypes.string
};

state = { todoText: '' };

submitTodo = ev => {
ev.preventDefault();

const { onSubmit } = this.props;
const { todoText } = this.state;

if (todoText && todoText !== '' && typeof onSubmit === 'function') {
onSubmit(todoText);
this.setState({ todoText: '' });
}
};

onTodoChange = ev => {
this.setState({ todoText: ev.target.value });
};

render() {
const { className } = this.props;
const { todoText } = this.state;

return (
<Form
className={classnames(css.todoForm, className)}
onSubmit={this.submitTodo}
>
<Form.Group>
<Form.Input
onChange={this.onTodoChange}
value={todoText}
type="text"
placeholder="Add a todo..."
/>
<Form.Button content="Add" icon="plus" />
</Form.Group>
</Form>
);
}
}

export default TodoForm;
Empty file.
42 changes: 42 additions & 0 deletions common/js/components/todos/TodoItem/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List, Checkbox, Button } from 'semantic-ui-react';
import classnames from 'classnames/bind';
import css from './index.scss';

const cx = classnames.bind(css);

const TodoItem = props => {
const { onRemove, onChange, todo: { id, completed, text } } = props;

return (
<List.Item className={classnames(css.todo, css.extra)}>
<List.Content floated="right">
<Button onClick={() => onRemove(id)} icon="remove" size="mini" />
</List.Content>
<List.Content floated="left">
<Checkbox
type="checkbox"
checked={completed}
onChange={() => onChange(id)}
/>
</List.Content>
<List.Content className={cx(css.text, { [css.completed]: completed })}>
{text}
</List.Content>
</List.Item>
);
};

TodoItem.propTypes = {
todo: PropTypes.object.isRequired,
onRemove: PropTypes.func,
onChange: PropTypes.func
};

TodoItem.defaultProps = {
onRemove: () => {},
onChange: () => {}
};

export default TodoItem;
20 changes: 20 additions & 0 deletions common/js/components/todos/TodoItem/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.todo {
&.extra {
padding: 10px;
}

.completeInput {}

.text {
&.completed {
text-decoration: line-through;
color: #ccc;
}
}

.delete {
cursor: pointer;
color: blue;
text-decoration: underline
}
}
32 changes: 32 additions & 0 deletions common/js/components/todos/TodoList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'semantic-ui-react';
import { TodoItem } from 'components/todos';
import classnames from 'classnames';
import css from './index.scss';

const TodoList = props => {
const { className, onChange, onRemove, todos: { todos } } = props;

return (
<List divided className={classnames(css.todos, className)}>
{todos.map((todo, idx) => (
<TodoItem
key={idx}
todo={todo}
onRemove={onRemove}
onChange={onChange}
/>
))}
</List>
);
};

TodoList.propTypes = {
todos: PropTypes.object.isRequired,
className: PropTypes.string,
onChange: PropTypes.func,
onRemove: PropTypes.func
};

export default TodoList;
6 changes: 6 additions & 0 deletions common/js/components/todos/TodoList/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.todos {
width: 300px;
margin: 0;
padding: 0;
list-style: none;
}
3 changes: 3 additions & 0 deletions common/js/components/todos/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as TodoForm } from './TodoForm';
export { default as TodoItem } from './TodoItem';
export { default as TodoList } from './TodoList';
Loading

0 comments on commit a32b05f

Please sign in to comment.