Jak wyświetlić wskaźnik ładowania w aplikacji React Redux podczas pobierania danych? [Zamknięte]

107

Jestem nowy w React / Redux. Używam oprogramowania pośredniczącego Fetch API w aplikacji Redux do przetwarzania interfejsów API. To ( oprogramowanie pośredniczące redux-api ). Myślę, że to dobry sposób na przetwarzanie akcji asynchronicznego interfejsu API. Ale znajduję przypadki, których nie mogę rozwiązać samodzielnie.

Jak podaje strona główna ( Lifecycle ), cykl życia API pobierania zaczyna się od wysłania akcji CALL_API i kończy się wysłaniem akcji FSA.

Mój pierwszy przypadek to pokazywanie / ukrywanie modułu wstępnego ładowania podczas pobierania interfejsów API. Oprogramowanie pośredniczące wyśle ​​akcję FSA na początku i wyśle ​​akcję FSA na końcu. Obie akcje są odbierane przez reduktory, które powinny wykonywać tylko normalne przetwarzanie danych. Żadnych operacji interfejsu użytkownika, żadnych więcej operacji. Może powinienem zapisać stan przetwarzania w stanie, a następnie renderować je podczas aktualizacji sklepu.

Ale jak to zrobić? Przepływ komponentu reagującego na całą stronę? co się dzieje z aktualizacją sklepu z innych działań? Chodzi mi o to, że są bardziej jak wydarzenia niż stan!

Co gorsza, co mam zrobić, gdy muszę użyć natywnego okna dialogowego potwierdzenia lub okna dialogowego ostrzeżenia w aplikacjach redux / react? Gdzie należy je umieścić, działania lub redukcje?

Wszystkiego najlepszego! Życzę odpowiedzi.

企业 应用 架构 模式 大师
źródło
1
Cofnięto ostatnią edycję tego pytania, ponieważ zmieniło to cały punkt pytania i odpowiedzi poniżej.
Gregg B
Wydarzenie to zmiana stanu!
企业 应用 架构 模式 大师
Spójrz na questrar. github.com/orar/questrar
Orar

Odpowiedzi:

152

Chodzi mi o to, że są bardziej jak wydarzenia niż stan!

Nie powiedziałbym tego. Myślę, że wskaźniki ładowania to świetny przypadek interfejsu użytkownika, który można łatwo opisać jako funkcję stanu: w tym przypadku jest to zmienna boolowska. Chociaż ta odpowiedź jest prawidłowa, chciałbym podać kod, który będzie z nią zgodny.

W asyncprzykładzie w repozytorium Redux reduktor aktualizuje pole o nazwieisFetching :

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

Komponent korzysta connect()z React Redux do subskrybowania stanu sklepu i zwraca isFetchingjako część mapStateToProps()wartości zwracanej, więc jest dostępny w właściwościach połączonego komponentu:

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

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Wreszcie komponent używa isFetchingwłaściwości w render()funkcji do renderowania etykiety „Ładowanie ...” (która mogłaby być zamiast tego spinner):

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

Co gorsza, co mam zrobić, gdy muszę użyć natywnego okna dialogowego potwierdzenia lub okna dialogowego ostrzeżenia w aplikacjach redux / react? Gdzie należy je umieścić, działania lub redukcje?

Żadne efekty uboczne (a wyświetlenie okna dialogowego jest z pewnością efektem ubocznym) nie należą do reduktorów. Pomyśl o reduktorach jako o pasywnych „budowniczych państwa”. Tak naprawdę nie „robią” rzeczy.

Jeśli chcesz pokazać alert, zrób to z komponentu przed wywołaniem akcji lub zrób to z twórcy akcji. Zanim akcja zostanie wysłana, jest już za późno na wywołanie efektów ubocznych w odpowiedzi na nią.

Każda reguła ma wyjątek. Czasami logika efektów ubocznych jest tak skomplikowana, że ​​w rzeczywistości chcesz połączyć je albo z określonymi typami akcji, albo z określonymi redukcjami. W takim przypadku sprawdź zaawansowane projekty, takie jak Redux Saga i Redux Loop . Rób to tylko wtedy, gdy czujesz się komfortowo z Vanilla Redux i masz prawdziwy problem z rozproszonymi efektami ubocznymi, które chcesz, aby były łatwiejsze do opanowania.

Dan Abramov
źródło
16
Co się stanie, jeśli mam wiele pobrań? Wtedy jednak jedna zmienna nie wystarczyłaby.
philk
1
@philk, jeśli masz wiele pobrań, możesz je zgrupować Promise.allw jedną obietnicę, a następnie wysłać jedną akcję dla wszystkich pobierania. Lub musisz zachować wiele isFetchingzmiennych w swoim stanie.
Sebastien Lorber
2
Proszę uważnie przyjrzeć się przykładowi, do którego odsyłam. Jest więcej niż jedna isFetchingflaga. Jest ustawiana dla każdego zestawu pobieranych obiektów. Aby to zaimplementować, możesz użyć kompozycji reduktora.
Dan Abramov
3
Zwróć uwagę, że jeśli żądanie nie powiedzie się i RECEIVE_POSTSnigdy nie zostanie wyzwolone, znak ładowania pozostanie na miejscu, chyba że utworzyłeś jakiś czas oczekiwania na wyświetlenie error loadingwiadomości.
James111,
2
@TomiS - jawnie umieszczam na czarnej liście wszystkie moje właściwości isFetching z jakiejkolwiek trwałości Redux, której używam.
duhseekoh
22

Świetna odpowiedź Dan Abramov! Chcę tylko dodać, że robiłem mniej więcej dokładnie to w jednej z moich aplikacji (zachowując isFetching jako wartość logiczną) i skończyło się na tym, że musiałem uczynić ją liczbą całkowitą (która kończy się odczytaniem jako liczba zaległych żądań), aby obsługiwać wiele jednoczesnych upraszanie.

z wartością logiczną:

żądanie 1 zaczyna się -> spinner włączony -> żądanie 2 się zaczyna -> żądanie 1 się kończy -> spinner wyłączony -> żądanie 2 się kończy

z liczbą całkowitą:

żądanie 1 rozpoczęło się -> spinner włączony -> żądanie 2 się zaczęło -> żądanie 1 się kończyło -> żądanie 2 się kończyło -> spinner wyłączony

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
Nuno Campos
źródło
2
To rozsądne. Jednak najczęściej oprócz flagi chcesz również przechowywać niektóre dane, które pobierasz. W tym momencie będziesz potrzebować więcej niż jednego obiektu z isFetchingflagą. Jeśli przyjrzysz się uważnie przykładowi, z którym się połączyłem, zobaczysz, że nie ma jednego obiektu z isFetchedwieloma: jeden na subreddit (co jest właśnie pobierane w tym przykładzie).
Dan Abramov
2
O. tak, nie zauważyłem tego. jednak w moim przypadku mam jeden globalny wpis isFetching w stanie i wpis pamięci podręcznej, w którym przechowywane są pobrane dane, a dla moich celów naprawdę zależy mi tylko na tym, że dzieje się jakaś aktywność sieciowa, tak naprawdę nie ma znaczenia po co
Nuno Campos
4
Tak! Zależy to od tego, czy chcesz wyświetlać wskaźnik pobierania w jednym, czy w wielu miejscach w interfejsie użytkownika. W rzeczywistości możesz połączyć te dwa podejścia i mieć zarówno globalny fetchCounterpasek postępu u góry ekranu, jak i kilka konkretnych isFetchingflag dla list i stron.
Dan Abramov
Jeśli mam żądania POST w więcej niż jednym pliku, jak ustawić stan isFetching, aby śledzić jego bieżący stan?
user989988
13

Chciałbym coś dodać. Przykład ze świata rzeczywistego używa pola isFetchingw sklepie do reprezentowania czasu pobierania kolekcji elementów. Każda kolekcja jest uogólniana na paginationreduktor, który można połączyć z komponentami w celu śledzenia stanu i pokazania, czy kolekcja jest ładowana.

Zdarzyło mi się, że chciałem pobrać szczegóły dotyczące konkretnego podmiotu, który nie mieści się we wzorcu paginacji. Chciałem mieć stan reprezentujący, czy szczegóły są pobierane z serwera, ale także nie chciałem mieć reduktora tylko do tego.

Aby rozwiązać ten problem, dodałem inny ogólny reduktor o nazwie fetching. Działa podobnie jak reduktor paginacji i jego zadaniem jest po prostu obserwowanie zestawu działań i generowanie nowego stanu parami [entity, isFetching]. Pozwala to connectna redukcję do dowolnego komponentu i sprawdzenie, czy aplikacja aktualnie pobiera dane nie tylko dla kolekcji, ale dla określonej jednostki.

javivelasco
źródło
2
Dziękuję za odpowiedź! Rzadko omawia się obsługę załadunku poszczególnych przedmiotów i ich status!
Gilad Peleg
Kiedy mam jeden komponent, który zależy od działania innego, szybkie i brudne wyjście jest w twoim mapStateToProps, połącz je w ten sposób: isFetching: posts.isFetching || comments.isFetching - teraz możesz blokować interakcję użytkownika dla obu komponentów, gdy jeden z nich jest aktualizowany.
Philip Murphy
5

Do tej pory nie spotkałem się z tym pytaniem, ale ponieważ żadna odpowiedź nie zostanie przyjęta, wrzucę kapelusz. Napisałem narzędzie do tego właśnie zadania: reaktor-loader-fabryka . Trochę więcej się dzieje niż rozwiązanie Abramova, ale jest bardziej modułowe i wygodne, ponieważ nie chciałem myśleć po tym, jak to napisałem.

Istnieją cztery duże elementy:

  • Wzorzec fabryczny: Pozwala to szybko wywołać tę samą funkcję, aby ustawić, które stany oznaczają „Ładowanie” dla twojego komponentu i które akcje mają zostać wysłane. (Zakłada się, że komponent jest odpowiedzialny za rozpoczęcie działań, na które czeka).const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Opakowanie: komponent produkowany przez fabrykę jest „komponentem wyższego rzędu” (tak jak to, co connect()powraca w Redux), więc możesz go po prostu przykręcić do istniejących rzeczy.const LoadingChild = loaderWrapper(ChildComponent);
  • Interakcja akcja / reduktor: Opakowanie sprawdza, czy reduktor, do którego jest podłączony, zawiera słowa kluczowe, które nakazują mu nie przechodzić do komponentu, który potrzebuje danych. Oczekuje się, że akcje wysyłane przez opakowanie wygenerują skojarzone słowa kluczowe (na przykład sposób wysyłania oprogramowania pośredniego redux-api ACTION_SUCCESSi ACTION_REQUEST). (Oczywiście, jeśli chcesz, możesz wysłać akcje w inne miejsce i po prostu monitorować z opakowania).
  • Throbber: komponent, który ma się pojawiać, gdy dane, od których zależy twój komponent, nie są gotowe. Dodałem tam mały div, abyś mógł go przetestować bez konieczności konfigurowania go.

Sam moduł jest niezależny od oprogramowania pośredniczącego redux-api, ale właśnie z tym go używam, więc oto przykładowy kod z pliku README:

Komponent z opakowaniem Loadera:

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 } />;
}

Reduktor do monitorowania przez Loader (chociaż możesz go podłączyć inaczej, jeśli chcesz):

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

Spodziewam się, że w najbliższej przyszłości dodam do modułu takie rzeczy, jak przekroczenie limitu czasu i błąd, ale wzorzec nie będzie się bardzo różnił.


Krótka odpowiedź na Twoje pytanie brzmi:

  1. Powiąż renderowanie z renderowaniem kodu - użyj opakowania wokół komponentu, który chcesz renderować, z danymi, takimi jak ten, który pokazałem powyżej.
  2. Dodaj reduktor, który sprawi, że stan żądań w aplikacji, na której Ci zależy, będzie łatwo przyswajalny, dzięki czemu nie musisz zbyt intensywnie myśleć o tym, co się dzieje.
  3. Wydarzenia i stan tak naprawdę się nie różnią.
  4. Reszta twoich intuicji wydaje mi się poprawna.
Jasna gwiazda
źródło
4

Czy tylko ja myślę, że wskaźniki ładowania nie powinny być dostępne w sklepie Redux? Chodzi mi o to, że nie sądzę, że jest to część stanu aplikacji jako takiej ...

Teraz pracuję z Angular2 i to, co robię, to to, że mam usługę "Loading", która pokazuje różne wskaźniki ładowania za pośrednictwem RxJS BehaviourSubjects .. Myślę, że mechanizm jest taki sam, po prostu nie przechowuję informacji w Redux.

Użytkownicy usługi LoadingService po prostu subskrybują te zdarzenia, których chcą słuchać.

Twórcy moich akcji Redux wywołują usługę LoadingService, gdy tylko coś się zmieni. Komponenty UX subskrybują ujawnione obserwable ...

Spock
źródło
dlatego podoba mi się idea sklepu, w którym wszystkie akcje mogą być odpytywane (ngrx i redux-logic), usługa nie działa, redux-logic - funkcjonalna. Niezła lektura
srghma
20
Cześć, sprawdzam ponownie ponad rok później, żeby tylko powiedzieć, że bardzo się myliłem. Oczywiście stan UX należy do stanu aplikacji. Jak głupia mogłem być?
Spock,
3

Możesz dodać odbiorników zmian do swoich sklepów, korzystając connect()z React Redux lub metody niskiego poziomu store.subscribe(). Powinieneś mieć wskaźnik ładowania w swoim sklepie, który program obsługi zmian sklepu może następnie sprawdzić i zaktualizować stan komponentu. Następnie komponent renderuje moduł wstępnego ładowania, jeśli to konieczne, na podstawie stanu.

alerti confirmnie powinno stanowić problemu. Blokują, a alarmy nie wymagają nawet żadnych danych wejściowych od użytkownika. Za pomocą confirmmożna ustawić stan na podstawie tego, co kliknął użytkownik, jeśli wybór użytkownika powinien wpłynąć na renderowanie komponentu. Jeśli nie, możesz zapisać wybór jako zmienną składową komponentu do późniejszego wykorzystania.

Miloš Rašić
źródło
o kodzie alertu / potwierdzenia, gdzie należy je umieścić, akcje czy redukcje?
企业 应用 架构 模式 大师
Zależy od tego, co chcesz z nimi zrobić, ale szczerze mówiąc, w większości przypadków umieściłbym je w kodzie składowym, ponieważ są one częścią interfejsu użytkownika, a nie warstwą danych.
Miloš Rašić
niektóre komponenty UI działają poprzez wywołanie zdarzenia (zdarzenia zmiany statusu) zamiast samego statusu. Na przykład animacja, pokazywanie / ukrywanie modułu wstępnego ładowania. Jak je przetwarzasz?
企业 应用 架构 模式 大师
Jeśli chcesz użyć w swojej aplikacji reagującego komponentu, zwykle używanym rozwiązaniem jest utworzenie komponentu opakowującego reagującego, a następnie użycie jego metod cyklu życia do zainicjowania, zaktualizowania i zniszczenia instancji komponentu niereagującego. Większość takich komponentów używa elementów zastępczych w DOM do inicjalizacji, a ty wyrenderowałbyś je w metodzie renderowania komponentu reagującego. Możesz przeczytać więcej o metodach cyklu życia tutaj: facebook.github.io/react/docs/component-specs.html
Miloš Rašić
Mam przypadek: obszar powiadomień w prawym górnym rogu, który zawiera jeden komunikat powiadomienia, każdy komunikat pojawia się, a następnie znika po 5 sekundach. Ten składnik jest poza widokiem sieci Web, dostarczanym przez natywną aplikację hosta. Zapewnia interfejs js, taki jak addNofication(message). Innym przypadkiem są preloadery, które są również dostarczane przez natywną aplikację hosta i wyzwalane przez jej interfejs API JavaScript. Dodaję opakowanie dla tych interfejsów API w componentDidUpdatekomponencie React. Jak zaprojektować rekwizyty lub stan tego komponentu?
企业 应用 架构 模式 大师
3

W naszej aplikacji mamy trzy rodzaje powiadomień, z których wszystkie są zaprojektowane jako aspekty:

  1. Wskaźnik obciążenia (modalny lub niemodalny w oparciu o rekwizyt)
  2. Wyskakujące okienko błędu (modalne)
  3. Powiadomienie Snackbar (niemodalny, samozamykający)

Wszystkie trzy z nich znajdują się na najwyższym poziomie naszej aplikacji (Main) i są połączone przez Redux, jak pokazano na poniższym fragmencie kodu. Te rekwizyty kontrolują wyświetlanie odpowiadających im aspektów.

Zaprojektowałem proxy, które obsługuje wszystkie nasze wywołania API, dlatego wszystkie błędy isFetching i (api) są zapośredniczone przez actionCreators, które importuję w proxy. (Nawiasem mówiąc, używam również pakietu webpack do wstrzyknięcia makiety usługi tworzenia kopii zapasowych dla programistów, abyśmy mogli pracować bez zależności od serwera).

Każde inne miejsce w aplikacji, które wymaga podania dowolnego rodzaju powiadomienia, po prostu importuje odpowiednią akcję. Snackbar i Error mają parametry wyświetlania komunikatów.

@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)
}}

) eksportuje domyślną klasę Main rozszerza React.Component {

Dreculah
źródło
Pracuję nad podobną konfiguracją z wyświetlaniem modułu ładującego / powiadomień. Mam problemy; czy miałbyś streszczenie lub przykład, jak wykonujesz te zadania?
Aymen
2

Zapisuję adresy URL takie jak:

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

A potem mam zapamiętany selektor (przez reselect).

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

Aby adres URL był unikalny w przypadku POST, przekazuję jakąś zmienną jako zapytanie.

A gdzie chcę pokazać wskaźnik, po prostu używam zmiennej getFetchCount

Sergiu
źródło
1
Można zastąpić Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falseprzez Object.keys(items).every(item => items[item])drodze.
Alexandre Annic
1
Myślę, że miałeś na myśli somezamiast every, ale tak, zbyt wiele niepotrzebnych porównań w pierwszym zaproponowanym rozwiązaniu. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena