Skip to content

Instantly share code, notes, and snippets.

@kcak11
Last active January 26, 2020 14:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kcak11/da0abfadee17499aac6d5d64ae3371e3 to your computer and use it in GitHub Desktop.
Save kcak11/da0abfadee17499aac6d5d64ae3371e3 to your computer and use it in GitHub Desktop.
React & Redux Apps Template

React & Redux Apps Template

<!-- Our App will reside here -->
<div id="app"></div>
<!-- Portal Container -->
<div id="portalContainer"></div>
<!--
© 2018 https://kcak11.com / https://ashishkumarkc.com
-->

React & Redux Apps Template

This template imports the CDNs for the following:

  • React
  • ReactDOM
  • Redux
  • React-Redux
  • Redux-Thunk
  • React-Router-DOM
  • Redux-Undo

It also provides a basic scaffolding for the App

The JavaScript preprocessor is set as Babel

The template has implementation examples for:

  • React
  • Redux
  • combineReducers
  • Redux-Thunk
  • React-Router-DOM
  • withRouter
  • compose multiple middlewares
  • Redux-Undo
  • Error Boundaries in React
  • Context creation in React
  • Function Component
  • PureComponent
  • createRef
  • forwardRef
  • callback ref
  • ReactDOM.findDOMNode
  • Accessing DOM element directly using "ref"
  • Data Binding (UI->State->UI)
  • render props
  • Fragments
  • Portals
  • any many more . . .

A Pen by K.C.Ashish Kumar on CodePen.

License.

/*
© 2018 https://kcak11.com / https://ashishkumarkc.com
*/
let _module = function() {
"use strict";
/* Start Dependency Imports */
const { Fragment } = React;
const {
createStore,
bindActionCreators,
combineReducers,
applyMiddleware,
compose
} = Redux;
const { Provider, connect } = ReactRedux;
const thunk = ReduxThunk.default;
const Router = ReactRouterDOM.BrowserRouter;
const { Route, Link, Switch, withRouter } = ReactRouterDOM;
const undoable = ReduxUndo.default;
const urActionCreators = ReduxUndo.ActionCreators;
const excludeAction = ReduxUndo.excludeAction;
const logMsg = CommonUtils.logger;
const getUniqueId = CommonUtils.uniqueToken;
/*End Dependency Imports*/
/* Creating a context for the App - optional */
const AppContext = React.createContext();
/* An example of PureComponent created using fat-arrow function instead of class */
/* AppContext.Consumer is for passing in the context, it can be ommitted if context is not needed */
const FunctionComponent = props => {
logMsg("render - FunctionComponent");
return (
<AppContext.Consumer>
{contextData => (
<div
data-locale={contextData.locale}
data-developer={contextData.dev}
>
Function Component: {props.attr1} {props.attr2} (passed via
props)
</div>
)}
</AppContext.Consumer>
);
};
/* Component that uses Ref Forwarding Technique */
const MyComponent = React.forwardRef((props, ref) => {
logMsg("render - MyComponent");
return (
<div ref={ref} data-info="mybutton">
An Inner Component that has forwarded ref
</div>
);
});
/* Component that uses ref callback technique */
const MyOtherComponent = props => {
logMsg("render - MyOtherComponent");
return <div ref={props.myOtherComponentRef} />;
};
/* Start demonstrating render props */
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
val1: 2,
val2: 4
};
}
render() {
logMsg("render - Parent");
return <div data-type="Parent">{this.props.render(this.state)}</div>;
}
}
class Child extends React.Component {
render() {
logMsg("render - Child");
return (
<Fragment>
<span style={{ display: "block" }}>
val1: {this.props.componentInfo.val1}
</span>
<span style={{ display: "block" }}>
val2: {this.props.componentInfo.val2}
</span>
</Fragment>
);
}
}
/* End demonstrating render props */
/* Start demonstrating findDOMNode Example */
class DomRefExample extends React.Component {
componentDidMount() {
logMsg("componentDidMount - DomRefExample");
this.refs.sampleText1.style.color = "#00f";
ReactDOM.findDOMNode(this.refs.sampleText2).querySelector(".sampleText2_class").style.color = "#f00";
}
render() {
logMsg("render - DomRefExample");
return (
<div>
<span ref="sampleText1">Text color changed by direct ref</span>
<br />
<span ref="sampleText2">
<span className="sampleText2_class">
Text color changed by findDOMNode
</span>
</span>
</div>
);
}
}
/* End demonstrating findDOMNode Example */
/* Start DataBinding Example */
class DataBindingExample extends React.Component {
constructor(props) {
super(props);
this.state = { textInput: "" };
this.updateTextInputValue = this.updateTextInputValue.bind(this);
logMsg("constructor - DataBindingExample");
}
updateTextInputValue(evt) {
this.setState({ textInput: evt.target.value });
}
render() {
logMsg("render - DataBindingExample");
return (
<div>
<input
style={{
width: "300px",
border: "2px solid #000",
padding: "5px"
}}
placeholder="type something in this box . . ."
type="text"
value={this.state.textInput}
onChange={this.updateTextInputValue}
/>
<span
style={{
color: "#f00",
position: "relative",
left: "20px",
top: "-7px"
}}
>
{this.state.textInput}
</span>
</div>
);
}
}
/* End DataBinding Example */
/* Start demonstrating Portals */
class MyPortalComponent extends React.Component {
render() {
return ReactDOM.createPortal(
<Fragment>
<br />
<div>
This text is rendered via MyPortalComponent using the React
Portals technique
</div>
</Fragment>,
document.getElementById("portalContainer")
);
}
}
/* End demonstrating Portals */
/* The main App */
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.myComponentRef = React.createRef();
logMsg("constructor - App");
}
componentDidMount() {
logMsg("componentDidMount - App");
this.setState((state, props) => {
return {
info: "Hello World - React App !!"
};
});
this.fetchPlayListData();
}
componentDidUpdate() {
logMsg("componentDidUpdate - App");
// using the ref and updating the UI component
if (this.myComponentRef.current) {
this.myComponentRef.current.style.color = "#f00";
}
if (this.otherComponentRef) {
this.otherComponentRef.innerHTML = "Ref obtained via callback ref";
}
}
componentWillUnmount() {
logMsg("componentWillUnmount - App");
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { errorOccurred: true };
}
componentDidCatch(error, info) {
logMsg("componentDidCatch - App\n", error, "\n\n", info);
}
renderDataLoadingUI() {
return <h4>Please wait, loading data . . .</h4>;
}
renderUserDataUI() {
let dataRows = [];
for (let i = 0; i < this.props.userList.present.length; i++) {
dataRows.push(
<UserDataRow
key={"user_" + getUniqueId()}
index={i}
removeUser={this.props.removeUser}
data={this.props.userList.present[i]}
userList={this.props.userList}
/>
);
}
return (
<Fragment>
<AddUser addUser={this.props.addUser} /> <br />
<table>
<thead>
<tr>
<th>FirstName</th>
<th>LastName</th>
<th className="center_align">Delete User</th>
</tr>
</thead>
<tbody>{dataRows}</tbody>
</table>
<hr />
</Fragment>
);
}
render() {
logMsg("render - App");
if (this.state.errorOccurred) {
return <div>Oops something failed :-(</div>;
}
return (
<AppContext.Provider value={{ locale: "en_US", dev: "kcak11" }}>
<Header fixed={true} />
<div>
{this.state.info} (rendered via&nbsp;
<b>&#123;this.state.info&#125;</b>) <br />
<MyComponent ref={this.myComponentRef} />
<MyOtherComponent
myOtherComponentRef={elem =>
(this.otherComponentRef = elem)
}
/>
<MyPortalComponent />
<FunctionComponent attr1="Value1" attr2="Value2" />
<hr />
<DomRefExample />
<hr />
<DataBindingExample />
<hr />
<h4>Demonstrate render props:</h4>
<Parent render={data => <Child componentInfo={data} />} />
<hr />Random Number (via Redux):
<a href="#" onClick={this.doUpdateRandomNum.bind(this)}>
{this.props.randomNum} [click to refresh]
</a>
<hr />
<SubComponent today={new Date()}>Test String</SubComponent>
<hr />
{_.get(this.state, 'playlist.data.length', 0) > 0 && (
<h3>PlayList Data (fetched via AJAX)</h3>
)}
{_.get(this.state, 'playlist.data', []).map((item, i) => (
<div
key={"playlistitem_" + getUniqueId()}
className="playListRow"
>
{item.name}
</div>
))}
<hr />
<ReactRouterExample />
<hr />
<h3>User Data (state maintained using Redux)</h3>
{!this.props.userDataLoaded
? this.renderDataLoadingUI()
: this.renderUserDataUI()}
<button
onClick={this.doUnmountApp.bind(this)}
className="unmountAppBtn"
type="button"
>
Unmount App
</button>
</div>
</AppContext.Provider>
);
}
doUpdateRandomNum(e) {
e.preventDefault();
this.props.updateRandomNum(Math.random());
}
doUnmountApp() {
let decision = confirm(
"Are you sure that you want to unmount the current App ?\nYou may have to refresh your browser to re-run the app."
);
if (decision) {
ReactDOM.unmountComponentAtNode(document.getElementById("app"));
let msgElem =
document.querySelector("#unmountMsgElem") ||
document.createElement("div");
msgElem.id = "unmountMsgElem";
msgElem.innerHTML =
"<h2>App Unmounted Successfully !!</h2><h4>Please <span id='remountLink' style='color:#f00;cursor:pointer;'>click here to ReMount the App</span> or refresh your browser.</h4>";
document.querySelector("body").appendChild(msgElem);
document.querySelector("#remountLink").addEventListener(
"click",
function(e) {
e.preventDefault();
document.querySelector("#unmountMsgElem").innerHTML = "";
new AppBootstrap(_module).run();
},
false
);
}
}
fetchPlayListData() {
let self = this;
$api.call({
method: "get",
url: "https://services-kcak11.firebaseio.com/playlist.json",
success: response => {
let obj;
try {
obj = JSON.parse(response);
self.setState({ playlist: obj });
} catch (exjs) {}
}
});
}
}
/* Header Component */
class Header extends React.Component {
render() {
logMsg("render - Header");
let headerClass = "pageHeader";
if (Boolean(this.props.fixed)) {
headerClass += " fixed";
}
return (
<Fragment>
<div className={headerClass}>
<h1 className="pageTitle">
React & Redux App (powered by{" "}
<a href="https://www.kcak11.com" target="_blank">
Ashish's Web
</a>)
</h1>
<h5 className="subTitle">
Watch the browser console for lifecycle methods
</h5>
</div>
{Boolean(this.props.fixed) && (
<div style={{ height: "100px" }} />
)}
</Fragment>
);
}
}
/* SubComponent class */
class SubComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { date: props.today.toString() };
logMsg("constructor - SubComponent");
}
render() {
logMsg("render - SubComponent");
return (
<div style={{ color: "#f00" }}>
- - Sub Component Rendered @ {this.state.date} <br />
- - - Child Content: {this.props.children}
<br />
<input type="checkbox" id="test_checkbox" />
<label htmlFor="test_checkbox" style={{ color: "#000" }}>
label linked using htmlFor
</label>
</div>
);
}
}
/* Component to demonstrate React Router usage */
class ReactRouterExample extends React.Component {
render() {
logMsg("render - ReactRouterExample");
return (
<Fragment>
<h2>React Router Example</h2>
<div>
<ul>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/about/12345">
About 12345 (invoking Router link with data)
</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
<div>
<Switch>
<Route path="/home" component={Home} />
<Route exact path="/about" component={About} />
<Route path="/about/:dataId" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</div>
</div>
<h6>
Note: The router example here is implemented only for client
side routing. The server side routes do not exist, hence you
might landup on a 404(Page not found) scenario if you refresh
the browser after clicking on the route links above.
</h6>
</Fragment>
);
}
}
/* A component that will render the Back link when navigating */
class Back extends React.Component {
doBack(e) {
e.preventDefault();
history.go(-1);
}
render() {
logMsg("render - Back");
return (
<a href="#" onClick={this.doBack.bind(this)}>
Back
</a>
);
}
}
/* Home Component */
class Home extends React.Component {
render() {
logMsg("render - Home");
return (
<div>
<h3>Home...</h3>
<Back />
</div>
);
}
componentWillUnmount() {
logMsg("componentWillUnmount - Home");
}
}
/* About Component */
class About extends React.Component {
render() {
logMsg("render - About");
return (
<div>
<h3>About... {this.props.match.params.dataId}</h3>
<Back />
</div>
);
}
componentWillUnmount() {
logMsg("componentWillUnmount - About");
}
}
/* Contact Component */
class Contact extends React.Component {
render() {
logMsg("render - Contact");
return (
<div>
<h3>Contact...</h3>
<Back />
</div>
);
}
componentWillUnmount() {
logMsg("componentWillUnmount - Contact");
}
}
/* AddUser Component - renders the form for information input and the undo/redo buttons */
class AddUser extends React.Component {
constructor(props) {
super(props);
}
handleAddUserSubmission(e) {
e.preventDefault();
let refs = this.refs;
let firstName = refs.firstName.value;
let lastName = refs.lastName.value;
if (!firstName.trim() || !lastName.trim()) {
alert("<First Name> and <Last Name> fields are required.");
return;
}
// Trigger action
this.props.addUser(firstName, lastName);
// Reset form
refs.firstName.value = "";
refs.lastName.value = "";
refs.firstName.focus();
}
doUndo() {
let stateObj = store.getState();
let undoAvailable = _.get(stateObj, 'userList.past.length', 0) > 1;
if (undoAvailable) {
store.dispatch(urActionCreators.undo());
}
}
doRedo() {
let stateObj = store.getState();
let redoAvailable = _.get(stateObj, 'userList.future.length' ,0) > 0;
if (redoAvailable) {
store.dispatch(urActionCreators.redo());
}
}
render() {
logMsg("render - AddUser");
let stateObj = store.getState();
let undoAvailable = _.get(stateObj, 'userList.past.length', 0) > 1;
let redoAvailable = _.get(stateObj, 'userList.future.length' ,0) > 0;
return (
<form onSubmit={this.handleAddUserSubmission.bind(this)}>
<input
type="text"
placeholder="First Name"
ref="firstName"
name="firstName"
/>
<input
type="text"
placeholder="Last Name"
ref="lastName"
name="lastName"
/>
<button type="submit" className="addUserBtn">
Add User
</button>
<button
type="button"
className="undoBtn"
onClick={this.doUndo.bind(this)}
disabled={!undoAvailable}
>
Undo
</button>
<button
type="button"
className="redoBtn"
onClick={this.doRedo.bind(this)}
disabled={!redoAvailable}
>
Redo
</button>
</form>
);
}
}
/* UserDataRow component to display the list of users */
class UserDataRow extends React.Component {
static contextType = AppContext;
constructor(props) {
super(props);
}
handleDeleteUserClick() {
let userList = this.props.userList.present;
if (userList.length === 1) {
alert("Cannot delete all users, atleast one is required");
return;
}
let index = this.props.index;
this.props.removeUser(index);
}
render() {
logMsg("render - UserDataRow");
return (
<tr
data-locale={this.context.locale}
data-developer={this.context.dev}
>
<td>{this.props.data.firstName}</td>
<td>{this.props.data.lastName}</td>
<td className="center_align">
<button
type="button"
className="delBtn"
onClick={this.handleDeleteUserClick.bind(this)}
>
&times; Delete User
</button>
</td>
</tr>
);
}
}
/* App's initial state */
const initialState = {
userList: [],
randomNum: 0,
userDataLoaded: false
};
/* Action Creator for Thunk */
const userDataLoaded = userdata => {
return {
type: "USER_DATA_LOADED",
data: userdata
};
};
/* Action Creator for Thunk */
const initialRandomLoaded = rval => {
return {
type: "INITIAL_RANDOM_LOADED",
data: rval
};
};
/* load initial data here */
let loadInitialStateData = () => {
return dispatch => {
dispatch(initialRandomLoaded(Math.random()));
$api.call({
method: "get",
url: "https://pen.kcak11.com/mockData/users.json",
success: response => {
let userList = JSON.parse(response);
dispatch(userDataLoaded(userList));
dispatch({ type: "USER_DATA_READY", data: true });
}
});
};
};
let userListReducer = (state = [], action) => {
let newState;
switch (action.type) {
case "USER_DATA_LOADED":
return action.data;
case "USER_DATA_LOADED_STATIC":
newState = Object.assign([], action.userList);
return newState;
case "ADD_USER":
// Return a new array with old state and added user.
newState = [
{
firstName: action.firstName,
lastName: action.lastName
},
...state
];
return newState;
case "REMOVE_USER":
newState = [
// Grab state from begging to index of one to delete
...state.slice(0, action.index),
// Grab state from the one after one we want to delete
...state.slice(action.index + 1)
];
return newState;
default:
return state;
}
};
let randomNumReducer = (state = 0, action) => {
let newState;
switch (action.type) {
case "INITIAL_RANDOM_LOADED":
return action.data;
case "UPDATE_RANDOM":
newState = Math.random();
return newState;
default:
return state;
}
};
let userDataLoadedReducer = (state = false, action) => {
switch (action.type) {
case "USER_DATA_READY":
return action.data;
default:
return state;
}
};
/* rootReducer combines the various reducers for our app */
let rootReducer = combineReducers({
userList: undoable(userListReducer, {
filter: excludeAction(["INITIAL_RANDOM_LOADED", "UPDATE_RANDOM"])
}),
randomNum: randomNumReducer,
userDataLoaded: userDataLoadedReducer
});
/* actions for our app */
const actions = {
addUser: (firstName, lastName) => {
return {
type: "ADD_USER",
id: getUniqueId(),
firstName,
lastName
};
},
removeUser: index => {
return {
type: "REMOVE_USER",
index
};
},
updateRandomNum: rnum => {
return {
type: "UPDATE_RANDOM",
rnum
};
}
};
/* AppContainer for the App */
/* withRouter is needed when we want the Router capabilities for our App, else it can be ommitted */
const AppContainer = withRouter(
connect(
function mapStateToProps(state) {
return {
userList: state.userList,
randomNum: state.randomNum,
userDataLoaded: state.userDataLoaded
};
},
function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch);
}
)(App)
);
let store;
/* Disabling Redux Dev Tools as it is buggy */
let reduxDevToolsEnabled = false;
let loadInitialStateDataViaStaticCode = () => {
let userList = [
{
firstName: "Ervin",
lastName: "Howell",
id: "B34E952CA61F87D0"
},
{
firstName: "Clementine",
lastName: "Bauch",
id: "23894BEC50A716DF"
}
];
let createStoreArgs = [];
createStoreArgs.push(rootReducer);
createStoreArgs.push(initialState);
if (reduxDevToolsEnabled && window.__REDUX_DEVTOOLS_EXTENSION__) {
createStoreArgs.push(window.__REDUX_DEVTOOLS_EXTENSION__());
}
store = createStore.apply(null, createStoreArgs);
store.dispatch({
type: "INITIAL_RANDOM_LOADED",
data: Math.random()
});
store.dispatch({
type: "USER_DATA_LOADED_STATIC",
userList: userList,
randomNum: Math.random()
});
store.dispatch({ type: "USER_DATA_READY", data: true });
};
// use 'compose' when applying multiple middlewares, in example below we apply the thunk, devtools extension
let loadInitialStateDataViaAJAX = () => {
let middlewares = [applyMiddleware(thunk)];
if (reduxDevToolsEnabled && window.__REDUX_DEVTOOLS_EXTENSION__) {
middlewares.push(window.__REDUX_DEVTOOLS_EXTENSION__());
}
store = createStore(rootReducer, compose.apply(null, middlewares));
store.dispatch(loadInitialStateData());
};
//loadInitialStateDataViaStaticCode();
loadInitialStateDataViaAJAX();
/* The React app gets mounted here */
ReactDOM.render(
<Provider store={store}>
<Router forceRefresh={false}>
<AppContainer />
</Router>
</Provider>,
document.getElementById("app")
);
document.addEventListener(
"contextmenu",
function(e) {
e.preventDefault();
},
false
);
document.title = "React & Redux Apps Template";
};
new AppBootstrap(_module).run();
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script src="https://cdn.kcak11.com/libraries/app-bootstrap.min.js"></script>
<script src="https://cdn.kcak11.com/polyfills/object-assign.js"></script>
<script src="https://cdn.kcak11.com/libraries/ajaxify.js"></script>
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.1.1/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/4.3.1/react-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-dom/4.3.1/react-router-dom.min.js"></script>
<script src="https://cdn.kcak11.com/libraries/redux-undo.min.js"></script>
/*
© 2018 https://kcak11.com / https://ashishkumarkc.com
*/
* {
outline: none;
}
html {
overflow-x: hidden;
}
body {
font-family: Verdana;
padding: 20px 10px;
overflow-x: hidden;
}
.pageHeader {
background-color: #000;
color: #fff;
margin: 0;
padding: 10px;
margin-bottom: 30px;
}
.pageHeader.fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 11;
}
a {
color: #6db1dc;
}
hr {
background-color: #97c5e4;
border: none;
height: 4px;
margin: 22px -30px;
}
.pageTitle {
font-size: 20px;
color: #6db1dc;
}
.subTitle {
position: relative;
top: -20px;
height: 1px;
}
.playListRow {
color: orange;
border: 1px solid #000;
}
table {
width: 600px;
border-spacing: 0;
border-collapse: collapse;
margin-left: 5px;
}
table th,
table td {
width: 200px;
text-align: left;
border: 1px solid #000;
padding-left: 5px;
}
.delBtn {
background-color: #f00;
color: #fff;
}
.center_align {
text-align: center;
}
input[type="text"] {
height: 30px;
width: 200px;
font-size: 16px;
box-sizing: border-box;
padding: 0;
margin-left: 5px;
display: inline-block;
vertical-align: bottom;
}
.addUserBtn,
.undoBtn,
.redoBtn,
.unmountAppBtn {
background-color: #000;
color: #fff;
height: 30px;
width: 100px;
margin-left: 5px;
display: inline-block;
vertical-align: bottom;
border: 2px solid #f00;
box-sizing: border-box;
}
.undoBtn,
.redoBtn {
border: 2px solid #97c5e4;
background-color: #fff;
color: #000;
}
.undoBtn[disabled],
.redoBtn[disabled] {
border: 2px solid #000;
background-color: #f3f3f3;
opacity: 0.25;
}
.unmountAppBtn {
height: 50px;
width: 242px;
font-size: 30px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment