Skip to content

chesterheng/react-training

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React JS Training Code Snippets and Reference

Table of Contents

  1. Introduction

    Click to view all steps
  2. JavaScript

  3. Prerequisite

    Click to view all steps
  4. React and JSX setup

    Click to view all steps
  5. State

    Click to view all steps
  6. Tests

    Click to view all steps
  7. Redux

    Click to view all steps
  8. REST Architecture

    Click to view all steps
  9. Routing

    Click to view all steps
  10. Forms

    Click to view all steps
  11. Performance Optimization Techniques

  12. Reference

Introduction

Zenika Singapore

Web Development Market Trends

⬆ back to top

JavaScript

// 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
}

⬆ back to top

Prerequisite

Preparation

  • Create a folder called react-training
  • Unzip resources.zip and server.zip
  • Move unzipped resources and server folder into react-training
  • react-training folder should have this structure
├── resources
│ ├── data.json
│ ├── edition.html
│ ├── navigation.html
│ └── rule.html
└── server
└── ...

⬆ back to top

Installation

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 a client app with create-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.

References

⬆ back to top

React and JSX setup

React Setup

  • 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

⬆ back to top

Bootstrap Setup

  • Install bootstrap
npm install bootstrap@3.x.x --save
  • In index.js file, import bootstrap
import 'bootstrap/dist/css/bootstrap.css';

References

⬆ back to top

First Components

  • 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 by className)
  • 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>
  );
};

References

⬆ back to top

Displaying The List

  • 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>
}
// 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 of newRules
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 to className)
  • 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

⬆ back to top

Externalize a component

  • 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 new Rule 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

References

⬆ back to top

Custom CSS

  • Create a Rule.css file sibling to Rule.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";

⬆ back to top

State

Handle Component State

  • 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

References

⬆ back to top

"likes" feature

  • 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, import LikeBtn 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>

⬆ back to top

Props Validation

  • 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
};

References

⬆ back to top

Tests

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";

⬆ back to top

First test for Rule component

  • 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

References

⬆ back to top

Second test for RuleList component

  • 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

⬆ back to top

Fake click event

  • Create a new file for LikeBtn tests and import relevant dependencies
  • Besides render, import fireEvent 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");
});

⬆ back to top

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");
  });
});

References

⬆ back to top

Redux

Load Rules

  • For the sake of separation of concerns, we will load the rules in a dedicated action instead of importing them in index.js

⬆ back to top

Action Creators

  • 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);
  });
});

References

⬆ back to top

Reducer

  • 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);
  });
}); 

References

⬆ back to top

Store

  • 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__()
  )
);

References

⬆ back to top

Update React Components

  • In RuleList.js import useSelector and useDispatch 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 the loadRules 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>
);

References

⬆ back to top

Likes and Dislikes Action Creators

  • 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 like doLike
  • doDislike function should look like this:
export const DO_DISLIKE = "DO_DISLIKE";
export const doDislike = id => {
  return {
    type: DO_DISLIKE,
    payload: id
  };
}; 

⬆ back to top

Likes and Dislikes Reducer

  • 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 like DO_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;
}

⬆ back to top

Likes and Dislikes Update React Component

  • Refactor Rule.js to pass LikeBtn 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 and doDislike 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

References

// 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;
  }
}

⬆ back to top

Redux Cycle:

Redux Cycle

React Redux:

React Redux ⬆ back to top

REST Architecture

REST Overview

  • 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 and http://localhost:4000/rest/rules return the same thing

⬆ back to top

Dependencies

  • 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

References

⬆ back to top

Fetch Data

  • 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 of data.json with axios
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

References / tools

⬆ back to top

Handle Likes & Dislikes

  • 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, like doLike. 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);
  }
};

⬆ back to top

Update Backend DB and Frontend Redux Store:

// 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);

References

⬆ back to top

Routing

Setup The Router

  • Install the router
npm install react-router-dom
  • In index.js, import the BrowserRouter and Route 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 all Routes components
<BrowserRouter>
  <Route path="/" component={RuleList} />
</BrowserRouter>
  • Check that the application is still working

References

⬆ back to top

Navigation Bar

  • 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 use Layout 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

⬆ back to top

Navigate to Rule Creation Page

  • 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, use RuleTitleField and RuleDescriptionField
<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} />

⬆ back to top

Navigate to Rules Modification Page

  • 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 the Rule 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, use match 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 and description property to RuleTitle and RuleDescription
<RuleTitleField title={title} />
<RuleDescriptionField description={description} />
  • Check that the application is working well

⬆ back to top

Bonus: 404 Page

  • 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, import Switch 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

Forms

Formik

  • Install formik library
npm install formik

References

⬆ back to top

Form Binding

  • 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 and component 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

⬆ back to top

Form Validation

  • Validation rules:
    • Title:
      • Mandatory
      • Up to 50 characters
    • Description:
      • Optional
      • If filled: At least 5 characters
      • Up to 100 characters
  • Formik component accepts a validationSchema property that works with Yup.
  • Install Yup
npm install yup
  • In RuleForm.js, create a validationSchema 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 the ErrorMessage 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

References

⬆ back to top

Submission

  • In rules-actions.js, create an action creator for addRule 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 like addRule
  • 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 call handleSubmit
<Formik
  onSubmit={handleSubmit}
  initialValues={initialValues}
  validationSchema={validationSchema}
>
  • To handle the resulting actions in the reducer, import RULES_ADDED and RULES_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

⬆ back to top

// 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;

⬆ back to top

Performance Optimization Techniques

A checklist for eliminating common React performance issues

  • 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>

Windowing - Virtualize Long Lists -

⬆ back to top

Reference

Frontend Resources

JavaScript Learning Resources - Beginner

JavaScript Learning Resources - Advanced

React Learning Resources

React 3rd Party Libraries

React UI Framework

React Charts

⬆ back to top