Gần đây, React boilerplate ver 3 vừa mới được phát hành bởi Max Stoiber, đây là một trong những React starter kit nổi tiếng nhất. Sau đây là một số kinh nghiệm hay ho được tác giả truyền lại cho các dev khi dùng react.

Trước đây, quy mô là điều mà hệ thống ở back-end quan tâm nhiều hơn cả. Vì nếu số lượng người dùng càng ngày càng tăng, bạn phải thêm server, rồi phải đảm bảo cơ sở dữ liệu có thể được kết nối từ nhiều server, ...

Ngày nay, do webapp càng ngày càng nhiều và phức tạp, việc mở rộng quy mô trở nên quan trọng đối với cả front-end nữa. Front-end của một app phức tạp giờ phải cân thêm lưu lượng người dùng, nhiều developer và các thành phần của nó nữa.

Container và Component

Đầu tiên, muốn cải tiến một app quy mô lớn thì phải phân biệt giữa stateful ("container") component và stateless ("component") component. Container quản lí data hoặc được kết nối đến state và thường ko có style đi kèm. Còn component thì thường có style đi kèm và không quản lí data hoặc state. Về cơ bản, container chịu trách nhiệm "mọi thứ làm việc như thế nào", và component quản lí "mọi thứ sẽ trông ra sao".

Chia component sẽ tách biệt giữa các component và các lớp trong quản lí dữ liệu. Nhờ vậy, bạn có thể thoải mái chỉnh sửa component mà không lo cấu trúc dữ liệu bị thay đổi và cũng có thể chỉnh sửa container mà không sợ style thay đổi. Phân định rõ ràng giữa container và component khiến mọi việc trở nên rõ ràng hơn.

Cấu trúc

Trước đây, developers cấu trúc React theo type, có nghĩa là chia theo folders như là actions/components/containers/, ...

Giả sử thanh navigation được đặt là  NavBar cộng một vài state đi kèm và một toggleNav để đóng và mở. Cấu trúc file sẽ như sau:

react-app-by-type 
├── css 
├── actions 
│ └── NavBarActions.js 
├── containers 
│ └── NavBar.jsx 
├── constants 
│ └── NavBarConstants.js 
├── components 
│ └── App.jsx 
└── reducers 
└── NavBarReducer.js

Ví dụ như trên thì không vấn đề gì, nhưng nếu bạn có hàng trăm hoặc có thể hàng nghìn component, việc phát triển sẽ rất phức tạp. Để thêm một tính năng, bạn sẽ phải tìm đúng file trong hàng đống thư mục với hàng trăm file khác. 

Giải pháp tốt nhất hiện nay là thay vì nhóm file theo type, hãy nhóm chúng theo tính năng(feature)! Tức là nhóm tất cả file liên quan đến một chức năng (ví dụ navigation bar) vào cùng một thư mục.

Chúng ta xem lại cấu trúc file sau khi nhóm vào thư mục NavBar :

react-app-by-feature 
├── css 
├── containers 
│    └── NavBar 
│        ├── NavBar.jsx 
│        ├── actions.js 
│        ├── constants.js 
│        └── reducer.js 
└── components 
└── App.jsx

Developer chỉ việc vào một thư mục khi muốn làm việc với tính năng nào đó, và cũng chỉ cần tạo một thư mục khi thêm tính năng mới. Đổi tên thì còn dễ dàng hơn, chỉ cần tìm thư mục và thay tên mới; nhiều developers có thể làm việc cùng lúc trong một app mà không gây lỗi conflict.

Cần chú ý là điều đó không có nghĩa là action và reducer của redux chỉ có thể sử dụng trong một component. Chúng có thể được import và được sử dụng từ những component khác.

Hai câu hỏi có lẽ bạn sẽ đặt ra trong quá trình làm việc này là "Xử lí style như thế nào?" và "Lấy data (data-fetching) ra sao?". 

Xử lý Style

Ngoài quyết định về cấu trúc, làm việc với CSS trong cấu trúc xây dựng theo component (component-based architecture) không phải đơn giản do hai đặc tính cơ bản của CSS: tên (name global) và tính kế thừa (inheritance).

Tên class không lặp lại

Thử tưởng tượng trong một app rất lớn có css như sau:

.header { /* … */ }
.title {
	background-color: yellow;
}

Bạn sẽ thấy ngay là title rất chung chung. Một dev khác (hoặc chính bản thân dev đó vào một thời điểm khác) sẽ viết code như sau chẳng hạn:

.footer { /* … */ }
.title {
	border-color: blue;
}

Thế là sinh ra conflict tên, và dĩ nhiên title của bạn có border xanh và background vàng khắp nơi, và bạn sẽ phải tìm trong hàng nghìn file xem chỗ nào đã khởi tạo style kia. 

Rất may là một trong những giải pháp đơn giản là sử dụng CSS Modules. Cách xử lí là thêm một file style.css vào folder của mỗi component

react-app-with-css-modules 
├── containers 
└── components      
     └── Button          
          ├── Button.jsx          
          └── styles.css

Các file CSS trông sẽ như nhau, và giờ bạn không phải quan tâm là đặt tên như thế nào nữa mà có thể đặt tên chung chung như thế này:

.button {
	/* … */
}

Sau đó require (hoặc import) các file CSS cùng component và đặt className của styles.button vào thẻ JSX:

/* Button.jsx */
var styles = require('./styles.css');

<div className={styles.button}></div>

Nếu bạn nhìn vào DOM của browser, bạn sẽ thấy <div class="MyApp__button__1co1k"></div>! CSS Modules xử lý việc đặt tên class cho bạn bằng việc gắn thêm một đoạn hash ngắn chứa nội dung của class. Điều này có nghĩa là tỉ lệ lặp lại tên class hầu như là bằng 0, nếu nó lặp lại có nghĩa nội dung của các class đó chắc chắn là như nhau (vì hash lấy từ nội dung như nhau mà).

Reset thuộc tính cho mỗi component

Trong CSS. một số thuộc tính nhất định sẽ thừa hưởng xuyên suốt các node. Ví dụ, nếu node cha (parent node) có  line-height  và node con không đặt lại thuộc tính đó thì sẽ được mặc định thừa hưởng  line-height từ node cha.

Tưởng tượng Headercomponent và Footer component có style : 

.header {
	line-height: 1.5em;
	/* … */
}

.footer {
	line-height: 1;
	/* … */
}

Nếu chúng ta render Button bên trong hai component này, và đột nhiên button ở header sẽ khác button ở footer. Khoảng một tá thuộc tính CSS khác cũng được kế thừa như trên, và việc tìm được và chỉnh lại style sẽ rất khó.

Trong thế giới front-end, sử dụng reset style để đặt lại style về mặc định xuyên suốt browser là chuyện cơm bữa. Lựa chọn phổ biến là Reset CSS, Normalize.css và Sanitize.css! Thế nếu áp dụng ý tưởng đó vào component ?

Có một plugin cho việc auto-reset gọi là PostCSS! Nếu bạn thêm PostCSS Auto Reset cho PostCSS plugin, nó sẽ làm việc: bọc quanh local và reset style cho mỗi component, cài đặt tất cả các thuộc tính bên ngoài về mặc định và bỏ tính kế thừa. 

Data-Fetching 

Vấn đề thứ hai cần giải quyết là data-fetching. Việc đặt chung các action với các component liên quan vào nhau nghe có vẻ hợp lí, nhưng data-fetching là một global action và không thể gắn vào một component duy nhất.

Hầu hết các developers hiện nay sử dụng Redux Thunk để xử lý data-fetching với redux. Một action khi được "thunk" sẽ trông như sau:

/* actions.js */

function fetchData() {
	return function thunk(dispatch) {
		// Load something asynchronously.
		fetch('https://someurl.com/somendpoint', function callback(data) {
			// Add the data to the store.
			dispatch(dataLoaded(data));
		});
	}
}

Đây là một ý tưởng tuyệt vời cho phép data-fetching từ actions, nhưng nó có hai điểm yếu: việc test function sẽ rất khó, và về mặt ý tưởng, data-fetching trong actions có vẻ không hợp lý.

 Lợi ích lớn nhất của Redux là một cái máy tạo action thuần, rất dễ test. Khi trả lại một thunk từ một action, bạn sẽ phải gọi đúp action đó, thử dispatch function, ...

Gần đây, một cách tiếp cận mới làm khuấy động thế giới React: react-saga. React-saga tận dụng Esnext generator functions để làm asynchronous code trông giống synchronous code, và làm asynchronous code dễ test hơn. Ý tưởng đằng sau là chia thread trong ứng dụng của bạn để xử lý asynchronous code mà không động đến phần còn lại của code.

Ví dụ như:

/* sagas.js */

import { call, take, put } from 'redux-saga/effects';

// The asterisk behind the function keyword tells us that this is a generator.
function* fetchData() {
	// The yield keyword means that we'll wait until the (asynchronous) function
	// after it completes.
	// In this case, we wait until the FETCH_DATA action happens.
	yield take(FETCH_DATA);
	// We then fetch the data from the server, again waiting for it with yield
	// before continuing.
	var data = yield call(fetch, 'https://someurl.com/someendpoint');
	// When the data has finished loading, we dispatch the dataLoaded action.
	put(dataLoaded(data));
}

Source code trên trông đẹp hơn hẳn, tránh hoàn toàn callback hell, và trên hết là ưu điểm cực kì dễ test. Giờ bạn sẽ hỏi sao lại dễ test? Lý do sẽ liên quan đến khả năng test "những ảnh hưởng" redux-saga exports mà không cần chúng hoàn chỉnh.

Những ảnh hưởng này chúng ta import trên cùng của file là công cụ để xử lý giúp chúng ta tiếp cận dễ dàng với redux code:

  • put() dispatch một action từ saga.
  • take() tạm dừng saga cho đến khi một action xảy ra.
  • select() nhận được một phần của redux state (ví dụ như mapStateToProps).
  • call() gọi function truyền vào là tham số đầu tiên với những tham số còn lại.

Tại sao những ảnh hưởng này lại có ich? Chúng ta hãy xem khi test ví dụ trên sẽ trông như thế này:

/* sagas.test.js */

var sagaGenerator = fetchData();

describe('fetchData saga', function() {
	// Test that our saga starts when an action is dispatched,
	// without having to simulate that the dispatch actually happened!
	it('should wait for the FETCH_DATA action', function() {
		expect(sagaGenerator.next()).to.equal(take(FETCH_DATA));
	});

	// Test that our saga calls fetch with a specific URL,
	// without having to mock fetch or use the API or be connected to a network!
	it('should fetch the data from the server', function() {
		expect(sagaGenerator.next()).to.equal(call(fetch, 'https://someurl.com/someendpoint'));
	});

	// Test that our saga dispatches an action,
	// without having to have the main application running!
	it('should dispatch the dataLoaded action when the data has loaded', function() {
		expect(sagaGenerator.next()).to.equal(put(dataLoaded()));
	});
});

Esnext generator (từ phải qua trái) sẽ không chạy quá từ khóa yield cho đến khi generator.next() được gọi, sau đó nó chạy function còn lại, cho đến khi nó lại gặp yield  tiếp theo ! Bằng việc sử dụng hiệu ứng redux-saga, chúng ta có thể test asynchronous dễ dàng mà không cần phụ thuộc vào test network.

Chúng ta cho các test file và các file cần test vào cùng một thư mục để đảm bảo tất cả các file liên quan đến một component sẽ ở trong cùng một folder, kể cả khi test.

Nếu bạn nghĩ chỉ được lợi có từng đó khi sử dụng redux-saga thì bạn đã nhầm! Thực chất, làm data-fetching dễ dàng hơn và dễ test hơn  chỉ là lợi ích nhỏ nhất từ việc sử dụng redux-saga.

Sử dụng Redux-Saga như một "liên kết"

Các components giờ được tách biệt. Chúng không liên quan đến style hoặc logic khác. 

Giả sử một  Clock và một  Timer component, khi một button trên Clock được ấn, chúng ta muốn chạy Timer, và khi button stop trên timer được ấn, chúng ta muốn hiển thị thời gian trên Clock.

Thông thường, bạn sẽ làm như thế này:

/* Clock.jsx */

import { startTimer } from '../Timer/actions';

class Clock extends React.Component {
	render() {
		return (
			/* … */
			<button onClick={this.props.dispatch(startTimer())} />
			/* … */
		);
	}
}
/* Timer.jsx */

import { showTime } from '../Clock/actions';

class Timer extends React.Component {
	render() {
		return (
			/* … */
			<button onClick={this.props.dispatch(showTime(currentTime))} />
			/* … */
		);
	}
}

Giờ thì bạn không thể dùng hai component này độc lập với nhau và việc tái sử dụng chúng gần như là không thể!

Thay vào đó, chúng ta có thể sử dụng redux-saga như "liên kết" giữa hai component này. Bằng việc chờ action cụ thể, chúng ta có thể xử lý theo nhiều cách khác nhau, (tuỳ app) và những component trên giờ có thể tái sử dụng được.

Thử sửa lại code như sau:

/* Clock.jsx */

import { startButtonClicked } from '../Clock/actions';

class Clock extends React.Component {
	/* … */
	<button onClick={this.props.dispatch(startButtonClicked())} />
	/* … */
}
/* Timer.jsx */

import { stopButtonClicked } from '../Timer/actions';

class Timer extends React.Component {
	/* … */
	<button onClick={this.props.dispatch(stopButtonClicked(currentTime))} />
	/* … */
}

Bạn có nhận thấy mỗi component chỉ chạy riêng rẽ và import action của riêng nó thôi không?

Giờ thì sử dụng saga để nối chúng vào nhau. 

/* sagas.js */

import { call, take, put, select } from 'redux-saga/effects';

import { showTime } from '../Clock/actions';
import { START_BUTTON_CLICKED } from '../Clock/constants';
import { startTimer } from '../Timer/actions';
import { STOP_BUTTON_CLICKED } from '../Timer/constants';

function* clockAndTimer() {
	// Wait for the startButtonClicked action of the Clock
	// to be dispatched.
	yield take(START_BUTTON_CLICKED);
	// When that happens, start the timer.
	put(startTimer());
	// Then, wait for the stopButtonClick action of the Timer
	// to be dispatched.
	yield take(STOP_BUTTON_CLICKED);
	// Get the current time of the timer from the global state.
	var currentTime = select(function (state) { return state.timer.currentTime });
	// And show the time on the clock.
	put(showTime(currentTime));
}

Đẹp hơn đúng không?

Tóm tắt

Bạn nên nhớ những key chính sau:

  • Phân biệt giữa container và component
  • Cấu trúc file theo tính năng
  • Dùng CSS module và PostCSS Auto Reset
  • Dùng redux-saga để
    • luồng xử lý async dễ đọc và dễ test hơn.
    • gắn các component độc lập lại với nhau.

Bài viết được dịch lại từ nguồn : https://www.smashingmagazine.com/2016/09/how-to-scale-react-applications?utm_source=mybridge&utm_medium=email&utm_campaign=read_more