Làm cách nào để hiển thị chỉ báo tải trong ứng dụng React Redux khi tìm nạp dữ liệu? [đóng cửa]


107

Tôi mới sử dụng React / Redux. Tôi sử dụng phần mềm trung gian api tìm nạp trong ứng dụng Redux để xử lý các API. Đó là ( redux-api-middleware ). Tôi nghĩ rằng đó là cách tốt để xử lý các hành động api không đồng bộ. Nhưng tôi thấy một số trường hợp không thể tự giải quyết được.

Như trang chủ ( Vòng đời ) nói, vòng đời API tìm nạp bắt đầu bằng việc gửi một hành động CALL_API kết thúc bằng việc gửi một hành động FSA.

Vì vậy, trường hợp đầu tiên của tôi là hiển thị / ẩn trình tải trước khi tìm nạp các API. Phần mềm trung gian sẽ gửi một hành động FSA ở đầu và gửi một hành động FSA vào cuối. Cả hai hành động đều được nhận bởi các bộ giảm thiểu mà chỉ thực hiện một số xử lý dữ liệu bình thường. Không có hoạt động giao diện người dùng, không có hoạt động nào nữa. Có lẽ tôi nên lưu trạng thái xử lý ở trạng thái sau đó kết xuất chúng khi cửa hàng cập nhật.

Nhưng làm thế nào? Một luồng thành phần phản ứng trên toàn bộ trang? điều gì xảy ra với việc cập nhật cửa hàng từ các hành động khác? Ý tôi là chúng giống các sự kiện hơn là trạng thái!

Thậm chí trong trường hợp tồi tệ hơn, tôi nên làm gì khi phải sử dụng hộp thoại xác nhận gốc hoặc hộp thoại cảnh báo trong ứng dụng redux / react? Chúng nên được đặt ở đâu, hành động hay bộ giảm bớt?

Lời chúc tốt nhất! Mong hồi âm.


1
Đã cuộn lại chỉnh sửa cuối cùng cho câu hỏi này vì nó đã thay đổi toàn bộ điểm của câu hỏi và câu trả lời bên dưới.
Gregg B

Một sự kiện là sự thay đổi trạng thái!
企业 应用 架构 模式 大师

Hãy nhìn vào questrar. github.com/orar/questrar
Orar

Câu trả lời:


152

Ý tôi là chúng giống các sự kiện hơn là trạng thái!

Tôi sẽ không nói như vậy. Tôi nghĩ rằng các chỉ báo tải là một trường hợp tuyệt vời của giao diện người dùng dễ dàng được mô tả như một hàm của trạng thái: trong trường hợp này là của một biến boolean. Mặc dù câu trả lời này đúng, nhưng tôi muốn cung cấp một số mã đi kèm với nó.

Trong asyncví dụ trong repo Redux , trình giảm thiểu cập nhật một trường có tênisFetching :

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: true,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: false,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

Thành phần sử dụng connect()từ React Redux để đăng ký trạng thái của cửa hàng và trả về isFetchingnhư một phần của mapStateToProps()giá trị trả về để nó có sẵn trong các đạo cụ của thành phần được kết nối:

function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Cuối cùng, thành phần sử dụng isFetchingprop trong render()hàm để hiển thị nhãn "Đang tải ..." (có thể hình dung nhãn này thay vào đó là một con quay):

{isEmpty
  ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
  : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
      <Posts posts={posts} />
    </div>
}

Thậm chí trong trường hợp tồi tệ hơn, tôi nên làm gì khi phải sử dụng hộp thoại xác nhận gốc hoặc hộp thoại cảnh báo trong ứng dụng redux / react? Chúng nên được đặt ở đâu, hành động hay bộ giảm bớt?

Bất kỳ tác dụng phụ nào (và hiển thị hộp thoại chắc chắn là tác dụng phụ) không thuộc về bộ giảm tốc. Hãy coi những bộ giảm tốc là “những người xây dựng nhà nước” thụ động. Họ không thực sự "làm" mọi thứ.

Nếu bạn muốn hiển thị cảnh báo, hãy thực hiện việc này từ một thành phần trước khi thực hiện một hành động hoặc thực hiện việc này từ một người tạo hành động. Vào thời điểm một hành động được đưa ra, đã quá muộn để thực hiện các tác dụng phụ để đáp ứng với nó.

Đối với mọi quy tắc, có một ngoại lệ. Đôi khi logic tác dụng phụ của bạn phức tạp đến mức bạn thực sự muốn ghép chúng với các loại hành động cụ thể hoặc với các bộ giảm cụ thể. Trong trường hợp này, hãy xem các dự án nâng cao như Redux SagaRedux Loop . Chỉ làm điều này khi bạn cảm thấy thoải mái với vani Redux và gặp vấn đề thực sự về các tác dụng phụ rải rác mà bạn muốn kiểm soát tốt hơn.


16
Điều gì sẽ xảy ra nếu tôi có nhiều lần tìm nạp? Tuy nhiên, một biến sẽ không đủ.
philk

1
@philk nếu bạn có nhiều lần tìm nạp, bạn có thể nhóm chúng Promise.allthành một lời hứa và sau đó thực hiện một hành động cho tất cả các lần tìm nạp. Hoặc bạn phải duy trì nhiều isFetchingbiến trong trạng thái của mình.
Sebastien Lorber

2
Hãy xem kỹ ví dụ mà tôi liên kết tới. Có nhiều hơn một isFetchinglá cờ. Nó được thiết lập cho mọi tập hợp các đối tượng đang được tìm nạp. Bạn có thể sử dụng thành phần giảm thiểu để thực hiện điều đó.
Dan Abramov

3
Lưu ý rằng nếu yêu cầu không thành công và RECEIVE_POSTSkhông bao giờ được kích hoạt, dấu hiệu tải sẽ vẫn ở nguyên vị trí trừ khi bạn đã tạo một số loại thời gian chờ để hiển thị error loadingthông báo.
James111,

2
@TomiS - Tôi đã đưa vào danh sách đen rõ ràng tất cả các thuộc tính isFetching của mình khỏi bất kỳ tính năng redux nào tôi đang sử dụng.
duhseekoh

22

Câu trả lời tuyệt vời Dan Abramov! Chỉ muốn nói thêm rằng tôi đã làm nhiều hơn hoặc ít hơn chính xác điều đó trong một trong các ứng dụng của mình (giữ isFetching dưới dạng boolean) và cuối cùng phải đặt nó thành một số nguyên (kết thúc bằng cách đọc là số lượng yêu cầu chưa xử lý) để hỗ trợ đồng thời nhiều các yêu cầu.

với boolean:

yêu cầu 1 bắt đầu -> bật spinner -> yêu cầu 2 bắt đầu -> yêu cầu 1 kết thúc -> tắt spinner -> yêu cầu 2 kết thúc

với số nguyên:

yêu cầu 1 bắt đầu -> bật spinner -> yêu cầu 2 bắt đầu -> yêu cầu 1 kết thúc -> yêu cầu 2 kết thúc -> tắt spinner

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching + 1,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching - 1,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

2
Điều này là hợp lý. Tuy nhiên, thông thường bạn cũng muốn lưu trữ một số dữ liệu mà bạn tìm nạp ngoài cờ. Tại thời điểm này, bạn sẽ cần phải có nhiều hơn một đối tượng có isFetchingcờ. Nếu bạn nhìn kỹ vào ví dụ mà tôi đã liên kết, bạn sẽ thấy rằng không có một đối tượng isFetchedmà có nhiều đối tượng: một đối tượng trên mỗi subreddit (chính là những gì đang được tìm nạp trong ví dụ đó).
Dan Abramov

2
Oh. vâng tôi đã không nhận thấy điều đó. tuy nhiên trong trường hợp của tôi, tôi có một mục nhập isFetching toàn cục trong trạng thái và một mục nhập bộ nhớ cache nơi dữ liệu đã tìm nạp được lưu trữ và đối với mục đích của tôi, tôi chỉ thực sự quan tâm rằng một số hoạt động mạng đang diễn ra, điều đó không thực sự quan trọng
Nuno Campos

4
Vâng! Nó phụ thuộc vào việc bạn muốn hiển thị chỉ báo tìm nạp ở một hay nhiều nơi trong giao diện người dùng. Trên thực tế, bạn có thể kết hợp hai cách tiếp cận và có cả một toàn cục fetchCountercho một số thanh tiến trình ở đầu màn hình và một số isFetchingcờ cụ thể cho danh sách và trang.
Dan Abramov

Nếu tôi có Yêu cầu ĐĂNG trong nhiều tệp, tôi sẽ đặt trạng thái isFetching như thế nào để theo dõi trạng thái hiện tại của nó?
user989988

13

Tôi muốn thêm một cái gì đó. Ví dụ trong thế giới thực sử dụng một trường isFetchingtrong cửa hàng để thể hiện khi nào một bộ sưu tập các mục đang được tìm nạp. Mọi bộ sưu tập đều được tổng quát hóa thành một bộ paginationgiảm thiểu có thể được kết nối với các thành phần của bạn để theo dõi trạng thái và cho biết liệu bộ sưu tập có đang tải hay không.

Tình cờ xảy ra với tôi rằng tôi muốn tìm nạp thông tin chi tiết cho một thực thể cụ thể không phù hợp với mẫu phân trang. Tôi muốn có một trạng thái đại diện nếu các chi tiết đang được tìm nạp từ máy chủ nhưng tôi cũng không muốn có một bộ giảm chỉ cho điều đó.

Để giải quyết vấn đề này, tôi đã thêm một trình giảm thiểu chung khác được gọi là fetching. Nó hoạt động theo kiểu tương tự như công cụ giảm phân trang và trách nhiệm chỉ là xem một tập hợp các hành động và tạo ra trạng thái mới với các cặp [entity, isFetching]. Điều đó cho phép rút connectgọn thành phần bất kỳ và biết liệu ứng dụng hiện có đang tìm nạp dữ liệu không chỉ cho một bộ sưu tập mà cho một thực thể cụ thể hay không.


2
Cảm ơn vì câu trả lời! Xử lý việc tải các mặt hàng riêng lẻ và trạng thái của chúng hiếm khi được thảo luận!
Gilad Peleg

Khi tôi có một thành phần phụ thuộc vào hành động của một thành phần khác, một cách nhanh chóng và bẩn thỉu là trong mapStateToProps của bạn, hãy kết hợp chúng lại như sau: isFetching: posts.isFetching || comments.isFetching - bây giờ bạn có thể chặn tương tác của người dùng đối với cả hai thành phần khi một trong hai thành phần đang được cập nhật.
Philip Murphy

5

Tôi đã không xảy ra câu hỏi này cho đến bây giờ, nhưng vì không có câu trả lời nào được chấp nhận nên tôi sẽ ngả mũ. Tôi đã viết một công cụ cho công việc này: react-loader-factory . Nó diễn ra nhiều hơn một chút so với giải pháp của Abramov, nhưng mang tính mô-đun và tiện lợi hơn, vì tôi không muốn phải suy nghĩ sau khi viết nó.

Có bốn phần lớn:

  • Mẫu xuất xưởng: Điều này cho phép bạn nhanh chóng gọi cùng một hàm để thiết lập trạng thái nào có nghĩa là "Đang tải" cho thành phần của bạn và hành động nào cần gửi. (Điều này giả định rằng thành phần chịu trách nhiệm bắt đầu các hành động mà nó chờ đợi.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: Thành phần mà Nhà máy sản xuất là "thành phần có thứ tự cao hơn" (giống như những gì connect()trả về trong Redux), vì vậy bạn có thể gắn nó vào những thứ hiện có của mình.const LoadingChild = loaderWrapper(ChildComponent);
  • Tương tác Hành động / Bộ giảm: Trình bao bọc kiểm tra xem liệu một bộ giảm tốc mà nó được cắm vào có chứa các từ khóa yêu cầu nó không chuyển đến thành phần cần dữ liệu hay không. Các hành động được gửi bởi trình bao bọc dự kiến ​​sẽ tạo ra các từ khóa liên quan ( ví dụ như cách redux-api-middleware gửi đi ACTION_SUCCESSACTION_REQUEST). (Tất nhiên, bạn có thể gửi các hành động ở nơi khác và chỉ cần giám sát từ trình bao bọc nếu bạn muốn.)
  • Throbber: Thành phần bạn muốn xuất hiện trong khi dữ liệu mà thành phần của bạn phụ thuộc vào chưa sẵn sàng. Tôi đã thêm một số div nhỏ vào đó để bạn có thể kiểm tra nó mà không cần phải tăng cường.

Bản thân mô-đun độc lập với redux-api-middleware, nhưng đó là những gì tôi sử dụng với nó, vì vậy đây là một số mã mẫu từ README:

Một thành phần có Trình tải bao bọc nó:

import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory';
import ChildComponent from './ChildComponent';

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);

const LoadingChild = loaderWrapper(ChildComponent);

const containingComponent = props => {
  // Do whatever you need to do with your usual containing component 

  const childProps = { someProps: 'props' };

  return <LoadingChild { ...childProps } />;
}

Một bộ giảm tốc để Bộ tải giám sát (mặc dù bạn có thể nối dây khác nếu muốn):

export function activeRequests(state = [], action) {
  const newState = state.slice();

  // regex that tests for an API action string ending with _REQUEST 
  const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
  // regex that tests for a API action string ending with _SUCCESS 
  const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);

  // if a _REQUEST comes in, add it to the activeRequests list 
  if (reqReg.test(action.type)) {
    newState.push(action.type);
  }

  // if a _SUCCESS comes in, delete its corresponding _REQUEST 
  if (sucReg.test(action.type)) {
    const reqType = action.type.split('_')[0].concat('_REQUEST');
    const deleteInd = state.indexOf(reqType);

    if (deleteInd !== -1) {
      newState.splice(deleteInd, 1);
    }
  }

  return newState;
}

Tôi hy vọng trong tương lai gần, tôi sẽ thêm những thứ như thời gian chờ và lỗi vào mô-đun, nhưng mô hình sẽ không khác biệt lắm.


Câu trả lời ngắn gọn cho câu hỏi của bạn là:

  1. Liên kết kết xuất với mã hiển thị - sử dụng trình bao bọc xung quanh thành phần bạn cần kết xuất với dữ liệu như tôi đã trình bày ở trên.
  2. Thêm một trình giảm thiểu khiến trạng thái của các yêu cầu xung quanh ứng dụng mà bạn có thể quan tâm có thể dễ dàng tiêu hóa, vì vậy bạn không phải suy nghĩ quá nhiều về những gì đang xảy ra.
  3. Sự kiện và trạng thái không thực sự khác nhau.
  4. Phần còn lại của trực giác của bạn có vẻ đúng với tôi.

4

Tôi có phải là người duy nhất nghĩ rằng các chỉ báo tải không thuộc về cửa hàng Redux không? Ý tôi là, tôi không nghĩ đó là một phần của trạng thái ứng dụng.

Bây giờ, tôi làm việc với Angular2 và những gì tôi làm là tôi có một dịch vụ "Đang tải" hiển thị các chỉ báo tải khác nhau thông qua RxJS BehaviourSubjects .. Tôi đoán cơ chế giống nhau, chỉ là tôi không lưu trữ thông tin trong Redux.

Người dùng của LoadingService chỉ cần đăng ký những sự kiện mà họ muốn nghe ..

Những người tạo hành động Redux của tôi gọi LoadingService bất cứ khi nào mọi thứ cần thay đổi. Các thành phần UX đăng ký với các vật thể quan sát được tiếp xúc ...


đây là lý do tại sao tôi thích ý tưởng về cửa hàng, nơi tất cả các hành động có thể được thăm dò (ngrx và redux-logic), dịch vụ không phải là chức năng, redux-logic - chức năng. Thoải mái đọc
srghma

20
Xin chào, kiểm tra lại hơn một năm sau, chỉ để nói rằng tôi đã rất sai lầm. Tất nhiên trạng thái UX thuộc về trạng thái ứng dụng. Tôi có thể ngu ngốc đến mức nào?
Spock

3

Bạn có thể thêm trình nghe thay đổi vào cửa hàng của mình, bằng cách sử dụng connect()từ React Redux hoặc store.subscribe()phương pháp cấp thấp . Bạn nên có chỉ báo tải trong cửa hàng của mình, bộ xử lý thay đổi cửa hàng sau đó có thể kiểm tra và cập nhật trạng thái thành phần. Sau đó, thành phần hiển thị trình tải trước nếu cần, dựa trên trạng thái.

alertconfirmkhông phải là một vấn đề. Họ đang chặn và cảnh báo thậm chí không nhận bất kỳ đầu vào nào từ người dùng. Với confirm, bạn có thể đặt trạng thái dựa trên những gì người dùng đã nhấp vào nếu lựa chọn của người dùng sẽ ảnh hưởng đến kết xuất thành phần. Nếu không, bạn có thể lưu lựa chọn dưới dạng biến thành phần để sử dụng sau này.


về mã cảnh báo / xác nhận, chúng nên được đặt ở đâu, các hành động hoặc bộ giảm bớt?
企业 应用 架构 模式 大师

Phụ thuộc vào những gì bạn muốn làm với chúng, nhưng thành thật mà nói, tôi sẽ đặt chúng trong mã thành phần trong hầu hết các trường hợp vì chúng là một phần của giao diện người dùng, không phải lớp dữ liệu.
Miloš Rašić

một số thành phần giao diện người dùng hoạt động bằng cách kích hoạt một sự kiện (sự kiện thay đổi trạng thái) thay vì chính trạng thái. Chẳng hạn như hoạt ảnh, hiển thị / ẩn trình tải trước. Bạn xử lý chúng như thế nào?
企业 应用 架构 模式 大师

Nếu bạn muốn sử dụng một thành phần không phản ứng trong ứng dụng phản ứng của mình, giải pháp thường được sử dụng là tạo thành phần phản ứng của trình bao bọc, sau đó sử dụng các phương thức vòng đời của nó để khởi tạo, cập nhật và hủy một phiên bản của thành phần không phản ứng. Hầu hết các thành phần như vậy sử dụng các phần tử giữ chỗ trong DOM để khởi tạo và bạn sẽ hiển thị các phần tử đó trong phương thức kết xuất của thành phần phản ứng. Bạn có thể đọc thêm về các phương pháp vòng đời tại đây: facebook.github.io/react/docs/component-specs.html
Miloš Rašić

Tôi có một trường hợp: khu vực thông báo ở góc trên bên phải, chứa một tin nhắn thông báo, mỗi tin nhắn hiển thị rồi biến mất sau 5 giây. Thành phần này nằm ngoài chế độ xem web, do ứng dụng gốc của máy chủ cung cấp. Nó cung cấp một số giao diện js như addNofication(message). Một trường hợp khác là trình tải trước cũng được cung cấp bởi ứng dụng gốc máy chủ và được kích hoạt bởi API javascript của nó. Tôi thêm một trình bao bọc cho các api đó, trong componentDidUpdatemột thành phần React. Làm cách nào để thiết kế các đạo cụ hoặc trạng thái của thành phần này?
企业 应用 架构 模式 大师

3

Chúng tôi có ba loại thông báo trong ứng dụng của mình, tất cả đều được thiết kế dưới dạng các khía cạnh:

  1. Chỉ báo tải (phương thức hoặc không phương thức dựa trên chống đỡ)
  2. Cửa sổ bật lên lỗi (phương thức)
  3. Thanh nhanh thông báo (không theo phương thức, tự đóng)

Cả ba thứ này đều ở cấp cao nhất của ứng dụng của chúng tôi (Chính) và được kết nối qua Redux như được hiển thị trong đoạn mã bên dưới. Các đạo cụ này kiểm soát hiển thị các khía cạnh tương ứng của chúng.

Tôi đã thiết kế một proxy xử lý tất cả các lệnh gọi API của chúng tôi, do đó tất cả các lỗi isFetching và (api) đều được dàn xếp với actionCreators mà tôi nhập vào proxy. (Ngoài ra, tôi cũng sử dụng webpack để đưa vào một mô hình của dịch vụ hỗ trợ cho nhà phát triển để chúng tôi có thể làm việc mà không phụ thuộc vào máy chủ.)

Bất kỳ vị trí nào khác trong ứng dụng cần cung cấp bất kỳ loại thông báo nào chỉ cần nhập hành động thích hợp. Snackbar & Error có các thông số để hiển thị thông báo.

@connect(
// map state to props
state => ({
    isFetching      :state.main.get('isFetching'),   // ProgressIndicator
    notification    :state.main.get('notification'), // Snackbar
    error           :state.main.get('error')         // ErrorPopup
}),
// mapDispatchToProps
(dispatch) => { return {
    actions: bindActionCreators(actionCreators, dispatch)
}}

) xuất lớp mặc định Main mở rộng React.Component {


Tôi đang thực hiện một thiết lập tương tự với việc hiển thị trình tải / thông báo. Tôi đang gặp vấn đề; bạn sẽ có một ý chính hoặc ví dụ về cách bạn hoàn thành những nhiệm vụ này?
Aymen

2

Tôi đang lưu các url như ::

isFetching: {
    /api/posts/1: true,
    api/posts/3: false,
    api/search?q=322: true,
}

Và sau đó tôi có một bộ chọn được ghi nhớ (thông qua chọn lại).

const getIsFetching = createSelector(
    state => state.isFetching,
    items => items => Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false
);

Để tạo url duy nhất trong trường hợp POST, tôi chuyển một số biến làm truy vấn.

Và nơi tôi muốn hiển thị chỉ báo, tôi chỉ cần sử dụng biến getFetchCount


1
Bạn có thể thay thế Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsebằng Object.keys(items).every(item => items[item])cách này.
Alexandre Annic

1
Tôi nghĩ bạn có ý somethay vì every, nhưng có, quá nhiều so sánh không cần thiết trong giải pháp được đề xuất đầu tiên. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.