Redux hay Relay?
Nếu bạn là dev phát triển web sử dụng React thì kiểu gì cũng sẽ có lúc bạn phải giải bài toán làm sao kiểm soát được tất cả state của component ở client-side. Web hiện đại không thể chờ đợi được phản ứng từ server và cũng không thể lúc nào cũng gọi lại data từ database khi thay đổi ...
Nếu bạn là dev phát triển web sử dụng React thì kiểu gì cũng sẽ có lúc bạn phải giải bài toán làm sao kiểm soát được tất cả state của component ở client-side. Web hiện đại không thể chờ đợi được phản ứng từ server và cũng không thể lúc nào cũng gọi lại data từ database khi thay đổi trang. Do đó, lớp quản lý state được sinh ra và tính tới thời điểm bài viết, hai thư viện được dùng nhiết nhất để quản lý lớp này là Redux và Relay. Bài viết sẽ phân tích sự khác nhau giữa hai thư viện này.
Tổng quan kiến trúc
Cả Redux và Relay lấy cảm hứng từ thiết kế của Flux, một kiến trúc kiểu mẫu để thiết kế web app. Ý tưởng nền tảng của Flux là luôn để data chạy một chiều từ các Store đến những React component. Component gọi Action Creators, chúng làm nhiệm vụ dispatch các Action mà Stores đang theo dõi. Flux được giới thiệu bởi Facebook nhưng họ không cung cấp một thư viện cho nó. Nhưng ý tưởng của Flux thì lại được cộng đồng đón nhận và phát triển rất mãnh liệt, thậm chí có những kiểu áp dụng còn được tùy biến theo nhu cầu của từng doanh nghiệp. Đặc biệt là Flux rất hợp để đi với React vì nó cũng ngăn không cho data xuất ngược trực tiếp từ component về store mà chuyển cho action dispatch đảm nhiệm.
REDUX
Redux hầu như lấy rất ít ý tưởng từ mẫu thiết kế kiểu Flux. Việc tóm gọn lại nhiều Store trong Flux về đúng một Store trung tâm và xử lý các actions qua Reducer khiến quản lý state dễ dàng hơn. Điều đặc biệt là Reducer là các function thuần nhận biến là một state và trả về state mới ( không làm thay đổi state của một component như trong thiết kế của Flux). Redux Store có thể chứa bất kì data nào và Redux cũng không quan tâm đến nguồn data.
Redux là một thư viện nhỏ gọn nên nếu bạn muốn, bạn có thể thêm các middleware để xử ý thêm các công đoạn khác (rất nhiều middleware mã nguồn mở bạn có thể bổ sung vào hoàn toàn miễn phí).
RELAY
Relay thì thừa hưởng nhiều hơn từ Flux. Cũng có store trung tâm, mọi thay đổi đều thông qua actions (Relay gọi là Mutation). Nhưng Relay không cho dev kiểm soát nội dung của Store mà thay vào đó, Relay dựa vào sự trợ giúp từ GraphQL query để tự động query những yêu cầu cần thiết của các component trong cây component hiện tại. Store có thể được chỉnh thông qua việc thay đổi API nhưng tất cả mutation này sẽ tương ứng với mutation của server. Điểm khác biệt so với Redux là Relay store chỉ lưu data tương ứng ở server trong Relay và server đó bắt buộc phải có GraphQL API.
Relay cung cấp rất nhiều tính năng bao gòm gọi data từ CSDL và đảm bảo rằng chỉ có những data được yêu cầu thì sẽ được xuất. Relay hỗ trợ tốt pagination (chia trang) và đặc biệt là những trang scroll vĩnh viễn (cứ scroll hết trang là load trang mới). Relay mutation có thể update, báo cáo trạng thái và rollback (quay lại thời điểm trước).
Liên kết Component
Redux
Cả hai đều có thể liên kết tốt các React Component. Redux thì ít phụ thuộc vào React hơn mà có thể dùng kèm các thư viện view khác thoải mái (Pre-act hoặc Deku) nhưng Relay thì phải phụ thuộc vào React (hoặc React Native). Mặc dù vậy, nếu muốn, bạn vẫn có thể lấy các lớp component từ Relay và sử dụng ngoài React.
Redux khuyến khích việc trình bày và xử lý logic data thông qua khái niệm về components ngu (dumb) và khôn (smart). Thường thì component khôn sẽ được tạo bởi Redux, store sẽ nghe state của chung và dispatch actions tương ứng còn component ngu là React component bình thường. Component khôn sẽ truyền data vào component ngu bằng function và qua props. Thường thì hệ thống component sẽ ưu tiên thiết kế càng nhiều component ngu càng tốt và càng ít component khôn càng tốt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { connect } from 'react-redux'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const VisibleTodoList = connect(
mapStateToProps,
)(TodoList)
export default VisibleTodoList
|
Redux yêu cầu một component hạng cao nhất gọi là Provider chuyên chịu trách nhiệm truyền props cho các Redux component khôn.
1
2
3
4
5
6
7
8
|
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
|
Relay
Trong Relay thì hầu hết component sẽ là khôn. Relay bọc React components trong Relay Container. Container thông báo data cần để render với GraphQL fragment. Container có thể tạo fragment từ component con, nhưng các yêu cầu thường là không rõ ràng. Bằng cách này, container bị tách khỏi nhau và bạn có thể dùng lại hoặc thay đổi chúng mà không lo liên lụy đến data truyền vào. Fragment mà được giải quyết sẽ được truyền vào component qua props.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Todo extends React.Component {
render() {
return (
<div>{this.props.todo.id} {this.props.todo.text}</div>
);
}
}
const TodoContainer = Relay.createContainer(Todo, {
fragments: {
todos: () => Relay.QL`
fragment on Todo {
id,
text
}
`,
}
})
|
Các fragment trong cây component được tạo cùng nhau vào một query. Data mà được gọi từ trước sẽ được điều chỉnh cho phù hợp với query nếu đủ lượng data cần thiết, không có query thực sự nào bị thực thi thêm.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const TodoListContainer = Relay.createContainer(Todo, {
fragments: {
todoList: () => Relay.QL`
fragment on TodoConnection {
count,
edges {
node {
${TodoContainer.getFragment('todo')}
}
}
}
`,
},
})
|
Relay có Component ở tầng gốc gọi là RootContainer làm nhiệm vụ như một cổng vào (entry point). RootContainer yêu cầu một Container và một Route. Relay Route xác định khởi tạo query, query này sẽ được tập hợp cùng fragment của cả cây component đó thành một query root. Do các cây component giống nhau nhưng có thể có các data khởi nguồn khác nhau nên bắt buộc phải có Route, ví dụ như một <TodoList /> component có thể render toàn bộ danh sách việc phải làm cho một team hoặc một người. Route có thể nhận parameter (tham số), ví dụ một object id có thể được truyền vào.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class TodoRoute extends Relay.Route {
static routeName = 'TodoRoute';
static queries = {
todoById: () => Relay.QL`
query {
todoById(id: $id)
}
`
};
static paramDefinitions = {
id: { required: true }
};
}
render(
<Relay.RootContainer
Component={SingleTodoContainer}
route={new TodoListRoute} />,
document.getElementById('root')
)
|
Mutations
Thay đổi data trong store thường thấy trong quản lý lớp data server-side. Chúng ta luôn muốn phản hồi lại actions của người dùng càng nhanh càng tốt (optimistic update), sau đó, chúng ta đợi server trả lại kết quả của data thực sự được gọi và xác nhận lại nếu lấy data thành công.
Chúng ta thử làm một mutation thay đổi một TODO item, cập nhập trên server và rollback nếu nó thất bại.
Redux
Chúng ta sử dụng thunk middleware để thực hiện async actions. Chúng ta sẽ dispatch thử một optimistic change trước, sau đó hoặc là xác nhận nó, hoặc là rollback nó.
function todoChange(id, text, isOptimistic) { return { type: TODO_CHANGE, todo: { id, text, isOptimistic } }; } function editTodo(id, text) { return (dispatch, getState) => { const oldTodo = getState().todos[id]; // Perform optimistic update dispatch(todoChange(id, text, true)) fetch(`/todo/${id}`, { method: 'POST' }).then((result) => { if (result.code === '200') { // Confirm update dispatch(todoChange(id, text, false)) } else { dispatch(todoChange(oldTodo.id, oldTodo.text, false)) } }) } } // Trong store handler case TODO_CHANGE: return { ...state, todos: { ...state.todos, [id]: { id, text, isOptimistic } } } ) // Giờ thì dispatch store.dispatch(editTodo(todo.id, todo.text));
Relay
Relay thì lại không tùy chỉnh được actions. Thay vào đó, ta khởi tạo Mutation sử dụng Relay mutation DSL. Một điều đáng lưu ý là GraphQL mutation hoạt động rất đặc biệt – mutation là một operation (tạm dịch là thực thi) trước và sau đó mới là một query, nên chúng ta có thể request data với GraphQL mutation giống cách làm với GraphQL query. Chúng ta có thể request data thay đổi để Relay biết phải update cái gì.
class ChangeTodoTextMutation extends Relay.Mutation { // Lấy tên của mutation trên server để chúng ta có thể gọi server getMutation() { return Relay.QL`mutation{ updateTodo }`; } // Hướng props truyền vào mutation cho server input object getVariables() { return { id: this.props.id, text: this.props.text, }; } // Lấy query dựa vào kết quả của payload cùng tất cả data thay đổi getFatQuery() { return Relay.QL` fragment on _TodoPayload { changedTodo { id text } } `; } // Xác định thực sự Relay nên thay đổi gì trong store. Trường hợp này là // item trong `changedTodo` element trong kêt quả phải giống // với item trong store thông qua id sau đó update item tương ứng trong store getConfigs() { return [{ type: 'FIELDS_CHANGE', fieldIDs: { changedTodo: this.props.id, }
Vấn đề này hơi khoai đấy.