-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
-
Click to view all steps
- Average Front End Developer Salary
- State of JavaScript
- State of CSS
- HackerRank Developer Skills Report
- Stack Overflow Developer Survey
// ES6 Modules - export
export const myNumbers = [1, 2, 3, 4];
const myLogger = () => {
console.log(myNumbers, pets);
}
export default myLogger;
// ES6 Modules - import
import { myNumbers } from 'app.js';
import myLogger from 'app.js';
// Arrow function
const sum = (x,y) => {
return x + y;
}
const sum = (x,y) => x + y;
const sum = (x,y) => {
return x + y;
}
sum(1,2);
// Array spreading
const sum = (x, y, z) => x + y + z;
const numbers = [1, 2, 3];
sum(...numbers);
// Object spreading
const coordinates = {
address: '59 New Bridge Road',
zipCode: '059405',
country: 'Singapour'
}
const employee = {
firstName: 'John',
lastName: 'Doe',
...coordinates
}
- Create a folder called
react-training
- Unzip
resources.zip
andserver.zip
- Move unzipped
resources
andserver
folder intoreact-training
react-training
folder should have this structure
├── resources
│ ├── data.json
│ ├── edition.html
│ ├── navigation.html
│ └── rule.html
└── server
└── ...
-
Install the following apps
-
Check if node is installed
node -v
- If node is not installed, download 12.18.3 LTS from https://nodejs.org/en/
- Run the installation
- Check if node is installed successfully
node -v
npm -v
- In
react-training
folder, create aclient
app withcreate-react-app
npx create-react-app client
react-training
folder should have this structure
├── resources
│ ├── data.json
│ ├── edition.html
│ ├── navigation.html
│ └── rule.html
└── server
└── client
- Start app
cd client
npm start
- Open the browser at http://localhost:3000 URL, you should see a welcome message and a spinning React logo.
- To see the hot-reload in action, open the
App.js
file and change one of the text string, the text is updated in the browser immediately, without manual refresh.
- Create a New React App
- How to create a new app
- How to develop apps bootstrapped with Create React App
- Open the
src/index.js
file - Replace existing
ReactDOM.render
const reactElement = React.createElement('div', null, 'Hello World');
const domElement = document.getElementById('root'); ReactDOM.render(reactElement, domElement);
- Check that app should now show
Hello World
- Install bootstrap
npm install bootstrap@3.x.x --save
- In
index.js
file, importbootstrap
import 'bootstrap/dist/css/bootstrap.css';
- HTML code for the first components can be found in
resources/rule.html
- When copying HTML code in the return JSX, think about the syntax differences between HTML and JSX (
class
attribute must be replaced byclassName
) - Different ways of writing a functional component
// Function component with no props
const Rule = () => <div>Rule</div>;
const Rule = () => {
return <div>Rule</div>;
};
const Rule = () => {
return (
<div>Rule</div>
);
};
// Function component with props
const Rule = props => <div>{props.title}</div>;
const Rule = props => {
return <div>{props.title}</div>;
};
const Rule = props => {
return (
<div>{props.title}</div>
);
};
// Function component with Destructuring props
const Rule = ({ title }) => <div>{title}</div>;
const Rule = ({ title }) => {
return <div>{title}</div>;
};
const Rule = ({ title }) => {
return (
<div>{title}</div>
);
};
- In src folder, create a file named
RuleList.js
- Import React
import React, { Fragment } from "react";
- Create a function
RuleList
for this new component
const RuleList = () => {
}
- At the end of the file, export it by default
export default RuleList;
- Implement the return JSX
const RuleList = () => {
return ()
}
- The rules to display will be provided as props
const RuleList = ({ rules }) => {
return ()
}
- The function must return a root JSX element
const RuleList = ({ rules }) => {
return <Fragment></Fragment>
}
- To create a React list from a JavaScript array, use the map function: (Array.prototype.map() )
// Array as children
const newRules = (rules || []).map(rule => {
return (
<div>{rule.title}</div>
);
});
- Deconstruct all values we need from each rule
const { title, description, likes, dislikes, tags } = rule;
- In newrules, get list of tags
const newTags = (tags || []).map(tag => (
<span key={tag} className="badge">
{tag}
</span>
));
- Paste the copied HTML from
resources/rule.html
into the return ofnewRules
return (
<div class="panel panel-primary">
<div class="panel-heading" role="presentation">
Leave the code cleaner than you found it.
<i class="pull-right glyphicon glyphicon-chevron-down" />
</div>
<div class="panel-body">
<p>From Clean Code: always leave the code cleaner than it was before.</p>
</div>
<div class="panel-footer">
<div class="btn-toolbar">
<span class="badge">craftsmanship</span>
<span class="badge">clean code</span>
<div class="btn-group btn-group-xs pull-right">
<button class="btn btn-primary" title="Update">
<i class="glyphicon glyphicon-pencil" />
</button>
</div>
<div class="btn-group btn-group-xs pull-right">
<button class="btn btn-default" title="+1">
0 <i class="glyphicon glyphicon-thumbs-up" />
</button>
<button class="btn btn-default" title="-1">
0 <i class="glyphicon glyphicon-thumbs-down" />
</button>
</div>
</div>
</div>
</div>
);
- Update HTML to JSX syntax (
class
toclassName
) - Update HTML to be receive dynamic information
return(
<div className="panel panel-primary">
<div className="panel-heading" role="presentation">
{title}
<i className="pull-right glyphicon glyphicon-chevron-down"></i>
</div>
<div className="panel-body">
<p>{description}</p>
</div>
<div className="panel-footer">
<div className="btn-toolbar">
{newTags}
<div className="btn-group btn-group-xs pull-right">
<button className="btn btn-primary" title="Update">
<i className="glyphicon glyphicon-pencil"></i>
</button>
</div>
<div className="btn-group btn-group-xs pull-right">
<button className="btn btn-default" title="+1">
{likes} <i className="glyphicon glyphicon-thumbs-up"></i>
</button>
<button className="btn btn-default" title="-1">
{dislikes} <i className="glyphicon glyphicon-thumbs-down"></i>
</button>
</div>
</div>
</div>
</div>
)
- To bootstrap the app, open
src/index.js
and import the new component
import RuleList from "./RuleList";
- Provide the rules to display by draging the file
data.json
from the resources directory into src - Import
data.json
import rules from './data';
console.log('rules = ', rules);
- Call ReactDOM.render method to render the element inside the DOM element with the id
root
const reactElement = <RuleList rules={rules} />;
- Check if the application is working well
- In src folder, create a file named
Rule.js
- In this file, create a function
Rule
and export it by default - Implement the return JSX
- The rule to be displayed will be provided as props
const Rule = ({ rule: { title, description, likes, dislikes, tags } }) => {
...
}
- In
RuleList.js
, import the newRule
component
import Rule from "./Rule";
- Update return jsx to use new
Rule
component
const newRules = (rules || []).map(rule => (
<Rule key={rule.id} rule={rule} />
));
- Check if the application is working well
- Create a
Rule.css
file sibling toRule.js
- Add a CSS property to display the "hand" cursor when the user moves the mouse over the title panel
.panel-heading {
cursor: pointer;
}
- Import the CSS file in
Rule.js
import "./Rule.css";
- In
Rule.js
, import the useState hook
import React, { useState } from "react";
- Initialize the default component state with useState hook. By default, this property must be false to display the description.
const [folded, setFolded] = useState(!description);
- Display or hide the description using the hidden CSS class depending on the folded value
<div className={`panel-body ${folded ? "hidden" : ""}`}>
...
</div>
- Update CSS class of icon in the title depending on the folded value. The icon should either be glyphicon-chevron-down or glyphicon-chevron-up
const cssFolded = folded ? "up" : "down";
<i className={`pull-right glyphicon glyphicon-chevron-${cssFolded}`}></i>
- Create a function which toggles the folded value
const toggleFolded = () => setFolded(!folded);
- Call that function to display / hide the description when the user clicks on the title of a rule
<div className="panel-heading" role="presentation" onClick={toggleFolded}>
...
</div>
- Check if the application is working well
- Using the State Hook
- React Hooks Cheatsheet
- 3 Mistakes Junior Developers Make With React Function Component State
- Template literals (Template strings)
- Conditional (ternary) operator
- Falsy
- Create a file named
LikeBtn.js
- Create a function
LikeBtn
and export it by default - The same button will be used for "like" and "dislike". Button type will be provided as props to generate the appropriate HTML code
const LikeBtn = ({ type }) => {
};
- Initial counter value will be provided as props.
const LikeBtn = ({ type, counter: initialCount }) => {
};
- Create constant for title depending on button type
const title = type === "up" ? "+1" : "-1";
- Initialize the default counter state with useState hook.
const [counter, setCounter] = useState(initialCount);
- Implement return JSX
return (
<button className="btn btn-default" title={title}>
{counter} <i className={`glyphicon glyphicon-thumbs-${type}`}></i>
</button>
);
- Create a method to increment the counter
const increment = () => {
setCounter(prev => prev + 1);
};
- Call increment method when clicking on the button
<button className="btn btn-default" title={title} onClick={increment}>
...
</button>
- In
Rule
component, importLikeBtn
component and use it to replace<button>
<div className="btn-group btn-group-xs pull-right">
<LikeBtn type="up" counter={likes} />
<LikeBtn type="down" counter={likes} />
</div>
- Install the prop-types module
- In
RuleList.js
, import prop-types module
import PropTypes from "prop-types";
- Attach a propTypes object property
RuleList.propTypes = {
};
- Define a type for each props used in the component
RuleList.propTypes = {
rules: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired
})
).isRequired
};
- Define defaultProps which will be used unless the parent overrides them
RuleList.defaultProps = {
rules: []
};
- Add propTypes to the other components.
- In
Rule.js
, it should look like this:
Rule.defaultProps = {
rule: {
title: "",
description: "",
likes: 0,
dislikes: 0,
tags: []
}
};
Rule.propTypes = {
rule: PropTypes.shape({
title: PropTypes.string,
description: PropTypes.string,
likes: PropTypes.number,
dislikes: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.string)
}).isRequired
};
- In
LikeBtn.js
it should look like this:
LikeBtn.defaultProps = {
counter: 0
};
LikeBtn.propTypes = {
type: PropTypes.oneOf(["up", "down"]).isRequired,
counter: PropTypes.number
};
create-react-app
is not embed React Testing Library by default- Install React Testing Library
npm install --save-dev @testing-library/react @testing-library/jest-dom
- To start writing tests, create a folder named
__tests__
- In this src folder, create a file with the name of the component being tested, eg
Rule.test.js
- In the file, import React Testing Library, the component being tested and other relevant libraries
import { cleanup, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import Rule from "../Rule";
- To implement the first test to check a rule is displayed correctly, add rules json import
import rules from "../data.json";
- Create a test suite with
describe
describe("Rule", () => {
});
- Instantiate the component and use the
render
method to render the component in the DOM before each test
let rule;
let wrapper;
beforeEach(() => {
rule = rules[0];
wrapper = render(<Rule rule={rule} />);
});
- In test suite, create a test case with
test
and a short description
test("should render rule title", () => {
});
- Get title element with
getByText
const titleElement = getByText(rule.title);
- Add assertion to check the component renders rule title
expect(titleElement).toBeInTheDocument();
- Add a cleanup function in test suite
afterEach(cleanup);
- The test suite should look like this
describe("Rule", () => {
let rule;
let wrapper;
beforeEach(() => {
rule = rules[0];
wrapper = render(<Rule rule={rule} />);
});
afterEach(cleanup);
test("should render rule title", () => {
const titleElement = getByText(rule.title);
expect(titleElement).toBeInTheDocument();
});
});
- Run the tests
npm test
- Create a new file for RuleList tests and import relevant dependencies
- Add a new test suite for RuleList with
describe
describe("Rule List", () => {
});
- Instantiate the component with
render
let getByText;
beforeEach(() => {
({ getByText } = render(<RuleList rules={rules} />));
});
- Create a test case with
test
and description of the test
test("should display rules titles", () => {
});
- Add an assertion to check the component renders all rule titles
rules.forEach(rule => {
const titleElement = getByText(rule.title);
expect(titleElement).toBeInTheDocument();
});
- Run the tests
- Create a new file for LikeBtn tests and import relevant dependencies
- Besides
render
, importfireEvent
from React Testing Library
import { fireEvent, render } from "@testing-library/react";
- Create a test suite and instantiate the component
describe("LikeBtn", () => {
let getByTitle;
beforeEach(() => {
({ getByTitle } = render(<LikeBtn type={"up"} counter={0} />));
});
});
- Create a test case with
test
and description
test("should increment counter", () => {
});
- Instantiate the component and check the initial counter to be 0
const likeButtonElement = getByTitle("+1");
expect(likeButtonElement).toHaveTextContent("0");
- In the test case, use
fireEvent
method to simulate a click on the component
fireEvent.click(likeButtonElement);
- Check that the counter value has been incremented
expect(likeButtonElement).toHaveTextContent("1");
- The final test case should look like this
test("should increment counter", () => {
const likeButtonElement = getByTitle("+1");
expect(likeButtonElement).toHaveTextContent("0");
fireEvent.click(likeButtonElement);
expect(likeButtonElement).toHaveTextContent("1");
});
A full test suite will look like this:
import React from "react";
import { cleanup, fireEvent, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import Rule from "../Rule";
import rules from "../data.json";
describe("Rule", () => {
let rule;
let wrapper;
beforeEach(() => {
rule = rules[0];
wrapper = render(<Rule rule={rule} />);
});
afterEach(cleanup);
// Find by element text content
it("should render rule title", () => {
const titleElement = wrapper.getByText(rule.title);
expect(titleElement).toBeInTheDocument();
});
// Find by title attribute
it("should render rule likes", () => {
const likesElement = wrapper.getByTitle("+1");
expect(likesElement).toHaveTextContent(rule.likes);
});
// Callback fires on button click
it("should hide description when clicking on title", () => {
const titleElement = wrapper.getByText(rule.title);
fireEvent.click(titleElement);
const descriptionElement = wrapper.getByText(rule.description);
expect(descriptionElement.parentNode).toHaveClass("hidden");
});
});
- For the sake of separation of concerns, we will load the rules in a dedicated action instead of importing them in
index.js
- Inside the src folder, create a new folder called
actions
- Create a file named
rules-actions.js
- Import the rules from the
data.json
file
import rules from "../data.json";
- Create a function named
loadRules
const loadRules = () => {
};
- Return an action named
RULES_LOADED
containing the rules
return {
type: RULES_LOADED,
payload: rules
};
- Export the function
export const loadRules = () => {
...
}
- Export the action name as a constant
export const RULES_LOADED = "RULES_LOADED";
- Bonus: unit test should look something like:
import rules from "../../data.json";
import { RULES_LOADED, loadRules } from "../rules-actions";
describe("Rules Actions", () => {
test("should load rules", () => {
const expectedAction = {
type: RULES_LOADED,
payload: rules
};
const action = loadRules();
expect(action).toEqual(expectedAction);
});
});
- Inside the src folder, create a folder named
reducers
- Create a file named
rules-reducer.js
- Import action name from rules-action
import { RULES_LOADED } from "../actions/rules-actions";
- Create a rulesReducer function state as first parameter (initialized by default with [] ) and an action as second parameter
const rulesReducer = (state = [], action) => {
...
};
- Write a switch statement and return state by default
switch (action.type) {
default:
return state;
}
- Handle the RULES_LOADED action by saving the rules from the RULES_LOADED action into the state
switch (action.type) {
case RULES_LOADED: {
return action.payload;
}
...
}
- Export the reducer
export default rulesReducer;
- Bonus: unit test should look something like:
import rules from "../../data.json";
import reducer from "../rules-reducer";
import { RULES_LOADED } from "../../actions/rules-actions";
describe("Rules reducer", () => {
test("should return the initial state", () => {
const action = {};
const previousState = undefined;
const expectedNewState = [];
const newState = reducer(previousState, action);
expect(newState).toEqual(expectedNewState);
});
test("should load rules", () => {
const action = {
type: RULES_LOADED,
payload: rules
};
const previousState = [];
const expectedNewState = rules;
const newState = reducer(previousState, action);
expect(newState).toEqual(expectedNewState);
});
});
- Inside the src folder, create a folder named
store
- Create a file named
app-store.js
- Create a future-proof global reducer using combineReducers
import { combineReducers } from "redux";
import rulesReducer from "../reducers/rules-reducer";
const rootReducer = combineReducers({
rules: rulesReducer
});
- Use createStore from the Redux API to create the store
import { createStore, combineReducers } from "redux";
- Give the global reducer as parameter
const store = createStore(rootReducer);
- Install
redux-logger
- Create a constant using createLogger from redux-logger
import { createLogger } from "redux-logger";
const logger = createLogger();
- Use applyMiddleware and compose from Redux API to set up logger
import { applyMiddleware, createStore, combineReducers, compose } from "redux";
const store = createStore(
rootReducer,
undefined,
compose(
applyMiddleware(logger),
)
);
- Export store
export default store;
- Bonus: Install the redux-dev-tools chrome extension and enable it in your code
const store = createStore(
rootReducer,
undefined,
compose(
applyMiddleware(logger),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
- In
RuleList.js
importuseSelector
anduseDispatch
hooks from react-redux
import { useSelector, useDispatch } from "react-redux";
- Use
useSelector
to retrieve all rules in the store
const rules = useSelector(state => state.rules)
- Declare a constant with
useDispatch
const dispatch = useDispatch()
- Use
useEffect
from react to dispatch theloadRules
action
import { loadRules } from "./actions/rules-actions";
useEffect(() => {
dispatch(loadRules());
}, []);
- In
index.js
, provide the store to the application with the Provider component
import { Provider } from "react-redux";
import store from "./store/app-store";
const reactElement = (
<Provider store={store}>
<RuleList />
</Provider>
);
- In the actions folder, create a file named
likes-actions.js
- Create a function named
doLike
, which accepts one argument, the rule identifier
const doLike = id => {
};
- Return an action named
DO_LIKE
, containing the rule identifier
return {
type: DO_LIKE,
payload: id
};
- Export the function
export const doLike = () => {
...
}
- Export the action name as a constant
export const DO_LIKE = "DO_LIKE";
- In the same file, create a function named
doDislike
, just likedoLike
doDislike
function should look like this:
export const DO_DISLIKE = "DO_DISLIKE";
export const doDislike = id => {
return {
type: DO_DISLIKE,
payload: id
};
};
- In
rules-reducer.js
, add a case to handle DO_LIKE
case DO_LIKE: {
}
- Find the rule whose identifier is given to the action
const index = state.findIndex(rule => rule.id === action.payload);
- Create a copy of the rule and increment the likes
const newRule = {
...state[index]
};
newRule.likes += 1;
- Return a copy of the state with
newRule
const newRules = [...state];
newRules[index] = newRule;
return newRules;
- In the same file, handle
DO_DISLIKE
, just likeDO_LIKE
- The case should look like this:
case DO_DISLIKE: {
const index = state.findIndex(rule => rule.id === action.payload);
const newRule = {
...state[index]
};
newRule.dislikes += 1;
const newRules = [...state];
newRules[index] = newRule;
return newRules;
}
- Refactor
Rule.js
to passLikeBtn
rule id
const Rule = ({ rule: { id, title, description, tags } }) => {
...
<LikeBtn type="up" ruleID={id} />
<LikeBtn type="down" ruleID={id} />
...
}
- In
LikeBtn
component, refactor to accept rule id as props
const LikeBtn = ({ type, ruleID }) => {
...
}
- Provide the rule to
doLike
anddoDislike
functions
import { doLike, doDislike } from "./actions/likes-actions";
- Import redux hooks
import { useSelector, useDispatch } from 'react-redux'
- Get all rules with
useSelector
and current rule identifier
const rules = useSelector(state => state.rules)
const rule = rules.find(rule => rule.id === ruleID);
const counter = type === "up" ? rule.likes : rule.dislikes;
- Update
increment
function to dispatch doLike and doDislike
const increment = () => {
if (type === "up") dispatch(doLike(ruleID));
else dispatch(doDislike(ruleID));
}
- LikeBtn with redux should look like:
const LikeBtn = ({ type, ruleID }) => {
const dispatch = useDispatch()
const isUp = () => type === "up";
const increment = () => {
if (isUp()) dispatch(doLike(ruleID));
else dispatch(doDislike(ruleID));
};
const title = type === "up" ? "+1" : "-1";
const rules = useSelector(state => state.rules)
const rule = rules.find(rule => rule.id === ruleID);
const counter = isUp() ? rule.likes : rule.dislikes;
return (
<button className="btn btn-default" title={title} onClick={increment}>
{counter} <i className={`glyphicon glyphicon-thumbs-${type}`}></i>
</button>
);
};
- Check that application works well
// Array-based state
import {
FETCH_RULES,
FETCH_RULE,
CREATE_RULE,
EDIT_RULE,
DELETE_RULE
} from "../actions/types";
const rulesReducer = (state=[], action) => {
switch(action.type) {
case FETCH_RULES:
return action.payload;
case FETCH_RULE:
return [...state, action.payload];
case CREATE_RULE:
return [...state, action.payload];
case EDIT_RULE:
const index = state.find(rule => rule.id === action.payload.id);
const newRules = [...state];
newRules[index] = action.payload;
return newRules;
case DELETE_RULE:
const newRules = state.filter(rule => rule.id !== action.payload.id);
return newRules;
default: return state;
}
}
// Object-based state
import {
FETCH_RULES,
FETCH_RULE,
CREATE_RULE,
EDIT_RULE,
DELETE_RULE
} from "../actions/types";
const rulesReducer = (state={}, action) => {
switch(action.type) {
case FETCH_RULES:
return action.payload
case FETCH_RULE:
return { ...state, [action.payload.id]: action.payload };
case CREATE_RULE:
return { ...state, [action.payload.id]: action.payload };
case EDIT_RULE:
return { ...state, [action.payload.id]: action.payload };
case DELETE_RULE:
return { ...state, [action.payload.id]: undefined };
default: return state;
}
}
- The server provides a REST API with the following endpoints:
- GET /rest/rules : Get all the rules.
- GET /rest/rules/:id : Get the rule with the id specified in the URL.
- POST /rest/rules : Create a new rule.
- PUT /rest/rules/:id : Update rule with the id specified in the UR
- In order to increment "likes" and "dislikes", the server also provides the following endpoints:
- POST /rest/rules/:id/likes : Increment "likes" number for the rule identified with the id in the URL.
- POST /rest/rules/:id/dislikes : Increment "dislikes" number for the rule identified with the id in the URL.
- To start the server, open a new terminal and run the following command:
cd server
npm install
npm start
- To proxify requests to a particular host and prevent cross-origin (CORS) errors when calling the backend, install
http-proxy-middleware
- Refer to official react documentation here
npm install http-proxy-middleware --save-dev
- Create a file named
setupProxy.js
at the root of the src folder - Fill this file with the following content
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function(app) {
app.use(
'/rest',
createProxyMiddleware({
target: 'http://localhost:4000',
changeOrigin: true,
})
);
};
- Restart
create-react-app
- Test that
http://localhost:3000/rest/rules
andhttp://localhost:4000/rest/rules
return the same thing
- Install
redux-thunk
to handle asynchronous actions
npm install redux-thunk
- Configure it when creating the store
import thunk from "redux-thunk";
...
compose(
applyMiddleware(thunk, logger),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
...
- Install
axios
npm install axios
- To display rules to users, we will not import the rules from a JS file anymore, we will call the REST API instead
- In
rules-action.js
, replace import ofdata.json
withaxios
import axios from "axios";
- In
loadRules
, add a try / catch block
try {
...
} catch (error) {
...
}
- In try block, use axios to call
/rest/rules
URL
try {
const { data: rules } = await axios.get("/rest/rules");
}
- Dispatch a
RULES_LOADED
action once data received
try {
...
dispatch({
type: RULES_LOADED,
payload: rules
})
}
- The function must return a function in order to dispatch the action manually thanks to redux-thunk
export const loadRules = () => async dispatch => {
try {
const { data: rules } = await axios.get("/rest/rules");
dispatch({
type: RULES_LOADED,
payload: rules
})
}
...
};
- Handle errors from the catch block by logging the error
catch (error) {
console.log(error);
}
- Check that the application is working well
- In
likes-actions.js
, import axios - In
doLike
function, use axios to call/rest/rules/:id/likes
URL in a try block
try {
await axios.post(`/rest/rules/${id}/likes`)
}
- Dispatch
DO_LIKE
action once data received
dispatch({
type: DO_LIKE,
payload: id
})
- The function must return a function in order to dispatch the action manually thanks to redux-thunk
export const doLike = id => async dispatch => {
try {
...
} catch (error) {
...
}
};
- Handle errors from the catch block by logging the error
catch (error) {
console.log(error);
}
- Update
doUnlike
function, likedoLike
. It should look like this:
export const doDislike = id => async dispatch => {
try {
await axios.post(`/rest/rules/${id}/dislikes`);
dispatch({
type: DO_DISLIKE,
payload: id
})
} catch (error) {
console.log(error);
}
};
// Async action creator
import axios from "axios";
export const RULES_LOADED = "RULES_LOADED";
export const loadRules = () => {
return async dispatch => {
try {
const response = await axios.get("/rest/rules");
dispatch({
type: RULES_LOADED,
payload: response.data
});
} catch (error) {
console.log(error);
}
};
};
// Async action creator
import axios from "axios";
export const RULES_LOADED = "RULES_LOADED";
export const loadRules = () => async dispatch => {
try {
const response = await axios.get("/rest/rules");
dispatch({
type: RULES_LOADED,
payload: response.data
});
} catch (error) {
console.log(error);
}
};
// Using the Effect Hook
import React, { Fragment, useEffect } from "react";
import { connect } from "react-redux";
import Rule from "./Rule";
import { loadRules } from "./actions/rules-actions";
const RuleList = ({ rules, loadRules }) => {
useEffect(() => {
loadRules();
}, []);
const newRules = (rules || []).map(rule => (
<Rule key={rule.id} pRule={rule} />
));
return <Fragment>{newRules}</Fragment>;
};
const mapStateToProps = ({ rules }) => ({
rules
});
const mapDispatchToProps = {
loadRules
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(RuleList);
- Install the router
npm install react-router-dom
- In
index.js
, import theBrowserRouter
andRoute
from react-router-dom
import { BrowserRouter, Route } from "react-router-dom";
- Define a route that will instantiate a
RuleList
on / path.
<Route path="/" component={RuleList} />
- Use
BrowserRouter
to wrap allRoutes
components
<BrowserRouter>
<Route path="/" component={RuleList} />
</BrowserRouter>
- Check that the application is still working
- To create a navigation bar component, create a file named
Header.js
- Create a class named
Header
(default export) and implement render method
const Header = () => {
return ();
};
export default Header;
- Copy HTML code from
resources/navigation.html
file into the return - Be careful with JSX (Change class attributes by className)
- Import Link component from the react-router-dom library
import { Link } from "react-router-dom";
- Replace links ( tag) with the Link component
return (
<nav className="navbar navbar-default">
<div className="navbar-header">
<button type="button" className="navbar-toggle">
<span className="sr-only">Toggle navigation</span>
<span className="icon-bar" />
<span className="icon-bar" />
<span className="icon-bar" />
</button>
<Link to="/" className="navbar-brand brand">
Developers rules
</Link>
</div>
<div className="collapse navbar-collapse">
<ul className="nav navbar-nav">
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/new">New</Link>
</li>
</ul>
</div>
</nav>
);
- To create application layout, create a new file
Layout.js
- Create and export a new function called
Layout
Layout
should display the navigation menu (Header component)
return (
<div>
<Header />
<div className="container-fluid">
<div className="container">
</div>
</div>
</div>
);
- In
index.js
, change the router configuration to useLayout
component on / path
<Route path="/" component={Layout} />
- Back in
Layout.js
, define a route that match the exact / path and instantiate a RuleList when active
...
<div className="container">
<Route exact path="/" component={RuleList} />
</div>
...
- Check that the application is working well
- Inside the src folder, create a file named
RuleForm.js
- In this file, display the form with the HTML code in
resources/edition.html
file - Create a file named
RuleTitleField.js
- Externalize the field that displays the title in a component named
RuleTitleField
const RuleTitleField = ({ title }) => {
return (
<div className="form-group">
<label className="control-label" htmlFor="rule-title">
Title
</label>
<input
type="text"
className="form-control"
id="rule-title"
placeholder="Title"
defaultValue={title}
/>
</div>
);
};
- Do the same for the description with a component named
RuleDescriptionField
RuleDescriptionField
should look like:
const RuleDescriptionField = ({ description}) => {
return (
<div className="form-group">
<label className="control-label" htmlFor="rule-desc">
Description
</label>
<textarea
className="form-control"
id="rule-desc"
placeholder="Description"
defaultValue={description}
/>
</div>
);
};
- In
RuleForm.js
, useRuleTitleField
andRuleDescriptionField
<form>
<RuleTitleField />
<RuleDescriptionField />
<button type="submit" className="btn btn-primary pull-right">
Submit
</button>
</form>
- In
Layout.js
, add a new route with/new
path to display the form to add rules
import RuleForm from "./RuleForm";
...
<Route path="/new" component={RuleForm} />
- In
Layout.js
file, add a new route path for edit. (id is a dynamic value depending on the rule to update)
<Route path="/edit/:id" component={RuleForm} />
- Using the
Link
component, update theRule
component to navigate to the form by providing the rule identifier
<Link to={`/edit/${id}`} className="btn btn-primary" title="Update">
<i className="glyphicon glyphicon-pencil"></i>
</Link>
- Check that the application is working well
- In
RuleForm.js
, usematch
props to get the id
const id = Number(match.params.id);
- Update the panel title, if there's an id param, it should be Edit Rule. Otherwise it should be New Rule
<h3 className="panel-title">{id ? "Edit rule" : "New rule"}</h3>
- Connect component to the redux store and get all rules with
useSelector
const dispatch = useDispatch()
useEffect(() => {
dispatch(loadRules());
}, []);
const rules = useSelector(state => state.rules);
- Get the rule which corresponds to the id from params
const id = Number(match.params.id);
const rule = rules.find(rule => rule.id === id);
- Add a default values if no rule is found
const { title = "", description = "" } = rule || {};
- Pass the
title
anddescription
property toRuleTitle
andRuleDescription
<RuleTitleField title={title} />
<RuleDescriptionField description={description} />
- Check that the application is working well
- Create a new file
NotFound.js
- In this file, create a function Rule and export it by default
- Implement the return JSX
const NotFound = () => {
return (
<div>
404 Page Not Found.
</div>
);
};
- In
Layout.js
, importSwitch
from react-router-dom
import { Switch } from "react-router-dom"
- Wrap all Route components with the Switch component
<Switch>
<Route exact path="/" component={RuleList} />
<Route path="/new" component={RuleForm} />
<Route path="/edit/:id" component={RuleForm} />
</Switch>
- To implement the 404 page, create a new route that renders
NotFound
. You do not have to pass in a path
<Route path="/edit/:id" component={NotFound} />
- Check that the application is working well
- Install formik library
npm install formik
- Import the Formik components in the
RuleForm.js
file
import { Formik, Form, Field } from "formik";
- Wrap the element using Formik component
<Formik onSubmit={values => console.log(values)}>
<form>
<RuleTitleField title={title} />
<RuleDescriptionField description={description} />
...
</form>
</Formik>
- Replace element with Formik
<Formik
onSubmit={values => console.log(values)}
>
{props => {
return (
<Form>
<RuleTitleField title={title} />
<RuleDescriptionField description={description} />
...
</Form>
)
}}
</Formik>
- Use the Formik
Field
component, this component needs 2 properties -name
that has to matches the key in the rule object andcomponent
that renders the whole field
<Form>
<Field name="title" component={RuleTitleField} />
<Field name="description" component={RuleDescriptionField} />
...
</Form>
- Declare
initialValues
required to prefill the fields for edit
const initialValues = { id, title, description };
- In the Formik component, add a property
initialValues
with intialValues we just created
<Formik
onSubmit={values => console.log(values)}
initialValues={initialValues}
>
...
</Formik>
- In
RuleTitleField
, update props to receive field props from Formik
const RuleTitleField = ({ field }) => {
return (
...
<input
{...field}
type="text"
className="form-control"
id="rule-title"
placeholder="Title"
/>
...
);
};
- Do the same for
RuleDescriptionField
- Check that the form fields are prefilled when editing a rule
- Validation rules:
- Title:
- Mandatory
- Up to 50 characters
- Description:
- Optional
- If filled: At least 5 characters
- Up to 100 characters
- Title:
- Formik component accepts a
validationSchema
property that works with Yup. - Install Yup
npm install yup
- In
RuleForm.js
, create avalidationSchema
with a Yup object with the validation rules and error message
import * as Yup from "yup";
const validationSchema = Yup.object().shape({
title: Yup.string()
.max(50)
.required(),
description: Yup.string()
.min(5)
.max(100)
});
- In the Formik component, add a property
validationSchema
<Formik
onSubmit={values => console.log(values)}
initialValues={initialValues}
validationSchema={validationSchema}
>
...
</Formik>
- Add custom error messages in
validationSchema
const validationSchema = Yup.object().shape({
title: Yup.string()
.max(50, "The title must be shorter than 50 characters")
.required("Title is required"),
description: Yup.string()
.min(5, "The description must be longer than 5 characters")
.max(100, "The description must be shorter than 100 characters")
});
- In
RuleTitleField
, use theErrorMessage
component from Formik to display an error message
import { ErrorMessage } from "formik";
...
<ErrorMessage component="span" className="help-block" name="title" />
- Add the class
has-error
if there's a validation error
const RuleTitleField = ({ field, form }) => {
const { name } = field;
const { touched, errors } = form;
return (
<div className={`form-group ${touched[name] && errors[name] ? "has-error" : ""}`}>
...
</div>
);
};
- Do the same for
RuleDescriptionField
- In
RuleForm.js
disable the submit button if there's a validation error
const isObjectEmpty = obj => !Object.entries(obj).length;
...
{({ errors, dirty, isSubmitting }) => (
...
<button
type="submit"
className="btn btn-primary pull-right"
disabled={isSubmitting || !isObjectEmpty(errors) || !dirty}
>
Submit
</button>
)
}
...
- Try to trigger the errors to check that the validation is working
References
- In
rules-actions.js
, create an action creator foraddRule
that accepts rule as the first parameter
export const addRule = (rule) => async dispatch => {
...
};
- Add a try / catch block and use axios to post to /rest/rules URL
try {
const response = await axios.post("/rest/rules", rule);
} catch (error) {
console.log(error);
}
- Dispatch the response from the server.
try{
...
dispatch({
type: RULES_ADDED,
payload: response.data
});
}
- Accept history as a second param and use it to redirect user back to '/' after dispatch
export const addRule = (rule, history) => async dispatch => {
...
catch (error) {
console.log(error);
}
history.push("/");
};
- Export the action name as a constant
export const RULES_ADDED = "RULES_ADDED";
- Create an action creator for
updateRule
likeaddRule
- It should look like:
export const RULES_UPDATED = "RULES_UPDATED";
export const updateRule = (rule, history) => async dispatch => {
try {
const response = await axios.put(`/rest/rules/${rule.id}`, rule);
dispatch({
type: RULES_UPDATED,
payload: response.data
});
} catch (error) {
console.log(error);
}
history.push("/");
}; };
- In
RuleForm.js
, import action creators
import { addRule, updateRule } from "./actions/rules-actions";
- Refactor to expose
history
props
const RuleForm = ({ match, history }) => {
...
}
- Create a function
handleSubmit
to dispatch the appropriate action creator
const handleSubmit = values => {
const submitActionCreator = id ? updateRule : addRule;
dispatch(submitActionCreator(values, history));
};
- Define a
onSubmit
event on the form that callhandleSubmit
<Formik
onSubmit={handleSubmit}
initialValues={initialValues}
validationSchema={validationSchema}
>
- To handle the resulting actions in the reducer, import
RULES_ADDED
andRULES_UPDATED
import {
RULES_LOADED,
RULES_ADDED,
RULES_UPDATED
} from "../actions/rules-actions";
- Add a new case for
RULES_ADDED
, where the created rule must be appended to the current state
case RULES_ADDED: {
return [...state, action.payload];
}
- Add a new case for
RULES_UPDATED
, where the updated rule must be replaced in the current state
case RULES_UPDATED: {
const index = state.find(rule => rule.id === action.payload.id);
const newRules = [...state];
newRules[index] = action.payload;
return newRules;
}
- Check that both rule creation and edition are working well
// React form
import React, { Fragment } from "react";
import RuleTitleField from "./RuleTitleField";
const RuleForm = ({ rule }) => {
const { id, title, description } = rule;
return (
<Fragment>
<form>
<RuleTitleField title={title} />
<button type="submit">Submit</button>
</form>
</Fragment>
);
};
// Convert react form to formik
import React, { Fragment } from "react";
import { Field, Form, Formik } from "formik";
const RuleForm = ({ rule }) => {
const { id, title, description } = rule;
const initialValues = {
id,
title: title || "",
description: description || ""
};
return (
<Fragment>
<Formik
onSubmit={values => console.log(values)}
initialValues={initialValues}
render={props => (
<Form>
<Field type="text" name="title" component={RuleTitleField} />
<button type="submit">Submit</button>
</Form>
)}
>
</Fragment>
);
};
// Convert react form to formik
import React, { Fragment } from "react";
import { Field, Form, Formik } from "formik";
import * as Yup from "yup";
const RuleForm = ({ rule }) => {
const { id, title, description } = rule;
const initialValues = {
id,
title: title || "",
description: description || ""
};
const validationSchema = Yup.object().shape({
title: Yup.string()
.max(50, "The title must be shorter than 50 characters")
.required("Title is required"),
description: Yup.string()
.min(5, "The description must be longer than 5 characters")
.max(100, "The description must be shorter than 100 characters")
});
return (
<Fragment>
<Formik
onSubmit={values => console.log(values)}
initialValues={initialValues}
validationSchema={validationSchema}
render={props => (
<Form>
<Field type="text" name="title" component={RuleTitleField} />
<button type="submit">Submit</button>
</Form>
)}
>
</Fragment>
);
};
export default App;
- Identify wasted renders
- Chrome -> Right-click -> Inspect -> ... -> More tools -> Rendering -> Paint flashing
- Extract frequently updated regions into isolated components
- Use pure components when appropriate
- Avoid passing new objects as props
- Use the production build
- npm build -> cd build -> npm start
- Employ code splitting
// React.memo
import React, { memo } from 'react';
const MyComponent = memo(({username}) => {
return (
<div className="wrapper">
<p>{username}</p>
</div>
)
})
// React.PureComponent: if the state and props are the same
import React, { PureComponent } from 'react'
export default class MyComponent extends PureComponent {
render() {
return (
<div className="wrapper">
<p>{this.props.username}</p>
</div>
)
}
}
// Memoize expensive calculations - function
// `someProp` will be recalculated only when `item` changes
const MyComponent = ({ item }) => {
const someProp = useMemo(() => heavyCalculation(item), [item]);
return <AnotherComponent someProp={someProp} />
}
// Avoid inline objects
// Don't do this!
const MyComponent = props => {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
}
// Do this instead :)
const styles = { margin: 0 };
const MyComponent = props => {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={styles} {...aProp} />
}
// Avoid anonymous functions
// Don't do this!
const MyComponent = ({ id }) => {
return <AnotherComponent onChange={() => console.log(id)} />
}
// Do this instead :)
const MyComponent = ({ id }) => {
const handleChange = () => console.log(id);
return <AnotherComponent onChange={handleChange} />
}
// React.lazy: Code-Splitting with Suspense
import React, {lazy, Suspense} from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
const MyComponent = () => (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
)
export default MyComponent;
// Tweak CSS instead of forcing a component to mount & unmount
// Avoid this is the components are too "heavy" to mount/unmount
const MyComponent = props => {
const [view, setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// Do this instead if you' re opting for speed & performance gains
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
const MyComponent = props => {
const [view, setView] = useState('view1');
return (
<React.Fragment>
<SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
<AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
</React.Fragment>
)
}
// React.Fragments to Avoid Additional HTML Element Wrappers
import React, { Fragment } from 'react'
...
render() {
<Fragment>
<div></div>
<div></div>
</Fragment>
}
// componentDidCatch(error, info) {}
class ErrorBoundary extends Component {
state = { hasError: false };
componentDidCatch(error, info) {
this.setState({ hasError: true });
}
render() {
const { hasError } = this.state
if (hasError) {
return <h1>Something went wrong.</h1>;
}
return <div>All good!</div>
}
}
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
- Learn to code HTML, CSS, and JavaScript with Dash
- javascripting
- The Modern Javascript Tutorial
- JavaScript Guide
- Eloquent JavaScript
- JavaScript For Cats
- React Armory
- CRA vs Next.js vs Gatsby – Comparison and How to Choose One
- Roadmap to becoming a React developer
- All the React Fundamentals in One Place
- React official documentation
- React patterns
- The Road to learn React
- Overreacted
- Enterprise React in 2018–2019
- The (new) React lifecycle methods in plain, approachable language
- React Bootstrap
- Material-UI: A popular React UI framework
- Ant Design - The world's second most popular React UI framework
- PrimeReact - PrimeFaces