Học ReactJS Full Đầy Đủ Nhất

Bài 20: ReactJS - Bản rút gọn(Quick guide - P3)

I- List và Keys

1. Lists trong React 

Trong Javascript, bạn muốn tạo một mảng mới từ một mảng có sẵn bằng cách chế biến từng phần tử của mảng ban đầu để tạo ra phần tử tương ứng của mảng mới, bạn có thể sử dụng phương thức map().Để đơn giản hãy xem ví dụ, bạn có một mảng các số tự nhiên, chẳng hạn [1, 2 , 5], bạn muốn tạo ra một mảng khác bằng cách nhân mỗi phần tử của mảng ban đầu với 10.
var arr1 = [1, 2, 5];
 
console.log(arr1); // --> [1, 2, 5]
 
var arr2 = arr1.map( e  => e * 10 );
 
console.log(arr2); // --> [10, 20, 50]
Trong JSX bạn cũng có thể làm tương tự, từ một mảng các đối tượng (Object), tạo ra một mảng mới chứa các thẻ (Tag):
var array1 = [        
  { empId: 1, fullName: "Trump", gender: "Male" },        
  { empId: 2, fullName: "Ivanka", gender: "Female" },        
  { empId: 3, fullName: "Kushner", gender: "Male" }        
];
 
var array2 = array1.map (
    e =>
    <Emloyee fullName={e.fullName} gender={e.gender} />  
);
Ví dụ 1 :Trong ví dụ này, tôi có 1 mảng chứa thông tin của các nhân viên (employee), và tôi sẽ hiển thị thông tin của các nhân viên này trên giao diện, giống như hình minh họa dưới đây:
App,jsx
// Employee Component
class Employee extends React.Component {
  render() {
    return (
      <li className="employee">
        <div>
          <b>Full Name:</b> {this.props.fullName}
        </div>
        <div>
          <b>Gender:</b> {this.props.gender}
        </div>
      </li>
    );
  }
}
 
// EmployeeList Component
class EmployeeList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      employees: [
        { empId: 1, fullName: "Trump", gender: "Male" },
        { empId: 2, fullName: "Ivanka", gender: "Female" },
        { empId: 3, fullName: "Kushner", gender: "Male" }
      ]
    };
  }
 
  render() {
    // Array of <Employee>
    var listItems = this.state.employees.map(e => (
      <Employee fullName={e.fullName} gender={e.gender} />
    ));
    return (
        <ul className="employee-list">
           {listItems}
        </ul>
      );
  }
}
 
// Render
ReactDOM.render(<EmployeeList />, document.getElementById("employeelist1"));
lists-example.html
<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8">
      <title>ReactJS Lists</title>
      <script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom@16.4.2/umd/react-dom.production.min.js"></script>
      <script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
 
      <style>
         .employee-list  {
           border:1px solid #cbbfab;
           list-style-type : none;
           padding: 5px;
           margin: 5px;
         }
         .employee {
            border: 1px solid #ccc;
            margin: 5px;
            padding: 5px;
         }
      </style>
   </head>
   <body>
      <h3>React Lists:</h3>
 
      <div id="employeelist1"></div>
 
      <script src="lists-example.jsx" type="text/babel"></script>
   </body>
</html>
Ví dụ 2 :Ở đây mình sẽ tiến hành khởi tạo một lists các items.
import React from "react";
 
function ListComponent(props) {
  const myList = ["php", "javascript", "python", "C++"];
  const listItems = myList.map((item) =>
    <li>{item}</li>
  );
   
  return (
    <ul>{listItems}</ul>
  );
}
 
export default ListComponent
và trình duyệt sẽ hiện thị kết quả:
  • php
  • javascript
  • python
  • C++
Việc khởi tạo các lists trong React rất đơn giản, điều mình muốn tập trung nhấn mạnh trong bài viết này đó là về keys mà mình sẽ đề cập bên dưới.

2. React Keys

Trong quá trình làm việc với React, chúng ta phải thao tác với danh sách(lists) rất nhiều như danh sách các ảnh, danh sách các item trong giỏ hàng,...Khi các lists này có hàng tá các items thì React rất khó có thể kiểm soát được items. Bởi vậy chúng ta cần phải chỉ định cho nó một key để định danh.Keys (Khóa) giúp React phân biệt được các item trong một danh sách. Nó giúp React quản lý các item có thay đổi, các item mới được thêm vào, hoặc các item đã bị loại bỏ khỏi danh sách.Từ một mảng các đối tượng, bạn tạo ra một mảng mới chứa các thẻ (Tag), các thẻ này nên có thuộc tính key, và giá trị của chúng không được phép giống nhau.
keys-example.jsx
// Employee Component
class Employee extends React.Component {
  render() {
    return (
      <li className="employee">
        <div>
          <b>Full Name:</b> {this.props.fullName}
        </div>
        <div>
          <b>Gender:</b> {this.props.gender}
        </div>
      </li>
    );
  }
}
 
// EmployeeList Component
class EmployeeList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      employees: [
        { empId: 1, fullName: "Trump", gender: "Male" },
        { empId: 2, fullName: "Ivanka", gender: "Female" },
        { empId: 3, fullName: "Kushner", gender: "Male" }
      ]
    };
  }
 
  render() {
    // Array of <Employee>
    var listItems = this.state.employees.map(e => (
      <Employee key={e.empId} fullName={e.fullName} gender={e.gender} />
    ));
    return (
        <ul className="employee-list">
           {listItems}
        </ul>
      );
  }
}
 
// Render
ReactDOM.render(<EmployeeList />, document.getElementById("employeelist1"));
key-example.html
<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="UTF-8">
      <title>ReactJS Lists</title>
      <script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom@16.4.2/umd/react-dom.production.min.js"></script>
      <script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
 
      <style>
         .employee-list  {
           border:1px solid #cbbfab;
           list-style-type : none;
           padding: 5px;
           margin: 5px;
         }
         .employee {
            border: 1px solid #ccc;
            margin: 5px;
            padding: 5px;
         }
      </style>
   </head>
   <body>
      <h3>React Lists:</h3>
 
      <div id="employeelist1"></div>
 
      <script src="keys-example.jsx" type="text/babel"></script>
   </body>
</html>
Nếu bạn chạy ví dụ 2 ở phần thứ 1, React sẽ hiển thị cảnh báo như hình bên dưới:
Để loại bỏ cảnh báo bạn phải chỉ định cho các items trong lists một thuộc tính có tên là key. Chúng ta sẽ sửa ví dụ ở đầu bài thành:
import React from "react";
 
function ListComponent(props) {
  const myList = [
    {
      id : 'p',
      name : 'php'
    },
    {
      id : 'j',
      name : 'javascript'
    },
    {
      id : 'py',
      name : 'python'
    },
    {
      id : 'c',
      name : 'C++'
    },
  ]
 
  //Thêm thuộc tính key vào trong thẻ jsx
  const listItems = myList.map((item) =>
    <li key = {item.id}>{item.name}</li>
  );
 
  return (
    <ul>{listItems}</ul>
  );
}
 
export default ListComponent

3. Một vài lưu ý khi sử dụng Keys

Ở đây mình có một vài lưu ý sử dụng key cho list, các lưu ý này sẽ giúp quá trình làm việc với React không gặp các lỗi không mong muốn.Keys là duy nhấtBạn cần chỉ định các keys này là duy nhất, các keys này không được trùng lặp trong các lists.
const myList = [
    {
      id : 'p',
      name : 'php'
    },
    {
      id : 'j',
      name : 'javascript'
    },
    {
      id : 'p',
      name : 'python'
    },
    {
      id : 'c',
      name : 'C++'
    },
  ]
 
  const listItems = myList.map((item) =>
    <li key = {item.id}>{item.name}</li>
  );
khi các keys này trùng lặp bạn sẽ nhận được cảnh báo :
Warning: Encountered two children with the same key, `p`. 
Keys should be unique so that components maintain their identity across updates. 
Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
Các keys chỉ cần là duy nhất khi so sánh với các anh/chị của nó trong lists chứa chúng.Tránh chỉ định index làm keyTrong một vài trường hợp bạn thường chỉ định giá trị của biến index thành keys như trong ví dú này:
const listItems = myList.map((item, index) =>
  <li key = {index}>{item.name}</li>
);
React khuyên chúng ta không nên sử dụng cách này. Bởi khi bạn thực hiện sắp xếp mảng thì index sẽ thay đổi, React lại phải xác định lại keys môt lần nữa, gây ra giảm hiệu xuất làm việc.Chỉ sử dụng index làm key trong khi:
  • Nếu list của bạn là tĩnh và sẽ không thay đổi.
  • List sẽ không bao giờ được sắp xếp lại.
  • List sẽ không được lọc (thêm / xóa các mục khỏi danh sách).
  • Không có id cho các mục trong list.
Hãy chỉ sử dụng index làm key trong trường hợp đặc biệt này, và lưu ý trong quá trình sử dụng.

4. Các ví dụ :

Ở ví dụ này mình sẽ tạo một phần tử Content với unique(duy nhất) index(i). Hàm map sẽ tạo ra 3 phần tử từ dữ liệu có sẵn. Vì giá trị key cần phải là duy nhất cho mọi phần tử, mình sẽ gán i làm khoá cho mỗi phần tử được tạoApp.jsx
import React from 'react';

class App extends React.Component {
   constructor() {
      super();
		
      this.state = {
         data:[
            {
               component: 'First...',
               id: 1
            },
            {
               component: 'Second...',
               id: 2
            },
            {
               component: 'Third...',
               id: 3
            }
         ]
      }
   }
   render() {
      return (
         <div>
            <div>
               {this.state.data.map((dynamicComponent, i) => <Content 
                  key = {i} componentData = {dynamicComponent}/>)}
            </div>
         </div>
      );
   }
}
class Content extends React.Component {
   render() {
      return (
         <div>
            <div>{this.props.componentData.component}</div>
            <div>{this.props.componentData.id}</div>
         </div>
      );
   }
}
export default App;
main.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';

ReactDOM.render(<App/>, document.getElementById('app'));
Các bạn mở terminal là chay dòng lệnh sau :
npm start
Kết quả hiển thị ở hình dưới :

II- Router

1. Định nghĩa React-Router :

React-Router là một thư viện định tuyến (routing) tiêu chuẩn trong React. Nó giữ cho giao diện của ứng dụng đồng bộ với URL trên trình duyệt. React-Router cho phép bạn định tuyến "luồng dữ liệu" (data flow) trong ứng dụng của bạn một cách rõ ràng. Nó tương đương với sự khẳng định, nếu bạn có URL này, nó sẽ tương đương với Route này, và giao diện tương ứng.Ý tưởng của Router (bộ định tuyến) thực sự rất hữu ích vì bản chất bạn đang làm việc với React, một thư viện Javascript để lập trình các ứng dụng một trang ( Single Page Application). Để phát triển ứng dụng React bạn phải viết rất nhiều Component nhưng lại chỉ cần một tập tin duy nhất để phục vụ người dùng, đó là index.html (Cơ bản là thế).
React Router giúp bạn định nghĩa ra các URL động, và lựa chọn Component phù hợp để hiển thị trên trình duyệt người dùng ứng với từng URL.Để làm việc với router thì đầu tiên bạn cần cài đặt thư viên của nó, cú pháp như sau :
npm install react-router-dom --save

2. Các thành phần trong React-Router

a- BrowserRouter vs HashRouter
  • React-Router cung cấp cho chúng 2 thành phần hay sử dụng đó là BrowserRouter & HashRouter. Hai thành phần này khác nhau ở kiểu URL mà chúng sẽ tạo ra và đồng bộ.
  • BrowserRouter: Được sử dụng phổ biến hơn, nó sử dụng History API có trong HTML5 để theo dõi lịch sử bộ định tuyến của bạn.
  • HashRouter: Sử dụng hash của URL (window.location.hash) để ghi nhớ mọi thứ.
import { BrowserRouter as Router, Route, Link, NavLink } from "react-router-dom";
b - Route :
Route: Định nghĩa một ánh xạ (mapping) giữa một URL và một Component. Điều đó có nghĩa là khi người dùng truy cập theo một URL trên trình duyệt, một Component tương ứng sẽ được rendertrên giao diện.
<Router>
    <div className="App">
        <Route path="/" exact component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
        <Route component={NotFound}/>
    </div>
</Router>
Trong đó :
  • path: Là đường dẫn trên URL.
  • exact: Giúp cho route này này chỉ hoạt động nếu URL trên trình duyệt phù hợp tuyệt đối với giá trị của thuộc tính path của nó.
  • component: Là component sẽ đươc load ra tương ứng với Route đó.
<BrowserRouter>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/topics" component={Topics}/>
</BrowserRouter>
 
 
<HashRouter>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/topics" component={Topics}/>
</HashRouter>
<BrowserRouter>
    ...
    <Route path="/about" component={About}/>
    ...
</BrowserRouter>

http://example.com/about                      ==> OK Work!
http://example.com/about#somthing             ==> OK Work!
http://example.com/about/something            ==> OK Work!
http://example.com/about/a/b                  ==> OK Work!

-------------------

http://example.com/something/about            ==> Not Work!
http://example.com/something/about#something  ==> Not Work!
http://example.com/something/about/something  ==> Not Work!

<HashRouter>
    ...
    <Route path="/about" component={About}/>
    ...
</HashRouter>

http://example.com#/about                      ==> OK Work!
http://example.com#/about/somthing             ==> OK Work!

----------------

http://example.com/something             ==> Not Work!
http://example.com/something#/about      ==> Not Work!
<BrowserRouter>
    ...
    <Route exact path="/about" component={About}/>
    ...
</BrowserRouter>


http://example.com/about                      ==> OK Work!
http://example.com/about#somthing             ==> OK Work!

-------------

http://example.com/about/something            ==> Not Work!
http://example.com/about/a/b                  ==> Not Work!

http://example.com/something/about            ==> Not Work!
http://example.com/something/about#something  ==> Not Work!
http://example.com/something/about/something  ==> Not Work!

<HashRouter>
    ...
    <Route exact path="/about" component={About}/>
    ...
</HashRouter>

http://example.com#/about                      ==> OK Work!

----------------

http://example.com#/about/somthing             ==>  Not Work!
http://example.com/something                   ==> Not Work!
http://example.com/something#/about            ==> Not Work!
c - Link :
Trong HTML thì cặp thẻ để chuyển hướng đó là thẻ thì trong react chúng ta sẽ dử dụng cặp thẻ được import từ React-Router.
<Link to="/about">About</Link>
trong đó:
  • to: Giống như thuộc tính href trong thẻ a.
d - NavLink :
NavLink thì rất giống với Link về cách sử dụng, nhưng NavLink tốt hơn vì nó hỗ trợ thêm một số thuộc tính như là activeClassName và activeStyle 2 thuộc tính này giúp cho khi mà nó trùng khớp thì nó sẽ được active lên và chúng ta có thể style cho nó.
<NavLink exact activeStyle={{
    backgroundColor : 'white',
    color : 'red'
}} to="/" className="my-link">Trang Chu</NavLink>
e - Custom Link :
ở trên ta có thẻ NavLink giúp chúng ta có thêm một thuộc tính nhưng giả sử khi bạn không muốn activeClassName hoặc activeStyle tại thẻ NavLink mà nó lại nằm ở một thẻ bao nó ví dụ như thẻ div hay thẻ li thì sao? sau đây mình sẽ custom lại để có thể sử dụng các class hoặc style ở thẻ bao ngoài của nó.
const MenuLink = ({
    label, // nội dung trong thẻ
    to, // giống như href trong thẻ a
    activeOnlyWhenExact
}) => {
    return (
        <Route 
            path={to}
            exact={activeOnlyWhenExact}
            children={ ({ match }) => { //match la doi tuong xac dinh su trung khop cua URL
                var active = match ? 'active abc' : '';

                return (
                    <li className={`my-li ${active}`}>
                        <Link  to={to} className="my-link">{label}</Link>
                    </li>
                );
            }}
        />
    );
}
f - Đối tượng Match :
Khi bạn muốn lấy một số thông tin ở trên URL thì bạn có thể dùng đối tượng match để lấy dữ liệu về. Tại cấu hình Router ta chỉ cần truyền thêm đối tượng match vào component mà cần sử dụng đối tượng match
{
        path : '/products',
        exact : false,
        main : ({match}) => <Products match={match} />
    }
Khiconsole.log(match)ta sẽ có kết quả như sau.
Trong đối tượng params sẽ chứa các tham số mà ta truyền trên URL.
i - Đối tượng prompt - Xác nhận trước khi chuyển trang
Giả sử khi bạn đang nhập liệu ở form nào đó mà không may click nút back hay chuyển trang thì thôi xong dữ liệu bạn nhập sẽ mất hết để khác phục điều đó ta có đối tượng prompt nó sẽ giúp chúng ta trước khi back hay chuyển trang nó sẽ xác nhận xem là chúng ta có chắc chắn muốn back hay chuyển trang không!Khi muốn sử dụng đối tượng prompt thì chúng ta chỉ cần import nó từ react-router
import {Prompt} from 'react-router-dom';

<Prompt 
    when={true} // true | false
    message={ (location) => (`Ban chac chan muon di den ${location.pathname}`) }
/>
k - Redirect :
  • Chức năng dùng để chuyển trang.
  • Có thể truy xuất thông tin trang trước đó thông qua đối tượng location. Để sử dụng Redirect ta chỉ cần import nó từ react-router.
import { Redirect } from 'react-router-dom';
Khi bạn muốn sử dụng location thì tại cấu hình Router ta chỉ cần truyền thêm đối tượng location vào component mà cần sử dụng đối tượng location.
{
    path : '/login',
    exact : false,
    main : ({location}) => <Login location={location} />
}

3. Các ví dụ :

ví dụ 1 :Trước hết bạn cần cài đặt công cụ create-react-app,(ở các bài trước đã cài đặt) và tạo một dự án React với tên react-router-basic-app:
# Create project named 'react-router-basic-app':
 
create-react-app react-router-basic-app
Xóa hết nội dung của 2 tập tinApp.css & App.js, chúng ta sẽ viết code cho 2 tập tin này.App.css 
.main-route-place {
  border: 1px solid  #bb8fce;
  margin:3px;
  padding: 5px;
}
 
.secondary-route-place {
  border: 1px solid #a2d9ce;
  margin: 5px;
  padding: 5px;
}
App.js
import React from "react";
import { BrowserRouter, Route, Link } from "react-router-dom";
 
import './App.css';
 
class App extends React.Component {
 
  render()  {
    return  (
      <BrowserRouter>
        <div>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/topics">Topics</Link>
            </li>
          </ul>
 
          <hr />
          <div className="main-route-place">
            <Route exact path="/" component={Home} />
            <Route path="/about" component={About} />
            <Route path="/topics" component={Topics} />
          </div>
        </div>
      </BrowserRouter>
    );
  }
 
}
 
class Home extends React.Component {
 
  render()  {
    return (
      <div>
        <h2>Home</h2>
      </div>
    );
  }
}
 
class About  extends React.Component {
  render() {
    return (
      <div>
        <h2>About</h2>
      </div>
    );
  }
}
 
class Topics extends React.Component {
  render( ) {
    return (
      <div>
        <h2>Topics</h2>
        <ul>
          <li>
            <Link to={`${this.props.match.url}/rendering`}>
              Rendering with React
            </Link>
          </li>
          <li>
            <Link to={`${this.props.match.url}/components`}>Components</Link>
          </li>
          <li>
            <Link to={`${this.props.match.url}/props-v-state`}>
              Props v. State
            </Link>
          </li>
        </ul>
 
        <div className="secondary-route-place">
          <Route
            path={`${this.props.match.url}/:topicId`}
            component={Topic} />
          <Route
            exact
            path={this.props.match.url}
            render={() =>
              <h3>
                Please select a topic.
              </h3>
            }
            />
        </div>
      </div>
    );
  }
}
 
class Topic extends React.Component {
  render()  {
    return (
      <div>
        <h3>
          {this.props.match.params.topicId}
        </h3>
      </div>
    );
  }
}
 
export default App;
index.html 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
  
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
     
  </body>
</html>
Chạy ứng dụng của xem kết quả trên trình duyệt:
npm start

III- Flux

1 . Có nên dùng Flux không ?

Nếu ứng dụng của bạn phải làm việc với dữ liệu động thì câu trả lời là có, bạn nên dùng Flux.Nếu ứng dụng của bạn chỉ làm việc với dữ liệu tĩnh, không chia sẻ các trạng thái của ứng dụng, không lưu trữ hoặc cập nhật dữ liệu, thì câu trả lời là không bởi vì Flux không giúp ích gì cho bạn trong hoàn cảnh này cả 

2. Flux là gì ?

  • Flux và ReactJS cùng được tạo ra bởi Facebook để giải quyết một số những vấn đề rất đặc thù của bản thân Facebook
  • Trước khi có Flux và React thì hệ thống model và view của facebook như sau :
  • Do tương tác của người dùng là ở View nên đôi khi View cần phải update ngược lại dữ liệu ở Model khác. Điều này dẫn đến hiệu ứng "thác nước" khi một hành động của người dùng trigger một loạt các thay đổi, mỗi thay đổi lại có thể trigger ra một loạt các thay đổi khác, và tất cả các thay đổi là quá trình bất đồng bộ. Và nhìn vào mô hình trên sẽ khó khăn rất lớn khi debug nếu có lỗi xảy ra
Giải pháp: Luồng dữ liệu một hướng và Flux đã ra đời:
  1. Flux là architecture hơn là một framework hay một library
  2. Sử dụng cho việc xây dựng client-side web applications
  3. Nó bổ sung khả năng kết hợp các view components của React bằng việc sử dụng một unidirectional data flow(dữ liệu một chiều)
  • View : là các React Component.
  • Store: là nơi chứa, xử lý dữ liệu cho App.
  • Action: là nơi khởi tạo action.
  • Dispatcher: là nơi phát action.

a - Action Creators

  • Action Creators có nhiệm vụ tạo ra các action, là bước đầu tiên trong luồng mà các thay đổi và tương tác đều đi qua. Bất cứ khi nào trạng thái của web app hay là render của view thay đổi thì đầu tiên sẽ là một hành động được tạo ra.
  • Sau khi The Action Creator tạo ra action, anh ta sẽ gửi nó cho The dispatcher.

b - The dispatcher

  • The dispatcher về cơ bản là một tập hợp rất nhiều các callbacks. Khi một action được gửi đến The dispatcher, nó sẽ gửi chúng đến store tương ứng theo quy tắc đồng bộ.

c - The Store

  • The store là nơi chứa toàn bộ các trạng thái và logic chuyển trạng thái của app.Tất cả mọi thay đổi trạng thái đều được thực thi trực tiếp ở đây.
  • Mỗi khi bạn muốn thay đổi một trạng thái, bạn cần phải tạo một action, submit vào The action creator, đi qua The dispatcher rồi mới được The store xử lý.
  • Một store sẽ nhận rất nhiều các action, và trong store thường sẽ có một cấu trúc switch để quyết định xem có cần phải quan tâm đến action hay không.

d - The controller view and the view

  • The view có nhiệm vụ thu nhận lệnh thay đổi trạng thái và render hiển thị, cũng như nhận input từ người dùng.
  • Controller view như là một nhà quản lý cấp nhỏ đứng giữa store và view, nhận thông báo khi trạng thái thay đổi, tổng hợp những nội dung cần thay đổi và truyền đến những view trực thuộc.

3. Mô hình hoạt động

Sơ đồ chung về quan hệ giữa các thành phần trong Flux :
Ta có thể hiểu đơn giản như sau :
  • Views chính là thành phần làm nhiệm vụ hiển thị nội dung ứng dụng (có thể hiểu giống như thành phần V trong mô hình MVC).
  • Khi người dùng tương tác với ứng dụng làm thay đổi trạng thái (state) của ứng dụng (VD: thêm, sửa, xóa dữ liệu cá nhân), View sẽ thông qua Action gửi các thông tin thay đổi tới Dispatcher gồm có :action_name: tên của Action (VD: ADD_ITEM - thêm sản phẩm vào giỏ hàng).action_payload: thông tin chi tiết nội dung muốn gửi (VD: Object chứa thông tin ID, quantity, price, ... của sản phẩm).
  • action_name: tên của Action (VD: ADD_ITEM - thêm sản phẩm vào giỏ hàng).
  • action_payload: thông tin chi tiết nội dung muốn gửi (VD: Object chứa thông tin ID, quantity, price, ... của sản phẩm).
  • Sau khi nhận được thông tin từ Action, Dispatcher làm nhiệm vụ truyền tải (broadcast) nội dung nhận được tới các Store đăng ký lắng nghe sự kiện thay đổi từ trước đó.
  • Store sau khi nhận thông tin, tiến hành cập nhật dữ liệu (có thể hiểu việc cập nhật dữ liệu ở đây giống việc cập nhật state của Component).
  • Sau khi cập nhật, Store bắn sự kiện xuống View để tiến hành cập nhật hiển thị cho người dùng.
  • Ngoài ra trong sơ đồ trên còn có một thành phần API để lấy dữ liệu từ Remote Server.
Sơ đồ trên đảm bảo luồng dữ liệu di chuyển trong Flux bắt buộc đi theo một đường nhất định.

4 - Các ví dụ :

ví dụ 1 :  Để hiểu rõ hơn về cấu trúc hoạt động của flux, sau đây mình sẽ trình bày 1 demo nhỏ về crud bằng reactjs và flux
  • Yêu cầu bài toán là người sử dụng biết về reactjs cơ bản và sử dụng jsx và biết component và state và props
  • Bài toán đặt ra là xây dựng một app có đầy đủ các chức năng cơ bản:Xem danh sách bài viếtCác chức năng chính như: create, update, delete, search
  • Đây là cấu trúc thư mục reactjs với rails
Nhìn vào hình vẽ trên ta có thể thấy đầy đủ các thành phần của flux in reactjs như folder actions, dispatcher, stroes, hay components...Sau đây mình sẽ đi chi tiết vào một chức năng đơn giản nhất đó là remove một article khỏi danh sách:
  • Khi Remove Article, chúng ta sẽ phải tìm id của article cần xóa và hiển thị lại thông tin danh sách articles. Sau đây là source code của component remove-from-article
###* @jsx React.DOM ###
define [
  "react-with-addons"
  "actions/app-actions"
], (React, AppActions) ->
  "use strict"
  removeFromArticle = React.createClass(
    handleClick: (e) ->
      AppActions.removeArticle @props.index
    render: ->
      `<button onClick={this.handleClick}>-</button>`
  )
  removeFromArticle
Khi click button remove article, Action removeArticle sẽ được gọi, đồng thời truyền theo thông tin id của article đang được chọn. AppActions là nơi đăng ký các Action của ứng dụng và chuyển tải thông tin (payload) tới Dispatcher.Hãy cùng xem ../actions/app-actions.js thực hiện những công việc gì:
define [
  "actions/constants"
  "dispatcher/app-dispatcher"
], (Constants, AppDispatcher)->
  "use strict"
  AppActions =
    removeArticle: (index)->
      AppDispatcher.handleViewAction(actionType: Constants.REMOVE_ARTICLE, index: index)
  AppActions
Như trên chúng ta đã gọi đến function removeArticle của AppActions kèm theo thông tin id của article mà chúng ta chọn. Khi này AppActions sẽ chuyển các thông tin cho Dispatcher:
  • actionType: tên của Action, để thuận tiện cho việc đặt tên Action chúng ta quản lý thông qua AppConstants(chủ yếu để quản lý các text tĩnh đặt tên cho Action):
define [], ->
  "use strict"
  REMOVE_ARTICLE:    "REMOVE_ARTICLE"
Theo luồng hoạt động của Flux, hãy xem sau khi Action truyền thông tin cho Dispatcher thì Dispatcher sẽ xử lý thông tin và thông báo cho Store. Hãy xem file ../dispatcher/app-dispatcher.js:
define [
  "jquery"
  "dispatcher/dispatcher"
], ($, Dispatcher)->
  "use strict"
  AppDispatcher = $.extend(Dispatcher.prototype, {
    handleViewAction: (action)->
      @dispatch(
        source: 'VIEW_ACTION'
        action: action
      )
  })
  AppDispatcher
Ta có thể thấy function handleViewAction mà AppActions của chúng ta vừa gọi đến lúc trước. Khi này AppDispatcher sẽ phát lệnh gửi thông tin tới các Store.File ../stores/app-stores.js
define [
  "jquery"
  "lodash"
  "dispatcher/app-dispatcher"
  "actions/constants"
  "eventemitter2"
], ($, _, AppDispatcher, Constants, EventEmitter)->
  "use strict"

  CHANGE_EVENT = "change"

  _articles = []

  _removeArticle = (index)->
    _articles.splice(index, 1)

  AppStore = $.extend(new EventEmitter(), {
    emitChange: -> @emit(CHANGE_EVENT)
    addChangeListener: (callback)-> @on(CHANGE_EVENT, callback)
    removeChangeListener: (callback)-> @removeListener(CHANGE_EVENT, callback)
    getArticle: -> _articles
    dispatcherIndex: AppDispatcher.register((payload)->
      action = payload.action

      switch action.actionType
        when Constants.REMOVE_ARTICLE then _removeArticle(action.index)

      AppStore.emitChange()
    )
  })
AppStores đã đăng ký trước với Dispatcher để lắng nghe các Action. Sau khi thực hiện xóa article, AppStore tiến hành truyền sự kiện để thông báo tới cho các View dữ liệu lưu trữ đã được thay đổi nhờ sự trợ giúp của module EventEmitterVí dụ 2 :Xây dựng module shopping cart với flux Để có thể hiểu và thực hiện được bài hướng dẫn (tutorial) nho nhỏ này, tôi đặt giả thiết các bạn đã từng làm việc cơ bản với ReactJS :
  • Đã xây dựng được Hello Word Application bằng JSX.
  • Đã viết được Component nào đó trong React, nắm được khái niệm state và props trong Component.
  • Một chú ý nho nhỏ để phân biệt giữa state và props đó là khi state thay đổi thì Component sẽ được Render lại, còn props thì không.
Bài toán đặt ra là xây dựng một module Cart (giỏ hàng) cho phép người dùng thực hiện các thao tác :
  • Xem thông tin các mặt hàng đang có và đơn giá của từng mặt hàng.
  • Xem thông tin giỏ hàng hiện tại, số lượng các sản phẩm, giá từng mặt hàng và tổng giá trị đơn hàng.
  • Các nút chức năng thực hiện công việc:Thêm sản phẩm vào giỏ hàng (Add).Xóa sản phẩm trong giỏ hàng (Remove).Tăng số lượng (quantity) sản phẩm muốn mua (Increase).Giảm số lượng (quantity) sản phẩm muốn mua (Decrease).
  • Thêm sản phẩm vào giỏ hàng (Add).
  • Xóa sản phẩm trong giỏ hàng (Remove).
  • Tăng số lượng (quantity) sản phẩm muốn mua (Increase).
  • Giảm số lượng (quantity) sản phẩm muốn mua (Decrease).
Hình ảnh của Module khi hoàn thiện như sau :
Trước tiên hãy bắt đầu với cấu trúc thư mục của ứng dụng mà chúng ta sắp xây dựng:
dist/
    js/
        app.js
    index.html
node_modules/
src/
    js/
        actions/
        components/
        constants/
        dispatcher/
        stores/
        app.js
    index.html
gulpfile.js
package.json
Filepackage.jsoncó nội dung như sau:
{
  "name": "react-flux-stores",
  "version": "1.0.0",
  "description": "Building simple store with React and Flux",
  "main": "app.js",
  "scripts": {
    "test": "gulp"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/nguyenthanhtung88/react-flux-stores.git"
  },
  "keywords": [
    "react",
    "flux",
    "store"
  ],
  "author": "Tungshooter",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/nguyenthanhtung88/react-flux-stores/issues"
  },
  "homepage": "https://github.com/nguyenthanhtung88/react-flux-stores",
  "devDependencies": {
    "flux": "^2.0.1",
    "gulp": "^3.8.11",
    "gulp-browserify": "^0.5.1",
    "gulp-concat": "^2.5.2",
    "react": "^0.13.1",
    "reactify": "^1.1.0",
    "underscore": "^1.8.3"
  }
}
Sử dụng npm để quản lý các module liên quan như gulp, react, flux, underscore. Sau khi đã có package.json bạn chỉ việc vào thư mục làm việc và thực hiện lệnh:
npm install
để cài đặt các module sử dụng cho ứng dụng vào thư mụcnode_modules.gulpfle.js
var gulp = require("gulp");
var browserify = require("gulp-browserify");
var concat = require("gulp-concat");

gulp.task("browserify", function() {
    gulp.src("src/js/app.js")
        .pipe(browserify({transform: "reactify"}))
        .pipe(concat("app.js"))
        .pipe(gulp.dest("dist/js"));
});

gulp.task("copy", function() {
    gulp.src("src/index.html")
        .pipe(gulp.dest("dist"));
});

gulp.task("default", ["browserify", "copy"]);

gulp.task("watch", function() {
    gulp.watch("src/**/*.*", ["default"]);
});
Gulp làm 2 nhiệm vụ chính:
  • browserify: có sự trợ giúp của reactify để chuyển code từ jsx sang js, đồng thời copy app.js sang dist/js.
  • copy: chỉ làm nhiệm vụ copy index.html từ src sang dist.
Để thực hiện gulp task, điều đầu tiên bạn cần làm là cài đặt global gulp:
npm install --global gulp
Sau đó vào thư mục làm việc và chạy lệnh gulp, khi đó các gulp task sẽ được tự động thực hiện theo task default.Như vậy chúng ta đã hoàn thành việc cài đặt các công cụ trợ giúp cho việc phát triển ứng dụng. Chúng ta đã có trong tay thư viện react, thư viện flux. Giờ là lúc bắt đầu vào xây dựng View và các chức năng liên quan.Với hình ảnh ứng dụng hoàn thành, ta có thể chia màn hình thành các Component như hình ảnh sau:
Từ đó xây dựng được thư mục src/js/componentsgồm các thành phần View:
src/
    js/
        components/
            add-to-cart.js
            cart.js
            catalog.js
            decrease.js
            increase.js
            main.js
            remove-from-cart.js
Trong phạm vi bài viết này tôi xin phép chỉ giới thiệu một luồng hoạt động của chức năng Add To Cart theo mô hình Flux. Với các chức năng Remove From Cart, Increase, Decrease các bạn vui lòng xem source code để hiểu chi tiết hơn.Khi thực hiện Add To Cart, chúng ta sẽ phải thêm sản phẩm vào giỏ hàng và hiển thị thông tin cho người dùng. Hãy cùng nhau xem source code của component add-to-cart
var React = require("react");
var AppActions = require("../actions/app-actions");

var AddToCart = React.createClass({
    handleClick: function() {
        AppActions.addItem(this.props.item);
    },
    render: function() {
        return (
            <button onClick={this.handleClick}>Add To Cart</button>
        );
    }
});

module.exports = AddToCart;
Khi click button Add to cart, Action addItem sẽ được gọi, đồng thời truyền theo thông tin của sản phẩm đang được chọn (dưới dạng Object). AppActions là nơi đăng ký các Action của ứng dụng và chuyển tải thông tin (payload) tới Dispatcher.Hãy cùng xem src/js/actions/app-actions.js thực hiện những công việc gì:
var AppConstants = require("../constants/app-constants");
var AppDispatcher = require("../dispatcher/app-dispatcher");

var AppActions = {
    addItem: function(item) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.ADD_ITEM,
            item: item
        })
    },
    removeItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.REMOVE_ITEM,
            index: index
        })
    },
    increaseItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.INCREASE_ITEM,
            index: index
        })
    },
    decreaseItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.DECREASE_ITEM,
            index: index
        })
    }
}

module.exports = AppActions;
Như trên chúng ta đã gọi đến function addItem của AppActions kèm theo thông tin object của sản phẩm mà chúng ta chọn. Khi này AppActions sẽ chuyển các thông tin cho Dispatcher:
  • actionType: tên của Action, để thuận tiện cho việc đặt tên Action chúng ta quản lý thông qua AppConstants(chủ yếu để quản lý các text tĩnh đặt tên cho Action):
module.exports = {
    ADD_ITEM: "ADD_ITEM",
    REMOVE_ITEM: "REMOVE_ITEM",
    INCREASE_ITEM: "INCREASE_ITEM",
    DECREASE_ITEM: "DECREASE_ITEM",
}
  • item: thông tin object của sản phẩm (giá cả, số lượng, ...)
Chúng ta có thể tùy biến các tham số truyền cho Dispatcher, ví dụ bạn có thể đặt tên cho các tham số kiểu như my_item, cart_item,... không có vấn đề gì.Đúng theo luồng hoạt động của Flux, hãy xem sau khi Action truyền thông tin cho Dispatcher thì Dispatcher sẽ xử lý thông tin ra sao và thông báo cho Store thế nào. Hãy cùng xem file src/js/dispatcher/app-dispatcher.js:
var Dispatcher = require("flux").Dispatcher;
var _ = require("underscore");

var AppDispatcher = _.extend(new Dispatcher(), {
    handleViewAction: function(action) {
        this.dispatch({
            source: 'VIEW_ACTION',
            action: action
        })
    }
});

module.exports = AppDispatcher;
Ở đây chúng ta kế thừa Dispatcher từ module flux mà Facebook cung cấp. Ta có thể thấy function handleViewAction mà AppActions của chúng ta vừa gọi đến lúc trước. Khi này AppDispatcher sẽ coi như trạm trung chuyển thông tin, phát lệnh gửi thông tin tới các Store.Source code js/stores/app-stores.js
var AppDispatcher = require("../dispatcher/app-dispatcher");
var AppConstants = require("../constants/app-constants");
var _ = require("underscore");
var EventEmitter = require("events").EventEmitter;

var CHANGE_EVENT = "change";

var _catalog = [
    {id: 1, title: "Widget #1", cost: 1},
    {id: 2, title: "Widget #2", cost: 2},
    {id: 3, title: "Widget #3", cost: 3}
];

var _cartItems = [];

function _removeItem(index) {
    _cartItems[index].inCart = false;
    _cartItems.splice(index, 1);
}

function _increaseItem(index) {
    _cartItems[index].qty++;
}

function _decreaseItem(index) {
    if (_cartItems[index].qty > 1) {
        _cartItems[index].qty--;
    } else {
        _removeItem(index);
    }
}

function _addItem(item) {
    if (!item.inCart) {
        item['qty'] = 1;
        item['inCart'] = true;
        _cartItems.push(item);
    } else {
        _cartItems.forEach(function(cartItem, i) {
            if (cartItem.id == item.id) {
                _increaseItem(i);
            }
        });
    }
}

var AppStore = _.extend(EventEmitter.prototype, {
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
    getCart: function() {
        return _cartItems;
    },
    getCatalog: function() {
        return _catalog;
    }
});

AppDispatcher.register(function(payload) {
    var action = payload.action; // This is action from handleViewAction

    switch (action.actionType) {
        case AppConstants.ADD_ITEM:
            _addItem(action.item);
            break;
        case AppConstants.REMOVE_ITEM:
            _removeItem(action.index);
            break;
        case AppConstants.INCREASE_ITEM:
            _increaseItem(action.index);
            break;
        case AppConstants.DECREASE_ITEM:
            _decreaseItem(action.index);
            break;
    }

    AppStore.emitChange();

    return true;
});

module.exports = AppStore;
AppStores đã đăng ký trước với Dispatcher để lắng nghe các Action:
AppDispatcher.register(function(payload) {
    var action = payload.action; // This is action from handleViewAction

    switch (action.actionType) {
        case AppConstants.ADD_ITEM:
            _addItem(action.item);
            break;
        case AppConstants.REMOVE_ITEM:
            _removeItem(action.index);
            break;
        case AppConstants.INCREASE_ITEM:
            _increaseItem(action.index);
            break;
        case AppConstants.DECREASE_ITEM:
            _decreaseItem(action.index);
            break;
    }

    AppStore.emitChange();

    return true;
});
Sau khi thực hiện cập nhật các dữ liệu liên quan (trong luồng hoạt động này là thêm mới 1 sản phẩm vào giỏ hàng), AppStore tiến hành lan truyền sự kiện để thông báo tới cho các View dữ liệu lưu trữ đã được thay đổi nhờ sự trợ giúp của module EventEmitter
AppStore.emitChange();
var AppStore = _.extend(EventEmitter.prototype, {
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
    getCart: function() {
        return _cartItems;
    },
    getCatalog: function() {
        return _catalog;
    }
});
AppStore cung cấp hàm getCart() để lấy thông tin của biến _cartItems. Ta có thể tưởng tượng việc AppStore thực hiện thay đổi thông tin của biến _cartItems tương đương với việc thay đổi state của Component. Cuối cùng thông tin đã được truyền đến View và render lại để nhận được dữ liệu mới nhất. Hãy cùng xem source code của src/js/components/cart.js:
var React = require("react");
var AppStore = require("../stores/app-store");
var RemoveFromCart = require("../components/remove-from-cart");
var Increase = require("../components/increase");
var Decrease = require("../components/decrease");

function getCartItems() {
    return {
        items: AppStore.getCart()
    }
}

var Cart = React.createClass({
    getInitialState: function() {
        return getCartItems();
    },
    componentWillMount: function() {
        AppStore.addChangeListener(this._onChange);
    },
    _onChange: function() {
        this.setState(getCartItems());
    },
    render: function() {
        var total = 0;
        var items = this.state.items.map(function(item, i) {
            var subtotal = item.qty * item.cost;
            total += subtotal;
            return (
                <tr key={i}>
                    <td><RemoveFromCart index={i}/></td>
                    <td>{item.title}</td>
                    <td>{item.qty}</td>
                    <td>
                        <Increase index={i} />
                        <Decrease index={i} />
                    </td>
                    <td>${subtotal}</td>
                </tr>
            );
        });
        return (
            <table className="table table-hover">
                <thead>
                    <tr>
                        <th></th>
                        <th>Item</th>
                        <th>Quantity</th>
                        <th></th>
                        <th>Subtotal</th>
                    </tr>
                </thead>
                <tbody>
                    {items}
                </tbody>
                <tfoot>
                    <tr>
                        <td colSpan="4" className="text-right">Total</td>
                        <td>${total}</td>
                    </tr>
                </tfoot>
            </table>
        );
    }
});

module.exports = Cart;
View khi khởi tạo luôn lắng nghe sự thay đổi từ phía Store
componentWillMount: function() {
    AppStore.addChangeListener(this._onChange);
}
Khi Store thay đổi thông tin, View có nhiệm vụ gọi và lấy dữ liệu mới từ Store
_onChange: function() {
    this.setState(getCartItems());
}

 IV- Cách sử dụng Flux

1.  Cài đặt Redux

Bản chất làm việc với React là việc truyền dữ liệu giữa các component và thay đổi state để rerender lại giao diện component. Nếu dự án chúng ta lớn, việc truyền props từ Component cha sang nhiều component con khác sẽ rất khó để theo dõi code và bảo trì code.Redux là thư viện cung cấp cho ta một store trung tâm, lưu trữ tất cả các state, từ component muốn thay đổi state chỉ cần truy cập tới store để thay đổi.Chúng ta sẽ cài đặt Redux theo dòng lệnh sau :
install --save react-redux

2. Tạo files và folders :

Trong bước nàym ta sẽ tạo foldes và files với các actionsreducers, và components. Sau khi hoàn thành, chúng ta tiến hành cài đặt cấu trúc thư mục như sau :
C:\Users\Tutorialspoint\Desktop\reactApp>mkdir actions
C:\Users\Tutorialspoint\Desktop\reactApp>mkdir components
C:\Users\Tutorialspoint\Desktop\reactApp>mkdir reducers
C:\Users\Tutorialspoint\Desktop\reactApp>type nul > actions/actions.js
C:\Users\Tutorialspoint\Desktop\reactApp>type nul > reducers/reducers.js
C:\Users\Tutorialspoint\Desktop\reactApp>type nul > components/AddTodo.js
C:\Users\Tutorialspoint\Desktop\reactApp>type nul > components/Todo.js
C:\Users\Tutorialspoint\Desktop\reactApp>type nul > components/TodoList.js

3. Actions :

Actions là các đối tượng JavaScript sử dụng thuộc tính kiểu để thông báo về dữ liệu sẽ được gửi đến nơi lưu trữ. Mình sử dụng ADD_TODO để thêm mục mới vào danh sách Hàm addTodo trả về hành động và đặt một id cho mọi mục đã tạo.actions/actions.js
export const ADD_TODO = 'ADD_TODO'

let nextTodoId = 0;

export function addTodo(text) {
   return {
      type: ADD_TODO,
      id: nextTodoId++,
      text
   };
}

4. Reducers

Reducer chắc chắn là khái niệm quan trọng nhất trong Redux. Nó sản xuất state của ứng dụng.Nhưng làm thế nào để một reducer biết khi nào sẽ tạo ra state tiếp theo. Nguyên tắc thứ 2 của Redux cho biết cách duy nhất để thay đổi state là gửi các tín hiệu đến store. Tín hiệu này là một hành động (action). “Dispatching an action” là quá trình để gửi tín hiệu.Bây giờ làm thế nào để thay đổi state bất biến. State là kết quả của một bản sao của state hiện tại và dữ liệu mới.Điều đáng quan tâm ở đây là các hành động (action) redux là một đối tượng Javascript. Dưới đây là một hành động.Mặc dù các hành động chỉ kích hoạt các thay đổi trong ứng dụng, nhưng bộ giảm chỉ định các thay đổi đó.  đang sử dụng câu lệnh switch để tìm kiếm hành động ADD_TODO. Bộ giảm là một hàm nhận hai tham số (trạng thái và hành động) để tính toán và trả về trạng thái cập nhật.Chức năng đầu tiên sẽ được sử dụng để tạo một mục mới, trong khi chức năng thứ hai sẽ đẩy mục đó vào danh sách. Cuối cùng, chúng tôi đang sử dụng chức năng trợ giúp connectReducers nơi chúng tôi có thể thêm bất kỳ bộ giảm mới nào mà chúng tôi có thể sử dụng trong tương lai.reducers/reducers.js
import { combineReducers } from 'redux'
import { ADD_TODO } from '../actions/actions'

function todo(state, action) {
   switch (action.type) {
      case ADD_TODO:
         return {
            id: action.id,
            text: action.text,
         }
      default:
         return state
   }
}
function todos(state = [], action) {
   switch (action.type) {
      case ADD_TODO:
         return [
            ...state,
            todo(undefined, action)
         ]
      default:
         return state
   }
}
const todoApp = combineReducers({
   todos
})
export default todoApp

5. Store

Store là nơi lưu giữ trạng thái của ứng dụng. Rất dễ dàng để tạo một store sau khi bạn có reducers.Mình đang chuyển thuộc tính store cho phần tử cung cấp, phần tử này bao bọc thành phần router.
import React from 'react'

import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import App from './App.jsx'
import todoApp from './reducers/reducers'

let store = createStore(todoApp)
let rootElement = document.getElementById('app')

render(
   <Provider store = {store}>
      <App />
   </Provider>,
	
   rootElement
)

6. Root Component

App component là một component gốc của ứng dụng. Chỉ có component gốc nên biết vềredux. Điều qua trọng để lưu ý là hàm connect chỉ sử dụng khi kết nối component gốc App với storeHàm này sẽ sử dụng hàm select như là thuật toán. Hàm select lấy trạng thái từ nơi lưu trữ và trả về props(visibleTodos) mà ta có thể sử dụng trong các components App.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addTodo } from './actions/actions'

import AddTodo from './components/AddTodo.js'
import TodoList from './components/TodoList.js'

class App extends Component {
   render() {
      const { dispatch, visibleTodos } = this.props
      
      return (
         <div>
            <AddTodo onAddClick = {text =>dispatch(addTodo(text))} />
            <TodoList todos = {visibleTodos}/>
         </div>
      )
   }
}
function select(state) {
   return {
      visibleTodos: state.todos
   }
}
export default connect(select)(App);

7. Các component khác 

Các component này không nên sử dụng reduxAddTodo.js
import React, { Component, PropTypes } from 'react'

export default class AddTodo extends Component {
   render() {
      return (
         <div>
            <input type = 'text' ref = 'input' />
				
            <button onClick = {(e) => this.handleClick(e)}>
               Add
            </button>
         </div>
      )
   }
   handleClick(e) {
      const node = this.refs.input
      const text = node.value.trim()
      this.props.onAddClick(text)
      node.value = ''
   }
}
Todo.js
import React, { Component, PropTypes } from 'react'

export default class Todo extends Component {
   render() {
      return (
         <li>
            {this.props.text}
         </li>
      )
   }
}
TodoList.js
import React, { Component, PropTypes } from 'react'
import Todo from './Todo.js'

export default class TodoList extends Component {
   render() {
      return (
         <ul>
            {this.props.todos.map(todo =>
               <Todo
                  key = {todo.id}
                  {...todo}
               />
            )}
         </ul>
      )
   }
}
Mở terminal và chạy App, sau khi chạy xong thì ta được kết quả như sau :

V- Animation

1- Giới thiệu về Animation :

  • Để xử lý các animation, ta có thể lựa chọn cách viết truyền thống là css modules, viết css thuần, sau đó viết logic xử lý vài sự kiện bằng event. Tuy nhiên, ta nên thử lựa chọn 1 số thư viện, nó giúp việc quản lý animation bằng mã js dễ quản lý hơn. Trong đó 1 thư viện có thể giúp điều đó là Reacts-spring.
  • Nó là 1 thư viện xử lý các hoạt hình, lấy cảm hứng từ animated và react-motion, xử lý các hoạt hình hầu như hay xảy ra với UI như việc hoạt động xử lý vật lý của con lắc lò xo khi lắc( khối lượng, độ căng của dây lò xo, ma sát với không khí). Bởi vì nó rất nhẹ ( 10.7KB với bản trên web, 9.7kb với bản trên native. Với khả năng xử lý linh động và được support từ các ông lớn như airbnb hay matterapp, thì việc được cộng đồng ủng hộ nhiệt tình từ khi nó mới ra mắt, còn chần chờ gì nữa mà không tìm hiểu nó nào.

2-  Cài đặt Animation :

Cài đặt lệnh sau :
npm install react-spring

3- API và các đối tượng hay sử dụng :

a. CÁC COMMON API:

Đây là các api để config mà tất cả các spring(hook/component) đều sử dụng. Ta sử dụng chúng để config để có hoạt hình hợp lý, chia làm các nhóm sau(mình chỉnh liệt kê một số config cảm thấy hay dùng và có thể hiểu, vì nó rất nhiều).
Configs:
  • duration: thời gian hiệu ứng(giá trị mặc định là undefined)
  • precision: độ chính xác
  • tension: độ căng, giá trị này càng lớn hoạt hình càng diễn ra liên tục
  • friction: ma sát, giá trị càng lớn dẫn đến sức cản của hoạt hình càng cao, hoạt hình diễn ra chậm hơn
  • mass: khối lượng
Presets:
Đây là bộ giá trị của mass, tension và friction. Mặc định của nó là : config.default tương ứng với { mass: 1, tension: 170, friction: 26 }. Các bộ khác các bạn có thể coi trong docs của nó.
Properties:
Các thuộc tính hay dùng sẽ là:
  • from: (từ) cũng giống như from trong keyframe
  • to(đến):cũng giống như to trong keyframe
  • delay: tạm dừng mili giây trước khi bắt đầu
  • config: sử dụng các config đã nêu ở mục config trên
  • reset: set bằng true nếu muốn reset lại mỗi khi kết thúc 1 lần hoạt hình.
  • onStart: ở giai đoạn bắt đầu sẽ làm gì
  • onReset: ở giai đoạn đứng yên sẽ làm gì
    Interpolations: mình thấy các api hay dùng là map, output với extrapolate.
    Tham khảo tại đây: https://www.react-spring.io/docs/hooks/api

b.  Công cụ hỗ trợ (UNIT):

  • Màu sắc(names, rgb, rgba, hsl, hsla, gradients)
  • Đơn vị(cm, mm, in, px, pt, pc)
  • Relative lengths (em, ex, ch, rem, vw, vh, vmin, vmax, %)
  • Góc đo(deg, rad, grad, turn)
  • Flex and grid units (fr, etc)
  • All HTML attributes
  • SVG paths
  • Arrays
  • String patterns (transform, border, boxShadow, etc)
  • Non-animatable string values (visibility, pointerEvents, etc)
  • ScrollTop/scrollLeft

c.  Một số Hook Api xử lý animaiton hay dùng:

React-spring có 5 hook hay dùng(viết hook, nên nó được dùng như custom hook). Mình sẽ giới thiệu 1 số ví dụ cơ bản và 1 số ví dụ hay dùng của nó. Các hook sẽ dùng với các api common đã được giới thiệu trước đó.
a. Use Spring:
Được dùng với đơn hoạt hình, để di chuyển (giống như from -> to giống keyframe trong css).Một số ví dụ:
  • Ví dụ khi ta muốn hiệu ứng từ từ hiện ra đoạn text
Ví dụ này tương đương:
from  {
opacity: 0;
}
to {
opacity: 1;
}
Phân tích ví dụ:
  • Ta sử dụng useString như một hook, và chắc chắn có truyền vào from và tham số khác và opacity , nó sẽ trả về props styles tương ứng (ngoài ra còn có set để set lại trạng thái style cho hoạt hình nếu muốn thay đổi khi có event xảy ra hay stop nếu muốn stop hoạt hình).
  • animation.h1 → render ra thẻ h1 + props là styles được truyền vào → animation tương ứng
  • Ví dụ về hiệu scroll:
Lần này ta ta truyền vào thay vì param opacity , ta truyền vào scroll . Hiệu ứng này có kết quả như sau:
  • Ví dụ hiệu ứng vẽ svg:
Lần này ta thêm config delay và duration để có thể xem hiệu ứng từ từ( config là 1 hỗ trợ thêm từ react-spring nếu ta muốn hiển thị theo nhiều cách khác nhau)
b) useSprings:
Tương tự như với useSpring, tuy nhiên được áp dụng với 1 list các đối tượng mà ta muốn nó có hiệu ứng gần như giống nhau.
c) useTrail:
Nếu bạn muốn xử lý 1 mảng các phần tử, bạn muốn các phần tử trong mảng này sẽ có hiệu ứng lần lượt được thực hiện từ phần tử thứ nhất đến phần tử cuối cùng. Hiệu ứng này sẽ tạo ra hiệu ứng liên tiếp nhau, ví dụ như thế này:https://codesandbox.io/embed/zn2q57vn13?source=post_page—–636910f3abfa———————-
d) useTransition:
Nếu bạn muốn quản lý sự biến đổi của các hiệu ứng khác nhau:
Ngoài các api common, nó còn còn sử dụng các api khác như from, enter , update, leave,vv để quản lý các trạng thái khi đến, rời đi hay hủy bỏ.
Ví dụ mình thấy hay nhất trong docs của nó liên quan đến chuyển trang khi kết hợp react-router:https://codesandbox.io/embed/1y3yyqpq7q?source=post_page—–636910f3abfa———————-
e)useChain:
xử dụng kết hợp với useRef để xác định đối tượng và xác định thứ tự thực hiện các animation.
Như ví dụ này:https://codesandbox.io/embed/2v716k56pr?source=post_page—–636910f3abfa———————-

d.  Lưu ý:

Ngoài ra, trước đây react-spring viết thành các component xử lý animation có chức năng tương tự: Spring tương ứng với useSpring, Transition tương ứng với useTransition, Trail tương ứng với useTrail.
Nhưng giờ đa phần họ dùng với hook, các ví dụ về component trên github cũng đã đã bị xóa đi và đều chuyển sang dùng với hook.

4- Ví dụ :

Bước 1 : Cài đặt React CSS Transitions Group :Ta cài đặt bằng dòng lệnh sau :
npm install react-addons-css-transition-group
Bước 2 : Thêm file CSS :Ta tạo file style.css như sau :
css/style.css
Để có thể  sử dụng được file này trong App, ta cần phải liên kết nó với index.html như sau :
<!DOCTYPE html>
<html lang = "en">
   <head>
      <link rel = "stylesheet" type = "text/css" href = "./style.css">
      <meta charset = "UTF-8">
      <title>React App</title>
   </head>
   <body>
      <div id = "app"></div>
      <script src = 'index_bundle.js'></script>
   </body>
</html>
Bước 3 : Sửu dụng animation :Ta sẽ tạo một component React. Phần tử ReactCSSTransitionGroup sẽ được sử dụng để bao bọc component mà ta muốn animate cho nó. Nó sẽ sử dụng 2 hàm làtransitionAppear và transitionAppearTimeout, khitransitionEnter và transitionLeave  không đungApp.jsx
import React from 'react';
var ReactCSSTransitionGroup = require('react-addons-css-transition-group');

class App extends React.Component {
   render() {
      return (
         <div>
            <ReactCSSTransitionGroup transitionName = "example"
               transitionAppear = {true} transitionAppearTimeout = {500}
               transitionEnter = {false} transitionLeave = {false}>
					
               <h1>My Element...</h1>
            </ReactCSSTransitionGroup>
         </div>      
      );
   }
}
export default App;
main.js
import React from 'react'
import ReactDOM from 'react-dom';
import App from './App.jsx';

ReactDOM.render(<App />, document.getElementById('app'));
Animation trong CSS khá đơn giản như sau :css/style.css
.example-appear {
   opacity: 0.04;
}
.example-appear.example-appear-active {
   opacity: 2;
   transition: opacity 50s ease-in;
}
 Kết qủa khi chạy ứng dụng như hình dưới đây 
Bước 4 : Enter và Leave trong animations :Enter và leave animaitons được sử dụng khi ta muốn thêm hoặc xoá phần tử trong listapp.jsx
import React from 'react';
var ReactCSSTransitionGroup = require('react-addons-css-transition-group');

class App extends React.Component {
   constructor(props) {
      super(props);
		
      this.state = {
         items: ['Item 1...', 'Item 2...', 'Item 3...', 'Item 4...']
      }
      this.handleAdd = this.handleAdd.bind(this);
   };
   handleAdd() {
      var newItems = this.state.items.concat([prompt('Create New Item')]);
      this.setState({items: newItems});
   }
   handleRemove(i) {
      var newItems = this.state.items.slice();
      newItems.splice(i, 1);
      this.setState({items: newItems});
   }
   render() {
      var items = this.state.items.map(function(item, i) {
         return (
            <div key = {item} onClick = {this.handleRemove.bind(this, i)}>
               {item}
            </div>
         );
      }.bind(this));
      
      return (
         <div>      
            <button onClick = {this.handleAdd}>Add Item</button>
				
            <ReactCSSTransitionGroup transitionName = "example" 
               transitionEnterTimeout = {500} transitionLeaveTimeout = {500}>
               {items}
            </ReactCSSTransitionGroup>
         </div>
      );
   }
}
export default App;
main.js
import React from 'react'
import ReactDOM from 'react-dom';
import App from './App.jsx';

ReactDOM.render(<App />, document.getElementById('app'));
css/style.css
.example-enter {
   opacity: 0.04;
}
.example-enter.example-enter-active {
   opacity: 5;
   transition: opacity 50s ease-in;
}
.example-leave {
   opacity: 1;
}
.example-leave.example-leave-active {
   opacity: 0.04;
   transition: opacity 50s ease-in;
}
Kết qủa như sau :

5- Tổng kết :

Việc sử dụng react-spring rất linh hoạt, giúp việc kiểm soát animation bằng js , nó giúp kiểm soát khi nào bắt đầu , khi nào kết thúc hay hủy bỏ hiệu ứng một cách chủ động hơn.
Ngược lại, nó có thể nặng hơn so với việc sử dụng css thuần(nhưng chỉ áp dụng cho web, và 1 số platform rất hạn chế).
Do đó, việc sử dụng animation như thế nào còn tùy thuộc mức độ của dự án, cũng như mục đích hướng tới của nó là như thế nào.

VI- Higher Order Components 

1- Giới thiệu về HOC

Một HOC trong React là một patter được dùng để chia sẻ function chung giữa các component mà không phải lặp lại phần code chung. Nó thực tế không phải là component mà là function. Một HOC function nhận một component giống như tham số và trả về một component. Nó biến đổi một component thành component khác và thêm vào data hoặc function. Tức là:
const NewComponent = (BaseComponent) => {
// … create new component from old one and update
return UpdatedComponent
}
Trong hệ sinh thái React bạn có thể dễ dàng bắt gặp HOC được triển khai trong connect của Redux và withRouter trong React Router. Function connect trong Redux nhận một component truy cập vào global state trong Redux store, sau đó đưa vào component như là props. withRouter inject vào thông tin router và function trong component, cho phép lập trình viên có thể truy cập hoặc thay đổi route.

2- The higher-order component pattern

Higher-order component là một funtion, nó sẽ nhận một component giống như là một tham số và trả về một component. Điều này nghĩa là HOC sẽ luôn luôn có dạng như sau:
import React from 'react';
const higherOrderComponent = (WrappedComponent) => {
   class HOC extends React.Component {
    render() {
     return <WrappedComponent />
   }
  return HOC;
}
higherOrderComponent là function nhận một component gọi là WrappedComponentgiống như là tham số. Chúng ta sẽ tạo một component mới gọi là HOC nó sẽ trả về từ chính render function. Thực tế ví dụ trên không thêm vào bất kỳ function nào trong HOC tuy nhiên nó là pattern phổ biến cho mọi HOC tuân theo. Sau khi tạo được HOC cách chúng ta gọi nó sẽ là:
const SimpleHOC = higherOrderComponent(MyComponent);

3 - Phân tích vấn đề

Chúng ta hãy cùng đi vào một ví dụ thực tế: Giả sử bạn có một trang web thường xuyên sử dụng modal, như hình chẳng hạn.
Các thao tác cần hiện modal đó là:
  • Click vào từng dòng trong table.
  • Click vào icon User Profile để hiển thị thông tin người dùng đang đăng nhập hoặc login form.
  • ....
    Đây là cách mình xử lý modal cho table.
import React from 'react';

const TableModal = ({data, closeModal}) => {
    return (
        // hiển thị dữ liệu
        // xử lý đóng modal
    );
}
const TableRow = ({ data, showModal }) => {
    return (
        <tr onClick={() => showModal(data)}>
            <td>Name</td>
            <td>Gender</td>
            <td>Address</td>
        </tr>
    );
}

class Table extends React.Component {
    constructor() {
        super();
        this.state = {
            modal: {
                show: false,
                data: null
            }
        }
    }
    // hiển thị modal với data được truyền vào.
    showModal = (data) => {
        const { modal } = this.state;
        this.setState({modal: {
            modal: {
                show: true,
                data: data
            }
        })
    }
    // đóng modal, xóa bỏ dữ liệu trong modal
    closeModal = () => {
        this.setState({
            show: false,
            data: null
        })
    }
    
    render() {
        const { modal } = this.state;
        return (
            <table>
                <thead>
                    // table header
                </thead>
                <tbody>
                    <TableRow />
                    <TableRow />
                    <TableRow />
                    {
                        modal.show
                        ? <TableModal data={modal.data} closeModal={this.closeModal} />
                        : null
                    }
                <tbody>
            </table>
        );
    }
}
Và do cái icon User Profile cũng cần hiển thị modal chứa thông tin user khi click vào, chúng ta cũng sẽ phải viết ra một vài component và phải xử lý phần hiển thị modal => lặp lại code xử lý modal.
class UserProfile extends React.Component {
    constructor() {
        super();
        this.state = {
            modal: {
                show: false,
                data: null
            }
        }
    }
    // đoạn code trong khoảng dấu "*" này đã bị lặp lại, và khi chúng ta
    // có nhiều phần xử lý modal tương tự như thế này,
    // chúng ta rất có khả năng copy đoạn code này nhiều lần.
    *************************************************
    // hiển thị modal với data được truyền vào.
    showModal = (data) => {
        const { modal } = this.state;
        this.setState({modal: {
            modal: {
                show: true,
                data: data
            }
        })
    }
    // đóng modal, xóa bỏ dữ liệu trong modal
    closeModal = () => {
        this.setState({
            show: false,
            data: null
        })
    }
    *************************************************
    render() {
        return (
            {
                this.state.showModal
                ? <UserModal />
                : null
            }
            <button type="button">User Profile</button>
        );
    }
}
Vậy, chúng ta giải quyết tình huống này như thế nào???

4- HOC và áp dụng vào giải quyết bài toán

Như đã nói ở trên, trong React có một khái niệm gọi là Higher-Order Component. Bản chất của Higher-Order Component là nhận vào một component và trả về một component - Ý tưởng rất đơn giản phải không nào còn việc gì xảy ra bên trong thì ta không biết  . Lợi ích của việc sử dụng HOC là chúng ta gom nhóm việc xử lý, tái sử dụng được component. Một HOC có hình dáng như sau
const withModal = WrappedComponent => {
    return class extends React.Component {
     // do something
    }
}
Chúng ta hãy cũng dùng HOC để giải quyết vấn đề vừa nêu trên.
Như đã phân tích, chúng ta đang bị lặp code ở phần xử lý hiển thị và đóng modal. Chúng ta mong muốn rằng HOC mà chúng ta đang chuẩn bị viết sẽ phải xử lý được phần modal đó và sẽ phải áp dụng được với bất kì component nào mà nó nhận vào.
Hãy cùng xem.
import ModalComponent from '../ModalComponent' // giả sử bạn đã có component này.

const withModal = WrappedComponent => {
    return class extends React.Component {
        constructor() {
            super();
            this.state = {
                show: false,
                data: null,
            }
        }
        showModal = (data) => {
            this.setState({
                show: true,
                data: data
            });
        }
        closeModal = () => {
            this.setState({
                show: false,
                data: null
            })
        }
        render() {
            return (
                <React.Fragment>
                    // truyền hàm xử lý hiển thị modal vào component mà kích hoạt hành động click
                    <WrappedComponet showModal={this.showModal} />
                    {
                        this.state.show
                        ? <ModalComponent data={this.state.data}
                                closeModal={this.closeModal}
                          /> // truyền data và hàm xử lý đóng modal vào ModalComponent
                        : null
                    }
                </React.Fragment>
            );
        }
    }
}
export default withModal;
Và trong WrappedCompoent, ta viết như sau
import React from 'react';
import withModal from './withModal';
class UserProfile extends React.Component {
// viết xử lý của bạn
}
// chúng ta export ra component đã được "nâng cấp"
export default withModal(UserProfile);
Đến đây, chúng ta đã có một HOC đẹp đẽ, phân tách được logic xử lý, ko lặp lại code :v. Tuy nhiên, còn một vấn đề nữa ở đây, với mỗi WrappedComponent chúng ta cần các Modal component khác nhau. Cụ thể: modal hiển thị nội dung khi click vào table khác với modal hiển thị nội dung khi click vào icon User Profile. (khác nhau về giao diện, về cách hiển thị). Lúc này, HOC của chúng ta cần thay đổi một chút
const withModal = ModalComponent => WrappedComponent => {
    // xử lý như đoạn code ở trên
}
Việc chúng ta viết HOC mới như trên sẽ giúp bạn linh động trong việc viết các component modal, giả sử, bạn muốn viết mới một modal component, thì chỉ cần viết mới và truyền vào withModal, chứ không cần phải sửa lại modal component cũ. Lúc này, WrappedComponent của bạn sẽ viết như sau
import React from 'react';
import withModal from './withModal';
import UserModal from './UserModal';

class UserProfile extends React.Component {
    ...
}

export default withModal(UserModal)(UserProfile)
// bạn có thấy giống hàm connect của redux không???

5- Các ví dụ :

Ví dụ 1 :Bây giờ chúng ta sẽ mở rộng thêm higher-order component pattern trên để inject data vào phần wrapper.Trong ví dụ dưới đây chúng ta đã chỉ rõ secretToLife sẽ là 42. Một số component của chúng ta cũng cần được chia sẻ thông tin này, và chúng ta sẽ tạo một HOC gọi là withSecretToLife để đưa thông tin này vào như là pros.
import React from 'react'; 
const withSecretToLife = (WrappedComponent) => {  
   class HOC extends React.Component {    
       render() {      
        return (       
          <WrappedComponent          
          {...this.props}          
          secretToLife={42}        
          />     
        );    
       }  
    }      
   return HOC;
};
Để ý thấy rằng ví dụ này gần giống với ví dụ trước. Điều khác biệt là chúng ta thêm vào một prop secretToLife={42}, điều này cho phép các component sử dụng nó có thể truy cập đến giá trị gọi là this.props.secretToLife.Thêm vào đó chúng ta spread (syntax …) props để pass vào component. Điều này để chắc chắn rằng tất cả các props đều được pass vào wrapped component và nó có thể truy cập thông qua this.props .
import React from 'react';
import withSecretToLife from 'components/withSecretToLife'; 
const DisplayTheSecret = props => (  
    <div>    The secret to life is {props.secretToLife}.  </div>
); 
const WrappedComponent = withSecretToLife(DisplayTheSecret); 
export default WrappedComponent;
Component WrappedComponent chỉ là một version nâng cao của sẽ cho phép chúng ta sử dụng secretToLife giống như prop.Ví dụ 2 :Chúng ta hãy xem một ví dụ đơn giản để dễ dàng hiểu cách hoạt động của khái niệm này. MyHOC là một hàm bậc cao chỉ được sử dụng để chuyển dữ liệu đến MyComponent. Hàm này lấy MyComponent, tăng cường nó với newData và trả về thành phần nâng cao sẽ được hiển thị trên màn hình.
import React from 'react';

var newData = {
   data: 'Data from HOC...',
}

var MyHOC = ComposedComponent => class extends React.Component {
   componentDidMount() {
      this.setState({
         data: newData.data
      });
   }
   render() {
      return <ComposedComponent {...this.props} {...this.state} />
   }
};
class MyComponent extends React.Component {
   render() {
      return (
         <div>
            <h1>{this.props.data}</h1>
         </div>
      )
   }
}

export default MyHOC(MyComponent);
Chạy App và ta được kết quả như sau :

6- Những điều cần cân nhắc khi sử dụng Higher-order component

  • Một HOC nên là pure function không có side-effects. Nó không nên làm thay bất cứ thứ gì và chỉ kết hợp original comonent bằng cách bao nó bằng một component khác.
  • Không sử dụng HOC trong phương thức render của một component. Truy cập HOC ngoài component.
  • Static method phải được copy để vẫn có thể truy cập chúng. Một cách đơn giản để làm điều này đó là dùng hoist-non-react-statics
  • Không dùng Refs