Demo App : modokemdev.com/daily-organizer
Goal: Create a view layer which is fast, usable and easy to maintain.
Limitation: Until we add back end (MongoDB), data cannot be persisted.
This repository was built following the Building a Full Stack App with React and Express PluralSight course by Daniel Stern (Here is the original course repository in GitHub). This app uses React and Redux to display components. Routing determines which components to display. Express allows to communicate with MongoDB through REST API.
The express-react-app can run locally on your machine. Clone the repository and run npm install followed by npm run dev.
This README contains the notes I took from the course:
- Express-react-app
- Security Considerations
- Webpack setup
- Add Redux
- Add Routing and Navigation
- Add Sagas
- Creating Persistent Data storage with Node, Express, and MongoDB
If security is a concern for your app, you should look at https://www.pluralsight.com/authors/troy-hunt, or any other security resources. This application is not intended to be used for logins, store passwords, confidential data, etc.
Why should we use Webpack?... Because browsers can't understand .jsx files!
Webpackis a library that usesbabel(another library) to convert.jsxandES6files into.jsfiles.- One thing
Webpackdoes thatbabelcan't is it bundles set of files connected by import statements into one file. Thus the output in the gh-pages branch has only one.jsfile. Webpackhas a tool calledwebpack-dev-serverwhich allow us to create an application in a fast and convenient way.
- Generate a
package.jsonfile:
# --yes is used to generate a default package.json file.
npm init --yes- Install
Webpack:
# You can replace webpack with webpack@4.17.2 to avoid errors (latest at the time of the demo)
npm install --save-dev webpackIMPORTANT: Add
.gitignorefile, and addnode_modulesto it to stop indexing those files. This is important before the first commit.
- Install other dependencies:
# Webpack related dependencies
npm install --save-dev webpack-cli webpack-dev-server
# Babel (@babel/core@7.0.0 at the time of the demo)
npm install --save-dev @babel/core
# @babel/node compiles in the command line | @babel/preset-env compiles ES6 | @babel/preset-react compiles react | @babel/register needs to be present
npm install --save-dev @babel/node @babel/preset-env @babel/preset-react @babel/register
# This package allows transpiling JavaScript files using Babel and webpack.
npm install --save-dev babel-loaderNOTE: See this answer in Stack Overflow for a difference between compiling and transpiling.
The .babelrc file, is a JSON file that Babel automatically checks for to define how .jsx and ES6 should be handled.
The content of the JSON file should be the following:
- @babel/preset-env is for our ES6 compilation.
- @babel/preset-react is for our React.
{
"presets": [
["@babel/preset-env",{
"targets":{
"node":"current"
}
}],
"@babel/preset-react"
]
}The webpack.config.js file describes how our app should be bundled.
Add a webpack.config.js file with the following content:
entry: path.resolve(__dirname, 'src','app')indicates that the main js file is at./src/app/index.js.path: path.resolve(__dirname,'dist')indicates that the output folder will be at./dist.extensions: ['.js','.jsx']is an array of the extensions we want Webpack to process.historyApiFallback: trueis a setting we have to enable if we want to use React-Router.test: /\.jsx?/,means that all.jsor.jsxfiles will be compiled.
IMPORTANT: Add
dist(the output folder) to.gitignore.
const path = require("path");
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, 'src','app'),
output: {
path: path.resolve(__dirname,'dist'),
filename: 'bundle.js',
publicPath: '/',
},
resolve: {
extensions: ['.js','.jsx']
},
devServer: {
historyApiFallback: true,
port: 8080,
host: 'localhost',
open: true
},
module: {
rules: [{
test: /\.jsx?/,
loader:'babel-loader'
}]
}
}NOTE: As of Webpack 5, we need to specify
port,hostandopenoptions indevServerconfiguration. For more info, see: error: option '--open' argument missing and webpack output is served from undefined.
- Add the entry file at
./src/app/index.js:
console.log("Hello world!!!");You will need to add the index.html file in the dist folder. Add the folder to the project root if you don't have it and don't forget to put the dist folder in the .gitignore file:
NOTE: alternatively, you can setup html-webpack-plugin. See the getting started tutorial from Webpack for more info.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<title>Daily Organizer</title>
</head>
<body class="container">
<div id="app"></div>
<script src="/bundle.js"></script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>- Add the following scripts in
package.json:
{
"start": "webpack",
"dev": "webpack-dev-server --open"
}NOTE: As of Webpack 5, the script command is not anymore
webpack-dev-serverbutwebpack serve. For more info, see: webpack-dev-server and DevServer.
# Run the dev script
$ npm run devBecause we specify the
open: trueoption inwebpack.config.js, your browser should open automatically. If not, navigate to http://localhost:8080/. You should seeDaily Organizerdisplayed on the top left of the screen.
- Manages underlying data.
- Application state can be easily accessed.
- Changing application state occurs only via actions.
- Redux state is provided to React components via React-Redux, a small connector library.
If you need more info on Redux, you can look at: https://www.pluralsight.com/courses/flux-redux-mastering.
Add src/server/defaultState.js file:
- This is like a fake database.
- This defines the initial state of the application.
- Here we have users, groups, tasks and comments, and each have their own properties.
export const defaultState = {
users:[{
id:"U1",
name:"Dev"
},{
id:"U2",
name:"C. Eyo"
}],
groups:[{
name:"To Do",
id:"G1",
owner:"U1"
},{
name:"Doing",
id:"G2",
owner:"U1"
},{
name:"Done",
id:"G3",
owner:"U1"
}
],
tasks:[{
name:"Refactor tests",
id:"T1",
group:"G1",
owner:"U1",
isComplete:false,
},{
name:"Meet with CTO",
id:"T2",
group:"G1",
owner:"U1",
isComplete:true,
},{
name:"Compile ES6",
id:"T3",
group:"G2",
owner:"U2",
isComplete:false,
},{
name:"Update component snapshots",
id:"T4",
group:"G2",
owner:"U1",
isComplete:true,
},{
name:"Production optimizations",
id:"T5",
group:"G3",
owner:"U1",
isComplete:false,
}],
comments:[{
id:"C1",
owner:"U1",
task:"T1",
content:"Great work!"
}]
};The Redux store is going to provide the state to the application as necessary.
# redux is at redux@4.0.0 in the original demo
npm install --save reduxCreate the Redux store at ./src/app/store/index.jsx:
- reducer is a special function that always return a new state.
- This Redux store is very basic and only works with
defaultState.
import { createStore } from 'redux';
import { defaultState } from '../../server/defaultState';
export const store = createStore(
function reducer (state = defaultState, action) {
return state;
}
);Import the store in ./app/index.jsx:
- console.log should load the application full state.
import { store } from './store'
console.log(store.getState());NOTE: remove
console.log (store.getState())after testing but keep the import statement!
- Run the application. You should see an
Objectelement in theconsolewhich contains all the data we specified in./src/server/defaultState.js.
Install the following dependencies:
# react@16.4.2 (version in demo) | react-dom turns jsx into html | react-redux@5.0.7 (version in demo)
npm install --save react react-dom react-reduxAdd the Dashboard component at src/app/components/Dashboard.jsx:
.jsxindicates that it is a React file.- You always need to import React first:
import React from 'react';. - Here we use an arrow function to render the
Dashboardcomponent.
import React from 'react';
export const Dashboard = ({groups}) => (
<div>
<h2>Dashboard</h2>
</div>
);We need to specify that we want to render the Dashboard component in the app/index.jsx file:
NOTE: the file was previously named
index.jsbut you will need to rename itindex.jsxand restart you dev server (if it was running).
import { store } from './store'
import React from 'react';
import ReactDOM from 'react-dom';
import { Dashboard } from './components/Dashboard';
ReactDOM.render(
<Dashboard/>,
document.getElementById("app")
);Use React Redux to connect the Dashboard component to the Redux store. The easiest way is to create a parent component which we are going to call Main.jsx (located at src/app/components/Main.jsx):
- The Provider is an element which takes a store as a property and any connected component inside this Provider will have access to the store.
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
export const Main = () => (
<Provider store={store}>
<div>
Dashboard goes here.
</div>
</Provider>
);Update index.jsx to load Main.jsx component (which is our main component now):
import { store } from './store'
import React from 'react';
import ReactDOM from 'react-dom';
import { Main } from './components/Main';
ReactDOM.render(
<Main/>,
document.getElementById("app")
);Add function mapStateToProps(state) to Dashboard component and call connect from Redux.
NOTE: the map function is a basic JavaScript function that simply maps every group in the json file.
import React from 'react';
import { connect } from 'react-redux';
export const Dashboard = ({groups}) => (
<div>
<h2>Dashboard</h2>
{
groups.map(
group => (
<div>
{group.name}
</div>
)
)
}
</div>
);
function mapStateToProps(state) {
return {
groups:state.groups
}
}
export const ConnectedDashboard = connect(mapStateToProps)(Dashboard);Update Main.jsx and import the ConnectedDashboard:
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
import { ConnectedDashboard } from "./Dashboard";
export const Main = ()=>(
<Provider store={store}>
<div>
{/*Dashboard goes here.*/}
<ConnectedDashboard/>
</div>
</Provider>
);The TaskList component shows the tasks in each group. Add the TaskList component at src/app/components/TaskList.jsx:
- The second argument of mapStateToProps are the component properties:
const mapStateToProps = (state, ownProps) => {. They are called ownProps.
import React from 'react';
import { connect } from 'react-redux';
export const TaskList = ({tasks, name}) => (
<div>
<h3>
{name}
</h3>
<div>
{tasks.map(task=>(
<div key={task.id}>{task.name}</div>
))}
</div>
</div>
);
const mapStateToProps = (state, ownProps) => {
let groupID = ownProps.id;
return {
name: ownProps.name,
id: groupID,
tasks: state.tasks.filter(task=>task.group === groupID)
};
};
export const ConnectedTaskList = connect(mapStateToProps)(TaskList);- Update the
Dashboardcomponent to include theTaskListcomponent:
import React from 'react';
import { connect } from 'react-redux';
import { ConnectedTaskList } from './TaskList';
export const Dashboard = ({groups})=>(
<div>
<h2>Dashboard</h2>
{groups.map(group=>(
<ConnectedTaskList key={group.id} id={group.id} name={group.name}/>
))}
</div>
);
function mapStateToProps(state) {
return {
groups:state.groups
}
}
export const ConnectedDashboard = connect(mapStateToProps)(Dashboard);- "Routing" is a term for when the form of the application is affected by the URL bar.
react-routerdetermines which React component to display based on URL.- Good use of routing allows a lot of information to be codified in URL.
React Router will add routing capabilities for our app. Add react-router-dom which is a subset of React Router that is used in the browser:
NOTE: React router is at v6 as of 2022. Many commands have changed. If you have problems refer to the official documentation https://reactrouter.com/docs/en/v6. It is still possible to use v5 with the following documentation https://v5.reactrouter.com/web/guides/quick-start.
# react-router-dom@4.3.1 (at the time of the demo)
$ npm install react-router-dom --saveNOTE: in the original version,
historywas also installed but it seems deprecated. The command used wasnpm install --save history@4.7.2. The method imported from the library was createBrowserHistory which helped React Router to determine what the object is and what it was in the past.
Update Main.jsx to import BrowserRouter and Route:
- We also add a
Navigationcomponent. TheNavigationcomponent imports Link fromreact-router-domto navigate through the app. Refer to the code project for more info.
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
import { ConnectedDashboard } from './Dashboard';
import { BrowserRouter, Route, } from 'react-router-dom';
import { ConnectedNavigation } from './Navigation';
export const Main = ()=> (
<BrowserRouter>
<Provider store={store}>
<div className="container mt-3">
<ConnectedNavigation/>
{/*<ConnectedDashboard/>*/}
<Route
exact
path="/dashboard"
render={ () => (<ConnectedDashboard/>)}
/>
</div>
</Provider>
</BrowserRouter>
)We want to allow the user to create new tasks. This means that we want to allow the user to change the state of the application. This is possible with Sagas.
reducerfunction must be updated to allow tasks array to be changed. If you are following the tutorial from the beginning, you will notice that we are just passing thedefaultStateof the application to thereducerfunction:function reducer (state = defaultState, action) {return state;}.- Tasks need random ID, reducers can't be random, therefore Saga or Thunk is needed.
- Updated state is reflected automatically in React components because all of our components are connected.
You can use Sagas or Thunks to allow data transformation. You can use whichever you prefer.
- Sagas run in the background of Redux applications.
- Respond to actions by generating "side-effects" (anything outside the app).
- Sagas are denoted by a function star syntax (
function*) which is not found in many other situations. This makes Sagas one of the only few places where generators functions are found.
All Sagas are generators. A generator is a kind of JavaScript function. Standard functions return one value right away. However, generators can return any number of values: 3, 4, 5, 10, any number. Generators can return values later, not just right away. This is the main difference between a generator functions and a basic functions.
From developer.mozilla.org, the
function*declaration (functionkeyword followed by an asterisk) defines a generator function, which returns a Generator object.
- Standard JavaScript functions (non-generator) return a single value, instantly.
- Generators can return any number of values, not just one.
- Generator values can be returned at a later time (asynchronously).
function* myGenerator() {
let meaning = 42;
while (true) {
meaning += 1;
yield meaning;
}
}function*indicates special generator function type.- generator contains normal javascript code.
while (true)loops can exist in generator functions. Awhile(true)loop, would normally cause a crash but it is acceptable inside a generator function as long as it has the yield keyword.yieldkeyword returns value to the generator's caller (can return many values). In this case, the value returned ismeaning. It then waits until the generator is invoked again.- Yields 43, 44, 45, ...
In this app, Redux Saga will be invoking these generators for us. For more info, take a look at https://www.pluralsight.com/courses/redux-saga, or the official documentation.
Update TaskList.jsx:
- add a button to create a new task:
<button onClick={() => createNewTask(id)}>Add New</button>. - add
mapDispatchToPropsto pass the new methodcreateNewTaskto the component. We don't pass it through the existingmapStateToPropsmethod. - add
mapDispatchToPropstoconnectmethod which should provide access tocreateNewTaskto the component. - add
createNewTaskas component property:export const TaskList = ({tasks, name, id, createNewTask}) => (.
import React from 'react';
import { connect } from 'react-redux';
export const TaskList = ({tasks, name, id, createNewTask}) => (
<div>
<h3>
{name}
</h3>
<div>
{tasks.map(task=>(
<div key={task.id}>{task.name}</div>
))}
</div>
<button onClick={() => createNewTask(id)}>Add New</button>
</div>
);
const mapStateToProps = (state, ownProps)=>{
let groupID = ownProps.id;
return {
name: ownProps.name,
id: groupID,
tasks: state.tasks.filter(task=>task.group === groupID)
};
};
const mapDispatchToProps = (dispatch, ownProps)=>{
return {
createNewTask(id) {
console.log("Creating new task...", id);
}
};
};
export const ConnectedTaskList = connect(mapStateToProps, mapDispatchToProps)(TaskList);You should be able to see the Add New button and if you enter the console, each time you click the button, it logs "Creating new task..."!
Create a new file at app/store/mutations.js. This file is a template for all the changes to the application state:
REQUEST_TASK_CREATIONandCREATE_TASKare mutations.requestTaskCreationandcreateTaskare methods that automatically create objects to do these mutations.createTaskwill be dispatched by the saga once it's finished creating this object complete with its own random ID.
export const REQUEST_TASK_CREATION = `REQUEST_TASK_CREATION`;
export const CREATE_TASK = `CREATE_TASK`;
export const requestTaskCreation = (groupID) => ({
type:REQUEST_TASK_CREATION,
groupID
});
export const createTask = (taskID, groupID, ownerID) => ({
type:CREATE_TASK,
taskID,
groupID,
ownerID
});Update TaskList component:
- import
requestTaskCreationfrommutation.js. - add a call to
dispatchfunction increateNewTaskmethod which will dispatch therequestTaskCreationmutation with the id provided.
import React from 'react';
import { connect } from 'react-redux';
import { requestTaskCreation } from '../store/mutations';
export const TaskList = ({tasks, name, id, createNewTask})=>(
<div className="card p-2 m-2">
<h3>
{name}
</h3>
<div>
{tasks.map(task=>(
<div key={task.id}>{task.name}</div>
))}
</div>
<button onClick={ () => createNewTask(id) }>Add New</button>
</div>
);
const mapStateToProps = (state, ownProps)=>{
let groupID = ownProps.id;
return {
name: ownProps.name,
id: groupID,
tasks: state.tasks.filter(task=>task.group === groupID)
};
};
const mapDispatchToProps = (dispatch, ownProps)=>{
return {
createNewTask(id) {
console.log("Creating new task...", id);
dispatch(requestTaskCreation(id));
}
};
};
export const ConnectedTaskList = connect(mapStateToProps, mapDispatchToProps)(TaskList);To help us understand what is going on, we want to add logging (console logging, not user login!).
# redux-logger@3.0.6 (at the original time of the demo)
$ npm install --save redux-loggerUpdate the store/index.js file:
- import
createLoggerfromredux-logger. - add a second import from
reduxcalledapplyMiddleware. - add a second argument to
createStoreforredux-loggerto work:applyMiddleware(createLogger()).
import { createStore, applyMiddleware } from 'redux';
import { defaultState } from '../../server/defaultState';
import { createLogger } from 'redux-logger/src';
export const store = createStore(
function reducer (state = defaultState, action) {
return state;
},
applyMiddleware(createLogger())
);Now, whenever we dispatch an action, we will see it in the console! (Example: action REQUEST_TASK_CREATION)
Usually, actions change the state of the application. However, for actions that require some kind of randomness, like TASK_CREATION, we need an intermediary like a saga. A saga will deal with this unusual request.
# redux-saga@0.16.2 (at the original time of the demo)
$ npm install --save redux-logger redux-sagaWe need a library to generate random strings.
# uuid will generate random id
$ npm install --save uuid- Files with
.mockextension indicate the file does not contain the true business logic. - Used to reduce complexity (eg., does not depend on server).
- Mocks are commonly used in testing framework such as Jest.
Add a new saga at store/sagas.mock.js. All the real sagas will be communicating with the server, but until we have that, we are going to use these mocks that will do it on their own:
- You will need all the
importstatements. taskCreationSagais a saga to create a new task.takefunction will stop until the specified action is dispatched. In this context,groupIDis a property we get from the action. This is why we can log it just after.- The
ownerIDis hardcoded to'U1'because no login has been implemented. - The
taskIDneeds to be a random string so we call our random generatoruuid(). putfunction will send the action to the store. The mutation we want to send is thecreateTaskmutation.
import { take, put, select } from 'redux-saga/effects';
import * as mutations from './mutations';
import { v1 as uuid } from 'uuid';
export function* taskCreationSaga() {
while (true) {
const {groupID} = yield take(mutations.REQUEST_TASK_CREATION);
console.log("Got group ID", groupID);
const ownerID = 'U1';
const taskID = uuid();
yield put(mutations.createTask(taskID, groupID, ownerID));
}
}To run the saga, update store/index.js:
- add import
createSagaMiddlewarefromredux-sagaand assigned toconst sagaMiddleware. - import all sagas from
sagas.mock.js. - import all mutations from
mutations.js. - import
combineReducesfromredux. It creates a reducer that deals with each collection in our state differently. - replace
reducerfunction withcombineReducersfunction. combineReducerstakes objects as argument. The name of each property of the object corresponds to the collection.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { defaultState } from '../../server/defaultState';
import { createLogger} from 'redux-logger/src';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
import * as sagas from './sagas.mock';
import * as mutations from './mutations';
export const store = createStore(
combineReducers({
tasks(tasks = defaultState.tasks, action) {
switch (action.type) {
case mutations.CREATE_TASK:
return [...tasks, {
id:action.taskID,
name:"New Task",
group: action.groupID,
owner: action.ownerID,
isComplete: false
}]
}
return tasks;
},
comments(comments = defaultState.comments) {
return comments;
},
groups(groups = defaultState.groups) {
return groups;
},
users(users = defaultState.users) {
return users;
}
}),
applyMiddleware(createLogger(), sagaMiddleware)
);
for (let saga in sagas) {
sagaMiddleware.run(sagas[saga]);
}The Task Detail page will allow users to modify the tasks.
- Add route which displays the details of a single task
- Route will implement forms and buttons to allow user to change data
- Router will be used to indicate which task should be viewed
- Interactions which mutate the state will be added later
Create a new file app/components/TaskDetail.jsx:
- Import
React,connectandLink. - Add all the component UI.
- Add
mapStateToPropsfunction.
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
const TaskDetail = ({
id,
comments,
task,
isComplete,
groups
}) => {
return (
<div>
<div>
<input type="text" value={task.name} className="form-control form-control-lg"/>
</div>
<form className="form-inline">
<span>
Change Group
</span>
<select className="form-control">
{groups.map(group => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</form>
<form className="form-inline">
<input type="text" name="commentContents" autoComplete="off" placeholder="Add a comment" className="form-control"/>
<button type="submit" className="btn">Submit</button>
</form>
<div>
<Link to="/dashboard">
<button>
Done
</button>
</Link>
</div>
</div>
)
}
function mapStateToProps(state,ownProps) {
let id = ownProps.match.params.id;
let task = state.tasks.find(task => task.id === id);
let groups = state.groups;
return {
id,
task,
groups,
isComplete: task.isComplete
}
}
export const ConnectedTaskDetail = connect(mapStateToProps)(TaskDetail);Update components/Main.jsx:
- Add a route to the new
TaskDetailcomponent. - Import
TaskDetailcomponent. - The
matchargument is the path id.
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
import { ConnectedDashboard } from './Dashboard';
import { BrowserRouter, Route, } from 'react-router-dom';
import { ConnectedNavigation } from './Navigation';
import { ConnectedTaskDetail} from './TaskDetail';
export const Main = ()=>(
<BrowserRouter>
<Provider store={store}>
<div className="container mt-3">
<ConnectedNavigation/>
{/*<ConnectedDashboard/>*/}
<Route
exact
path="/dashboard"
render={() => (<ConnectedDashboard/>)}
/>
<Route
exact
path="/task/:id"
render={({ match }) => (<ConnectedTaskDetail match={match}/>)}
/>
</div>
</Provider>
</BrowserRouter>
)Update components/TaskList.jsx:
- Import
Linkfromreact-router-dom. - For every task that is mapped, we render a link to its task page:
Link to={`/task/${task.id}`}.
import React from 'react';
import { connect } from 'react-redux';
import { requestTaskCreation} from '../store/mutations';
import { Link} from 'react-router-dom';
export const TaskList = ({tasks, name, id, createNewTask})=>(
<div>
<h3>
{name}
</h3>
<div>
{tasks.map(task => (
<Link to={`/task/${task.id}`} key={task.id}>
<div>{task.name}</div>
</Link>
))}
</div>
<button onClick={() => createNewTask(id)}>Add New</button>
</div>
);
const mapStateToProps = (state, ownProps)=>{
let groupID = ownProps.id;
return {
name: ownProps.name,
id: groupID,
tasks: state.tasks.filter(task=>task.group === groupID)
};
};
const mapDispatchToProps = (dispatch, ownProps)=>{
return {
createNewTask(id) {
console.log("Creating new task...", id);
dispatch(requestTaskCreation(id));
}
};
};
export const ConnectedTaskList = connect(mapStateToProps, mapDispatchToProps)(TaskList);- Add methods which dispatch actions when form elements of the task detail are interacted with
- Add clauses to Redux reducer which causes state to be changed in response to relevant action
Update components/TaskDetail.jsx:
- Modify the
buttonoutput text (Reopen/Complete). - Add
mapDispatchToPropsfunction. - import all
mutationsfrommutations.js. - After adding the mutations to
mapDispatchToPropsfunction, pass the new functions as arguments toTaskDetailcomponent. - Pass
mapDispatchToPropsas a second argument toconnectfunction. - Add
setTaskGroupandsetTaskNametomapDispatchToPropsand pass them as arguments toTaskDetailcomponent. - Add
onChange={setTaskName}toinputbutton. - Add
onChange={setTaskGroup}toselectdropdown.
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import * as mutations from '../store/mutations';
const TaskDetail = ({
id,
comments,
task,
isComplete,
groups,
setTaskCompletion,
setTaskGroup,
setTaskName
})=>{
return (
<div>
<div>
<input type="text" value={task.name} onChange={setTaskName} className="form-control form-control-lg"/>
</div>
<button onClick={() => setTaskCompletion(id,!isComplete)}>
{isComplete ? `Reopen` : `Complete`} This Task
</button>
<form className="form-inline">
<span>
Change Group
</span>
<select onChange={setTaskGroup} value={task.group} className="form-control">
{groups.map(group=>(
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</form>
<form className="form-inline">
<input type="text" name="commentContents" autoComplete="off" placeholder="Add a comment" className="form-control"/>
<button type="submit" className="btn">Submit</button>
</form>
<div>
<Link to="/dashboard">
<button className="btn btn-primary mt-2">
Done
</button>
</Link>
</div>
</div>
)
}
function mapStateToProps(state,ownProps) {
let id = ownProps.match.params.id;
let task = state.tasks.find(task=>task.id === id);
let groups = state.groups;
return {
id,
task,
groups,
isComplete: task.isComplete
}
}
function mapDispatchToProps(dispatch, ownProps) {
let id = ownProps.match.params.id;
return {
setTaskCompletion(id,isComplete){
dispatch(mutations.setTaskCompletion(id,isComplete));
},
setTaskGroup(e){
dispatch(mutations.setTaskGroup(id,e.target.value));
},
setTaskName(e){
dispatch(mutations.setTaskName(id,e.target.value));
}
}
}
export const ConnectedTaskDetail = connect(mapStateToProps, mapDispatchToProps)(TaskDetail);Update store/mutations.js:
- Add constants:
SET_TASK_COMPLETE,SET_TASK_GROUP,SET_TASK_NAME. - Add action creators:
setTaskCompletion,setTaskGroup,setTaskName.
export const REQUEST_TASK_CREATION = `REQUEST_TASK_CREATION`;
export const CREATE_TASK = `CREATE_TASK`;
export const SET_TASK_COMPLETE = `SET_TASK_COMPLETE`;
export const SET_TASK_GROUP = `SET_TASK_GROUP`;
export const SET_TASK_NAME = `SET_TASK_NAME`;
export const requestTaskCreation = (groupID) => ({
type:REQUEST_TASK_CREATION,
groupID
});
export const createTask = (taskID, groupID, ownerID) => ({
type:CREATE_TASK,
taskID,
groupID,
ownerID
});
export const setTaskCompletion = (id, isComplete) => ({
type:SET_TASK_COMPLETE,
taskID: id,
isComplete
});
export const setTaskGroup = (id, groupID) => ({
type:SET_TASK_GROUP,
taskID: id,
groupID
});
export const setTaskName = (id, name) => ({
type:SET_TASK_NAME,
taskID: id,
name
});Update store/index.js (reducer):
- Add new case
mutations.SET_TASK_COMPLETE. - Add new cases for
mutations.SET_TASK_NAMEandmutations.SET_TASK_GROUP.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { defaultState } from '../../server/defaultState';
import { createLogger} from 'redux-logger/src';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
import * as sagas from './sagas.mock';
import * as mutations from './mutations';
export const store = createStore(
combineReducers({
tasks(tasks = defaultState.tasks, action) {
switch (action.type) {
case mutations.CREATE_TASK:
return [...tasks, {
id:action.taskID,
name:"New Task",
group: action.groupID,
owner: action.ownerID,
isComplete: false
}]
case mutations.SET_TASK_COMPLETE:
return tasks.map(task => {
return (task.id === action.taskID) ?
{...task, isComplete:action.isComplete} :
task;
})
case mutations.SET_TASK_NAME:
return tasks.map(task => {
return (task.id === action.taskID) ?
{...task, name:action.name} :
task;
})
case mutations.SET_TASK_GROUP:
return tasks.map(task => {
return (task.id === action.taskID) ?
{...task, group:action.groupID} :
task;
});
}
return tasks;
},
comments(comments = defaultState.comments) {
return comments;
},
groups(groups = defaultState.groups) {
return groups;
},
users(users = defaultState.users) {
return users;
}
}),
applyMiddleware(createLogger(), sagaMiddleware)
);
for (let saga in sagas) {
sagaMiddleware.run(sagas[saga]);
}- Webpack is useful as it allows us to write code using imports and with JSX.
- Redux is a reliable and convenient way to store and manage our application state.
- React components often contain forms used by the end user.
- Using React-Redux, React components can update automatically to reflect data.
- Database for storing persistent data
- Non-relational (collections, not tables, fluid data structure)
- Convenient JSON-based communication works with Node
- Alternative to relational databases such as MySQL