Uzyskujesz dostęp do stanu Redux w kreatorze akcji?

296

Powiedz, że mam następujące:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
  }
}

I w tym kreatorze akcji chcę uzyskać dostęp do globalnego stanu sklepu (wszystkie reduktory). Czy lepiej to zrobić:

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

albo to:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return (dispatch, getState) => {
    const {items} = getState().otherReducer;

    dispatch(anotherAction(items));
  }
}
ffxsam
źródło

Odpowiedzi:

522

Istnieją różne opinie na temat tego, czy uzyskanie dostępu do stanu w kreatorze akcji jest dobrym pomysłem:

  • Twórca Redux, Dan Abramov, uważa, że ​​należy go ograniczyć: „Nieliczne przypadki użycia, w których moim zdaniem jest dopuszczalne, to sprawdzenie danych w pamięci podręcznej przed złożeniem wniosku lub sprawdzenie, czy użytkownik jest uwierzytelniony (innymi słowy, dokonanie wysłania warunkowego). Myślę, że przekazywanie danych, takich jak state.something.itemsw kreatorze akcji, jest zdecydowanie anty-wzorcem i jest zniechęcane, ponieważ przesłaniało historię zmian: jeśli występuje błąd i itemssą one nieprawidłowe, trudno jest ustalić, skąd pochodzą te nieprawidłowe wartości, ponieważ są już stanowi część akcji, a nie jest bezpośrednio obliczana przez reduktor w odpowiedzi na akcję. Zrób to ostrożnie ”.
  • Obecny opiekun Redux, Mark Erikson, mówi, że jest w porządku, a nawet zachęca go do używania getStatew gromadach - dlatego istnieje . Omawia zalety i wady dostępu do twórców stanu w swoim blogu Idiomatic Redux: Myśli o Thunkach, Sagach, Abstrakcji i Wielokrotnym użyciu .

Jeśli okaże się, że jest to potrzebne, oba sugerowane podejścia są w porządku. Pierwsze podejście nie wymaga oprogramowania pośredniego:

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

Widać jednak, że opiera się na storebyciu singletonem eksportowanym z jakiegoś modułu. Nie zalecamy tego, ponieważ znacznie utrudnia dodanie renderowania serwera do aplikacji, ponieważ w większości przypadków na serwerze będziesz chciał mieć osobny sklep na żądanie . Chociaż technicznie to podejście działa, nie zalecamy eksportowania sklepu z modułu.

Dlatego zalecamy drugie podejście:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return (dispatch, getState) => {
    const {items} = getState().otherReducer;

    dispatch(anotherAction(items));
  }
}

Wymagałoby to użycia oprogramowania pośredniego Redux Thunk, ale działa dobrze zarówno na kliencie, jak i na serwerze. Możesz przeczytać więcej o Redux Thunk i dlaczego jest to konieczne w tym przypadku tutaj .

Idealnie, twoje działania nie powinny być „grube” i powinny zawierać tak mało informacji, jak to możliwe, ale powinieneś czuć się swobodnie, robiąc to, co najlepiej dla ciebie we własnej aplikacji. FAQ Redux zawiera informacje na temat podziału logiki między twórcami akcji i reduktorów oraz czasy, w których może być użyteczne getStatew kreatorze akcji .

Dan Abramov
źródło
5
Mam jedną sytuację, gdy wybranie czegoś w składniku może wywołać test PUT lub POST, w zależności od tego, czy sklep zawiera dane związane ze składnikiem, czy też nie. Czy lepiej jest umieścić logikę biznesową dla wyboru PUT / POST w komponencie zamiast twórcy akcji opartego na thunk?
vicusbass
3
Jaka jest najlepsza praktyka? Mam teraz do czynienia z podobnym problemem, gdy używam getState w moim kreatorze akcji. W moim przypadku używam go do ustalenia, czy wartości, jeśli formularz ma oczekujące zmiany (a jeśli tak, to wywołam akcję, która pokazuje okno dialogowe).
Arne H. Bitubekk
42
Czytanie ze sklepu w kreatorze akcji jest w porządku. Zachęcam do użycia selektora , abyś nie zależał od dokładnego kształtu stanu.
Dan Abramov
2
Używam oprogramowania pośredniego do wysyłania danych do mixpanel. Więc mam meta klucz w akcji. Muszę przekazać różne zmienne ze stanu do mixpanela. Ustawienie ich na twórców akcji wydaje się być anty-wzorcem. Jakie byłoby najlepsze podejście do obsługi tego rodzaju przypadków użycia?
Aakash Sigdel
2
O stary! Nie zawiązałem węzła, który otrzymałem getStatejako reduktor jako drugi parametr, łamałem sobie głowę, dziękuję bardzo
Lai32290
33

Gdy twój scenariusz jest prosty, możesz użyć

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

Ale czasami action creatortrzeba uruchomić wiele akcji

na przykład żądanie asynchroniczne, więc potrzebujesz REQUEST_LOAD REQUEST_LOAD_SUCCESS REQUEST_LOAD_FAILdziałań

export const [REQUEST_LOAD, REQUEST_LOAD_SUCCESS, REQUEST_LOAD_FAIL] = [`REQUEST_LOAD`
    `REQUEST_LOAD_SUCCESS`
    `REQUEST_LOAD_FAIL`
]
export function someAction() {
    return (dispatch, getState) => {
        const {
            items
        } = getState().otherReducer;
        dispatch({
            type: REQUEST_LOAD,
            loading: true
        });
        $.ajax('url', {
            success: (data) => {
                dispatch({
                    type: REQUEST_LOAD_SUCCESS,
                    loading: false,
                    data: data
                });
            },
            error: (error) => {
                dispatch({
                    type: REQUEST_LOAD_FAIL,
                    loading: false,
                    error: error
                });
            }
        })
    }
}

Uwaga: potrzebujesz funkcji redux-thunk, aby zwrócić funkcję w kreatorze akcji

Nour Sammour
źródło
3
Czy mogę po prostu zapytać, czy należy sprawdzić status stanu „ładowanie”, aby kolejne żądanie ajax nie było wysyłane podczas pierwszego?
JoeTidee
@JoeTidee W tym przykładzie wysyłka stanu załadowania jest zakończona. Jeśli wykonasz tę czynność na przykład za pomocą przycisku, sprawdzisz, czy loading === trueistnieje, i wyłączysz przycisk.
croraf
4

Zgadzam się z @Bloomca. Przekazywanie potrzebnej wartości ze sklepu do funkcji wysyłki jako argumentu wydaje się prostsze niż eksportowanie sklepu. Zrobiłem tutaj przykład:

import React from "react";
import {connect} from "react-redux";
import * as actions from '../actions';

class App extends React.Component {

  handleClick(){
    const data = this.props.someStateObject.data;
    this.props.someDispatchFunction(data);
  }

  render(){
    return (
      <div>       
      <div onClick={ this.handleClick.bind(this)}>Click Me!</div>      
      </div>
    );
  }
}


const mapStateToProps = (state) => {
  return { someStateObject: state.someStateObject };
};

const mapDispatchToProps = (dispatch) => {
  return {
    someDispatchFunction:(data) => { dispatch(actions.someDispatchFunction(data))},

  };
}


export default connect(mapStateToProps, mapDispatchToProps)(App);
Jason Allshorn
źródło
Ta metoda jest dość logiczna.
Ahmet Şimşek,
To jest właściwy sposób, aby to zrobić i jak to robię. Twórca akcji nie musi znać całego stanu, a jedynie odpowiedni fragment.
Ash
3

Chciałbym zaznaczyć, że czytanie ze sklepu nie jest takie złe - może być o wiele wygodniej zdecydować, co należy zrobić na podstawie sklepu, niż przekazać wszystko do komponentu, a następnie jako parametr funkcja. Zgadzam się z Danem całkowicie, że o wiele lepiej nie używać sklepu jako singletone, chyba że masz 100% pewności, że użyjesz go tylko do renderowania po stronie klienta (w przeciwnym razie mogą pojawić się trudne do śledzenia błędy).

Niedawno utworzyłem bibliotekę, aby poradzić sobie z gadatliwością redux, i myślę, że dobrym pomysłem jest umieszczenie wszystkiego w oprogramowaniu pośrednim, więc masz wszystko jako zastrzyk zależności.

Twój przykład będzie wyglądał następująco:

import { createSyncTile } from 'redux-tiles';

const someTile = createSyncTile({
  type: ['some', 'tile'],
  fn: ({ params, selectors, getState }) => {
    return {
      data: params.data,
      items: selectors.another.tile(getState())
    };
  },
});

Jednak, jak widać, tak naprawdę nie modyfikujemy tutaj danych, więc istnieje duża szansa, że ​​możemy użyć tego selektora w innym miejscu, aby połączyć go gdzie indziej.

Bloomca
źródło
1

Przedstawienie alternatywnego sposobu rozwiązania tego problemu. Może to być lepsze lub gorsze niż rozwiązanie Dana, w zależności od aplikacji.

Możesz uzyskać stan z reduktorów na akcje, dzieląc akcję na 2 oddzielne funkcje: najpierw zapytaj o dane, drugi działaj na danych. Możesz to zrobić za pomocą redux-loop.

Najpierw „uprzejmie zapytaj o dane”

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
    return {
        type: SOME_ACTION,
    }
}

W reduktorze przechwyć zapytanie i podaj dane do działania w drugim etapie za pomocą redux-loop.

import { loop, Cmd } from 'redux-loop';
const initialState = { data: '' }
export default (state=initialState, action) => {
    switch(action.type) {
        case SOME_ACTION: {
            return loop(state, Cmd.action(anotherAction(state.data))
        }
    }
}

Mając dane pod ręką, rób wszystko, co początkowo chciałeś

export const ANOTHER_ACTION = 'ANOTHER_ACTION';
export function anotherAction(data) {
    return {
        type: ANOTHER_ACTION,
        payload: data,
    }
}

Mam nadzieję, że to komuś pomoże.

Andrei Cioara
źródło
0

Wiem, że spóźniłem się na przyjęcie tutaj, ale przyszedłem tutaj, aby wyrazić opinię na temat własnego pragnienia wykorzystania stanu w działaniach, a następnie sformowałem własne, kiedy zdałem sobie sprawę, co moim zdaniem jest właściwym zachowaniem.

To właśnie selektor ma dla mnie największy sens. Twój komponent, który wysyła to żądanie, powinien zostać poinformowany, czy nadszedł czas, aby wysłać go przez wybór.

export const SOME_ACTION = 'SOME_ACTION';
export function someAction(items) {
  return (dispatch) => {
    dispatch(anotherAction(items));
  }
}

Może to wydawać się nieszczelnymi abstrakcjami, ale twój komponent wyraźnie musi wysłać wiadomość, a ładunek wiadomości powinien zawierać odpowiedni stan. Niestety twoje pytanie nie ma konkretnego przykładu, ponieważ moglibyśmy opracować „lepszy model” selektorów i działań w ten sposób.

SqueeDee
źródło
0

Chciałbym zasugerować jeszcze jedną alternatywę, którą uważam za najczystszą, ale wymaga to react-reduxlub czegoś podobnego - również korzystam z kilku innych fantazyjnych funkcji:

// actions.js
export const someAction = (items) => ({
    type: 'SOME_ACTION',
    payload: {items},
});
// Component.jsx
import {connect} from "react-redux";

const Component = ({boundSomeAction}) => (<div
    onClick={boundSomeAction}
/>);

const mapState = ({otherReducer: {items}}) => ({
    items,
});

const mapDispatch = (dispatch) => bindActionCreators({
    someAction,
}, dispatch);

const mergeProps = (mappedState, mappedDispatches) => {
    // you can only use what gets returned here, so you dont have access to `items` and 
    // `someAction` anymore
    return {
        boundSomeAction: () => mappedDispatches.someAction(mappedState.items),
    }
});

export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);
// (with  other mapped state or dispatches) Component.jsx
import {connect} from "react-redux";

const Component = ({boundSomeAction, otherAction, otherMappedState}) => (<div
    onClick={boundSomeAction}
    onSomeOtherEvent={otherAction}
>
    {JSON.stringify(otherMappedState)}
</div>);

const mapState = ({otherReducer: {items}, otherMappedState}) => ({
    items,
    otherMappedState,
});

const mapDispatch = (dispatch) => bindActionCreators({
    someAction,
    otherAction,
}, dispatch);

const mergeProps = (mappedState, mappedDispatches) => {
    const {items, ...remainingMappedState} = mappedState;
    const {someAction, ...remainingMappedDispatch} = mappedDispatch;
    // you can only use what gets returned here, so you dont have access to `items` and 
    // `someAction` anymore
    return {
        boundSomeAction: () => someAction(items),
        ...remainingMappedState,
        ...remainingMappedDispatch,
    }
});

export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);

Jeśli chcesz ponownie użyć tego trzeba będzie wyodrębnić konkretnego mapState, mapDispatcha mergePropsdo funkcji do ponownego użycia w innym miejscu, ale to sprawia Zależności zupełnie jasne.

Sam96
źródło