Học lập trình React

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ả ReduxRelay 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).

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

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.

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.

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.

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.

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.

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,
      },
    }];
  }

  // Để Relay tạo một optimistic update, chúng ta tạo một response trá hình
  // từ server. Rất đơn giản.
  getOptimisticResponse() {
    return {
      changedTodo: {
        id: this.props.id,
        text: this.props.text,
      },
    };
  }
}

Giờ thì mutation này có thể được dispatch tương tự như một action

Relay.Store.commitUpdate(
  new ChangeTodoTextMutation({
    id: this.props.todo.id,
    text: text,
  }),
);

Chúng ta có thể truyền thêm callback vào  commitUpdate để phản hồi lại mutation thành công hay thất bại. Trong trường hợp nào thì rollback sẽ luôn tự động cập nhật nếu thất bại.

Relay.Store.commitUpdate(
  new ChangeTodoTextMutation({
    id: this.props.todo.id,
    text: text,
  }), {
   onFailure: () => {
     console.error('error!');
   },
   onSuccess: (response) => {
     console.log('success!')
   }
});

Mutation DSL cũng bị coi là một điểm yếu của Relay. Đôi lúc tìm được chính xác query mà Relay trông đợi và các tham số truyền vào không phải dễ. Relay nếu sử dụng đúng cách sẽ giải quyết được nhiều tình huống phức tạp, như áp dụng mutation lên item trong các thành phần của một list data trong một page nào đó. Dù vậy, những thành viên bảo trì Relay đang thực hiện cải thiện viết mutation bằng việc thêm vào một lớp API thấp hơn (low level API). 

Làm việc với paginate data (data được phân page)

Redux

Có nhiều cách để phân trang trong Redux - nhiều như làm bên RESTful API thôi. Đầu tiên là tạo một reducer giữ thông tin số lượng data đã được gọi và số trang tiếp theo.

Ví dụ thực tiễn áp dụng phân trang của Redux có trong Redux github.

Do Redux không quy chuẩn server API nên framework này không thể xử lý phân trang tự động được. Nhưng cũng vì thế mà bạn có thể làm việc với API và với nhiều quy chuẩn khác nhau và lưu trữ data trong store Redux

Relay

GraphQl API có quan niệm Connectionmột khái niệm trừu tượng của danh sách data. Vấn đề then chốt là tất cả connection trong Relay GraphQL chấp nhận tham số phân trang và tất cả item của connection có một cursor, được sử dụng như một con trỏ để phân trang connection liên quan đến item. Connection có trường pageInfo bao gồm thông tin có liên quan đến trang tiếp theo.

Relay sẽ làm hầu hết những công việc nặng nhọc này dựa vào API theo chuẩn. Chúng ta chỉ việc update tham số và nó tự động gọi item chúng ta chưa có.

Để dùng cursor, chúng ta sử dụng các biến của Relay Container (có thể thay đổi trong Container luôn). Những biến này có thể được truyền vào GraphQL fragment

Sau đây là một ví dụ đơn giản:

class PaginatedTodoList extends React.Component {
  nextPage() {
    const lastElement = this.props.user.todos.edges.length - 1;
    this.setVariables({
      after: this.props.user.todos.edges[lastElement].cursor;
    });
  }

  render() {
    return (
      <div>
        <TodoList todos={this.props.user.todos} />
        {this.props.user.todos.pageInfo.hasNextPage ?
        <button onClick={this.nextPage} :
        <div>Last page</div>}
      </div>
    );
  }
}

const PaginatedTodoListContainer = Relay.createContainer(PaginatedTodoList, {
  fragments: {
    user: () => Relay.QL`
      fragment on User {
        todos(first: 10, after: $after) {
          edges {
            cursor
          }
          ${TodoList.getFragment('todos')}
          pageInfo {
            hasNextPage
          }
        }
      }
    `,
  },
  initialVariables: {
    after: null,
  },
});

Khi người dùng ấn vào button để load thêm item, chúng ta chỉ viêc update biến  after tự động gọi data còn thiếu và cập nhật danh sách.

Kết luận

Cả hai thư viện đều đã được test kĩ càng bởi nhiều công ty lớn. Relay cung cấp nhiều chức năng hơn nên hạn chế khả năng tùy chỉnh backend hơn và phụ thuộc vào GraphQL API. Redux thì rất linh hoạt nhưng vì thế mà bạn phải viết nhiều code hơn.

Bài viết được dịch từ nguồn: https://www.reindex.io/blog/redux-and-relay/