React Semantic UI boilerplate

init app

npx comes with npm 5.2+ and higher

1
2
3
# npx create-react-app react-semantic-ui-boilerplate
# cd react-semantic-ui-boilerplate
# yarn start

Open http://localhost:3000/ on browser

Set up Semantic UI

1
# yarn add semantic-ui-react semantic-ui-css font-awesome

Edit App.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 './App.css';

import 'semantic-ui-css/semantic.min.css';
import 'font-awesome/css/font-awesome.css';
import {Button, Segment} from "semantic-ui-react";

class App extends Component {
render() {
return (
<Segment padded={'very'}>
<Button primary>Semantic UI is ready</Button>
<Button secondary>Semantic UI is ready</Button>
</Segment>
);
}
}

export default App;

Refresh http://localhost:3000/ to make sure semantic ui works

Set up redux

1
# yarn add react-redux reduxsauce seamless-immutable

Edit index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom';
import './styles/index.css';
import Root from './containers/Root';
import registerServiceWorker from './registerServiceWorker';
import configureStore from "./redux/configureStore";

const initialState = {};
const store = configureStore(initialState);

ReactDOM.render(<Root store={store}/>, document.getElementById('root'));
registerServiceWorker();

Create src\containers\Root.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import {Provider} from 'react-redux';
import App from './App';

export default class Root extends React.Component {

render() {
return (
<Provider store={this.props.store}>
<App>
</App>
</Provider>
);
}
}

Create src\redux\configureStore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {applyMiddleware, createStore, compose} from 'redux';

import reducers from '../reducers';

export default function configureStore(initialState) {

const middleware = [];
const enhancers = [];

// add middleware here

enhancers.push(applyMiddleware(...middleware));

const store = createStore(reducers, initialState, compose(...enhancers));

return store

}

Create src\reducers\index.js

1
2
3
4
5
6
import {combineReducers} from 'redux';

export default combineReducers({
user: require('../reducers/UserRedux').reducer,
});

And reducer src\reducers\UserRedux.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 {createReducer, createActions} from 'reduxsauce'
import Immutable from 'seamless-immutable'

const {Types, Creators} = createActions({
getUser: null,
getUserSuccess: ['data'],
getUserFail: ['error']
}, {prefix: 'USER'});

export const UserTypes = Types;
export default Creators


export const INITIAL_STATE = Immutable({
loading: false,
users: []
});

const getUser = (state) => {
return state.merge({loading: true})
};

const getUserSuccess = (state, {data}) => {
return state.merge({loading: false, users: data})
};

const getUserFail = (state, {error}) => {
return state.merge({loading: false, errors: error})
};

export const reducer = createReducer(INITIAL_STATE, {
[Types.GET_USER]: getUser,
[Types.GET_USER_SUCCESS]: getUserSuccess,
[Types.GET_USER_FAIL]: getUserFail,
});

Move App.js to src\containers\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, {Component} from 'react';
import '../styles/App.css';

import 'semantic-ui-css/semantic.min.css';
import 'font-awesome/css/font-awesome.css';
import {Button, Segment} from "semantic-ui-react";

import {connect} from 'react-redux';
import UserActions from '../reducers/UserRedux'

class App extends Component {
render() {
return (
<Segment padded={'very'}>
<div>Loading: {this.props.loading ? "Yes" : "No"}</div>
<Button onClick={() => {
this.props.getUser()
}} primary>Get User</Button>
</Segment>
);
}
}


const mapStateToProps = (state, ownProps) => {
return {
loading: state.user.loading,
};
};

const mapDispatchToProps = (dispatch) => {
return {
getUser: () => dispatch(UserActions.getUser())
}
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

Refresh http://localhost:3000/, loading: No initial state works

Click Get User, loading: Yes, state user.loading changed

Set up redux-saga

1
# yarn add redux-saga

Edit index.js

1
2
3
4
5
...
import rootSagas from './sagas'
...
const store = configureStore(initialState, rootSagas);
...

Edit src\redux\configureStore.js

1
2
3
4
5
6
7
8
9
10
11
...
import createSagaMiddleware from 'redux-saga'
export default function configureStore(initialState, rootSagas) {
...
// add middleware here
const sagaMiddleware = createSagaMiddleware();
middleware.push(sagaMiddleware);
...
sagaMiddleware.run(rootSagas);
return store
}

Create src\sagas\index.js

1
2
3
4
5
6
7
8
9
10
import {takeLatest, all} from 'redux-saga/effects'
import {UserTypes} from "../reducers/UserRedux";
import {getUser} from "./UserSaga";


export default function* root() {
yield all([
takeLatest(UserTypes.GET_USER, getUser),
])
}

And src\sagas\UserSaga.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {put, call} from 'redux-saga/effects'
import {delay} from 'redux-saga'
import UserActions from '../reducers/UserRedux'

export function* getUser(action) {

yield call(delay, 1000);

// do async task, call api,...
const api_response = [{
id: 1,
name: 'Tony'
}];

yield put(UserActions.getUserSuccess(api_response))
}

Edit src\containers\App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
render() {
return (
<Segment padded={'very'}>
<div> loading: {this.props.loading ? "Yes" : "No"}</div>
<div> users: {JSON.stringify(this.props.users)}</div>
<Button onClick={() => {
this.props.getUser()
}} primary>Get User</Button>
</Segment>
);
}
...
const mapStateToProps = (state, ownProps) => {
return {
loading: state.user.loading,
users: state.user.users,
};
};
...

Refresh http://localhost:3000/

state user.users == [], initial state works

Click Get User, wait .5 second, got user object, saga works!

Set up react-router

1
# yarn add react-router react-router-dom react-router-redux@v5.0.0-alpha.9 history

Edit index.js

1
2
3
4
5
6
7
8
...
import createHistory from 'history/createBrowserHistory';
...
const history = createHistory();
const store = configureStore(initialState,rootSagas,history);

ReactDOM.render(<Root store={store} history={history}/>, document.getElementById('root'));
...

Edit src\redux\configureStore.js

1
2
3
4
5
6
7
8
9
10
11
...
import {routerMiddleware} from 'react-router-redux';
...
export default function configureStore(initialState, rootSagas, history) {
...
const reduxRouterMiddleware = routerMiddleware(history);
middleware.push(reduxRouterMiddleware);

enhancers.push(applyMiddleware(...middleware));
...
}

Edit src\containers\Root.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
import {ConnectedRouter} from 'react-router-redux';
import routes from '../routes';
...
render() {
return (
<Provider store={this.props.store}>
<App>
<ConnectedRouter history={this.props.history}>
{routes}
</ConnectedRouter>
</App>
</Provider>
);
}
...

Edit src\containers\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
import React, {Component} from 'react';
import '../styles/App.css';

import 'semantic-ui-css/semantic.min.css';
import 'font-awesome/css/font-awesome.css';

import {connect} from 'react-redux';

class App extends Component {
render() {
return (
<div className='App'>
{this.props.children}
</div>
);
}
}


const mapStateToProps = (state) => {
return {};
};

const mapDispatchToProps = (dispatch) => {
return {
}
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

Create src\routes.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import {Route, Switch} from 'react-router';
import {
HomeView,
NotFoundView,
AboutView
} from "./containers";

export default (
<Switch>
<Route exact path="/" component={HomeView}/>
<Route path="/about" component={AboutView}/>
<Route path="*" component={NotFoundView}/>
</Switch>
);

Create src\containers\index.js

1
2
3
export {default as HomeView} from './HomeView'
export {default as NotFoundView} from './NotFoundView'
export {default as AboutView} from './AboutView'

Create src\containers\HomeView.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
import React from 'react';
import {connect} from "react-redux";
import MainLayout from "../components/MainLayout";
import {Segment} from "semantic-ui-react";

class HomeView extends React.Component {
render() {
return (
<MainLayout>
<Segment>Home</Segment>
</MainLayout>
)
}
}

const mapStateToProps = (state) => {
return {}
};

const mapDispatchToProps = (dispatch) => {
return {}
};

export default connect(mapStateToProps, mapDispatchToProps)(HomeView);

Create src\containers\AboutView.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import MainLayout from "../components/MainLayout";
import {Segment} from "semantic-ui-react";

export default class AboutView extends React.Component {
render() {
return (
<MainLayout>
<Segment>About us</Segment>
</MainLayout>
);
}
}

Create src\containers\NotFoundView.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import EmptyLayout from "../components/EmptyLayout";

export default class AboutView extends React.Component {
render() {
return (
<EmptyLayout>
<h1>404 Not found</h1>
</EmptyLayout>
);
}
}

And src\components\MainLayout.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
import React, {Component} from 'react';
import {Container, Menu} from "semantic-ui-react";
import {push} from 'react-router-redux';
import {connect} from "react-redux";

class MainLayout extends Component {
render() {
return (
<div style={{flex: 1}}>
<Menu style={{flex: 1}} fixed='top'>
<Container>
<Menu.Item header>
Hello world
</Menu.Item>
<Menu.Item onClick={() => {
this.props.dispatch(push('/'))
}} as='a'>Home</Menu.Item>
<Menu.Item onClick={() => {
this.props.dispatch(push('/about'))
}} as='a'>About</Menu.Item>
</Container>
</Menu>
<Container style={{marginTop: '60px'}}>
{this.props.children}
</Container>
</div>
)
}
}

const mapStateToProps = (state) => {
return {}
};

const mapDispatchToProps = (dispatch) => {
return {
dispatch
}
};


export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);

And src\components\EmptyLayout.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, {Component} from 'react';
import {Container, Segment} from "semantic-ui-react";

class EmptyLayout extends Component {
render() {
return (
<Container>
<Segment style={{flex: 1}}>
{this.props.children}
</Segment>
</Container>

)
}
}

export default EmptyLayout;

It’s time to test http://localhost:3000/

HomeView

AboutView

fake view

Make sure top menu home, about and browser back button work

Set up API client

1
yarn add apisauce

Create src\services\Api.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
import apisauce from 'apisauce'

const create = (baseURL = 'https://jsonplaceholder.typicode.com') => {

const api = apisauce.create({
// base URL is read from the "constructor"
baseURL: baseURL,
// here are some default headers
headers: {
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
// 10 second timeout...
timeout: 10000
});

const getUser = () => api.get('/users');

return {
getUser
}
};

export default {
create
}

Edit src\sagas\index.js

1
2
3
4
5
6
7
8
9
...
import API from '../services/Api'
export const api = API.create('https://jsonplaceholder.typicode.com');
export default function* root() {
yield all([
takeLatest(UserTypes.GET_USER, getUser, api),
])
}
...

Edit src\sagas\UserSaga.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {put, call} from 'redux-saga/effects'
import UserActions from '../reducers/UserRedux'
import {delay} from "redux-saga";


export function* getUser(api, action) {

const response = yield call(api.getUser);

// more delay to see loading indicator
yield call(delay, 1000);

if (response.ok) {
yield put(UserActions.getUserSuccess(response.data))
} else {
yield put(UserActions.getUserFail(response.body))
}

}

Edit src\containers\HomeView.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
import React from 'react';
import {connect} from "react-redux";
import MainLayout from "../components/MainLayout";
import {Container, Header, Segment, Table} from "semantic-ui-react";
import UserActions from "../reducers/UserRedux";

class HomeView extends React.Component {
componentDidMount() {
this.props.getUser()
}

_render_row = (record) => {
return (<Table.Row key={record.id}>
<Table.Cell collapsing>
{record.id}
</Table.Cell>
<Table.Cell>
{record.name}
</Table.Cell>
<Table.Cell>{record.phone}</Table.Cell>
<Table.Cell>{record.website}</Table.Cell>
</Table.Row>)
};

render() {
return (
<MainLayout>
<Container textAlign={'left'}>
<Header>Home</Header>
<Segment style={{minHeight:300}} basic loading={this.props.loading}>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>ID</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Phone</Table.HeaderCell>
<Table.HeaderCell>Website</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.props.users.map((record) => {
return this._render_row(record)
})}
</Table.Body>
</Table>
</Segment>
</Container>
</MainLayout>
)
}
}

const mapStateToProps = (state) => {
return {
users: state.user.users,
loading: state.user.loading,
}
};

const mapDispatchToProps = (dispatch) => {
return {
getUser: () => dispatch(UserActions.getUser())
}
};

export default connect(mapStateToProps, mapDispatchToProps)(HomeView);

Refresh http://localhost:3000/, we got home page

Log and debug with Reactotron

1
# yarn add --dev reactotron-apisauce reactotron-react-js reactotron-redux reactotron-redux-saga

Edit src\redux\configureStore.js

1
2
3
4
5
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}

Create src\configureStore.dev.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
import {applyMiddleware, compose} from 'redux';
import {routerMiddleware} from 'react-router-redux';
import createSagaMiddleware from 'redux-saga'
import reducers from '../reducers';
import Reactotron from 'reactotron-react-js'
import {api} from '../sagas'
import '../config/Reactotron'

api.addMonitor(Reactotron.apisauce);

export default function configureStore(initialState, rootSagas, history) {

const middleware = [];
const enhancers = [];

// add middleware here
const sagaMonitor = Reactotron.createSagaMonitor();
const sagaMiddleware = createSagaMiddleware({sagaMonitor});
middleware.push(sagaMiddleware);

const reduxRouterMiddleware = routerMiddleware(history);
middleware.push(reduxRouterMiddleware);

enhancers.push(applyMiddleware(...middleware));

const store = Reactotron.createStore(reducers, initialState, compose(...enhancers));

sagaMiddleware.run(rootSagas);

return store

}

Create src\configureStore.prod.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 {applyMiddleware, createStore, compose} from 'redux';
import createSagaMiddleware from 'redux-saga'
import {routerMiddleware} from 'react-router-redux';

import reducers from '../reducers';


export default function configureStore(initialState, rootSagas, history) {

const middleware = [];
const enhancers = [];

// add middleware here
const sagaMiddleware = createSagaMiddleware();
middleware.push(sagaMiddleware);

const reduxRouterMiddleware = routerMiddleware(history);
middleware.push(reduxRouterMiddleware);

enhancers.push(applyMiddleware(...middleware));

const store = createStore(reducers, initialState, compose(...enhancers));

sagaMiddleware.run(rootSagas);

return store

}

Edit src\services\Api.js

1
2
3
4
5
6
7
8
9
10
...
const create = (baseURL = 'https://jsonplaceholder.typicode.com') => {
...
return {
addMonitor:api.addMonitor,
getUser
}
...
}
...

Create src\config\Reactotron.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Reactotron from 'reactotron-react-js'
import {reactotronRedux} from 'reactotron-redux'
import sagaPlugin from 'reactotron-redux-saga'
import Immutable from 'seamless-immutable'
import apisaucePlugin from 'reactotron-apisauce'

import {trackGlobalErrors} from 'reactotron-react-js'

Reactotron
.configure({name: 'React boilerplate'}) // we can use plugins here -- more on this later
.use(apisaucePlugin())
.use(trackGlobalErrors({offline: false}))
.use(reactotronRedux({onRestore: Immutable}))
.use(sagaPlugin())
.connect(); // let's connect!

Reactotron.clear();

Download and open Reactotron.app (https://github.com/infinitered/reactotron)

Reload react app and see how Reactotron log action, saga, api response

View state user

Finish!

Github: https://github.com/tamhv/react-semantic-ui-boilerplate