Общие Редукторы / Действия в React / Redux - PullRequest
0 голосов
/ 09 сентября 2018

Я пытаюсь определить, как добавить несколько фрагментов данных для использования в одном компоненте.

Каждый пример, который я вижу с React / Redux, запрашивает очень конкретные данные и содержит редукторы и действия для обработки именно этого типа данных. Однако я не смог найти информацию об обработке более общих данных.

Например, у меня есть несколько различных компонентов (или категорий) на моем сайте. Одним из таких компонентов является Cards. Таким образом, если пользователь нажимает на ссылку для /cards/hockey, он должен запросить хоккейные данные у API (если их уже нет в магазине) и отобразить на странице «Карты». Если пользователь щелкает ссылку для /cards/football, он должен следовать той же процедуре, проверяя, есть ли у него данные в хранилище, если не извлекает их из API, и отображая страницу Карт с этими данными.

Другой тип компонента может быть stats со статистикой о различных спортивных командах.

Я не всегда буду знать, какие типы карт доступны заранее, поэтому я не могу жестко задавать конкретные виды спорта в своем приложении.

Так что в этом случае я хотел бы создать только два компонента: карты и статистику, но иметь динамически загружаемые данные для заполнения этих компонентов.

Сейчас у меня слишком много повторений, и оно жестко запрограммировано. Это означает, что я не могу динамически добавлять новые типы в будущем без создания нового кода для обработки каждого из этих типов.

Так, например, сейчас у меня есть /actions/footballCardActions.js и /actions/hockeyCardActions.js. Затем у меня есть /reducers/footballCardReducers.js и /reducers/hockeyCardReducers.js. У меня могут быть похожие компоненты и для компонента Stats.

Я также указываю статус, например FETCH_HOCKEY_CARDS_SUCCESS или FETCH_FOOTBALL_CARDS_SUCCESS.

Опять же, все они жестко запрограммированы, что затрудняет масштабируемость.

Один пример, которому я пытаюсь следовать, это https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks - но опять-таки он использует очень специфические запросы данных, а не общие.

Что я могу сделать, чтобы мой код работал более обобщенно, чтобы мне не нужно было жестко кодировать определенные наборы данных. Есть ли хорошие уроки, которые имеют дело с подобной ситуацией?

Дополнительные уточнения

Одним из моих компонентов (экранов) является экран спортивной карты. Система меню (со ссылками) автоматически генерируется при загрузке сайта из API, поэтому я не всегда знаю, какие ссылки доступны. Так что здесь могут быть ссылки на хоккей, футбол, а также ряд других видов спорта, о которых я не задумывался. Если щелкнуть ссылку меню, она вызовет API для этого вида спорта и отобразит данные на экране спортивной карты.

На основе приведенной выше ссылки (и других подобных сайтов) я выяснил, как жестко закодировать каждый запрос для конкретного вида спорта в разделе действий и редукторов, но я не смог выяснить, как это сделать вообще, если я не знаю спорт раньше времени.

Дополнительные разъяснения на основе текущих ответов

Если кто-то добавляет новый вид спорта в базу данных API под названием MuffiBall, мое приложение должно быть в состоянии справиться с этим. Поэтому нельзя ожидать добавления нового кода JavaScript для каждого нового вида спорта, добавляемого в API.

Все спортивные карты, извлеченные из базы данных, имеют одинаковую структуру.

Схема моего текущего кода

index.js

//index.js
//Other imports here (not shown)
import Cards from './components/CardsPage'
import * as cardActions from './actions/cardActions';
import * as statsActions from './actions/statsActions';

import configureStore from './store/configureStore';

const store = configureStore();

/* Bad place to put these, and currently I am expected to know what every sport is*/
store.dispatch(hockeyActions.fetchHockey());
store.dispatch(footballActions.fetchFootball());
store.dispatch(muffiballActions.fetchMuffiball());


render(
  <Provider store={store}>
          <Router>
                <div>

                    /* Navigation menu here (not shown) */
                    /* Currently it is manually coded, */
                    /* but I will be automatically generating it based on API */

                      <Route exact path="/" component={Home} />
                      <Route path="/about" component={About} />
                      <Route path="/cards/:val" component={Cards} />
                      <Route path="/stats/:val" component={Stats} />
                </div>
          </Router>
  </Provider>,
  document.getElementById('app')
);

магазин / configureStore.js

// store/configureStore.js
import {createStore, compose, applyMiddleware} from 'redux';
// Import thunk middleware
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

export default function configureStore(initialState) {
  return createStore(rootReducer, initialState,
    // Apply to store
    applyMiddleware(thunk)
  );
}

Действия / actionTypes

// actions/actionTypes

export const FETCH_HOCKEY_SUCCESS = 'FETCH_HOCKEY_SUCCESS';
export const FETCH_FOOTBALL_SUCCESS = 'FETCH_FOOTBALL_SUCCESS';
export const FETCH_MUFFIBALL_SUCCESS = 'FETCH_MUFFIBALL_SUCCESS';

actions / hockeyActions.js (один такой файл для каждого вида спорта - необходимо создать этот один общий файл):

// hockeyActions.js (one such file for every sport - need to make this one generic file):

import Axios from 'axios';

const apiUrl = '/api/hockey/';
// Sync Action
export const fetchHockeySuccess = (hockey) => {
  return {
    type: 'FETCH_HOCKEY_SUCCESS',
    hockey
  }
};


//Async Action
export const fetchHockey = () => {
  // Returns a dispatcher function
  // that dispatches an action at a later time
  return (dispatch) => {
    // Returns a promise
    return Axios.get(apiUrl)
      .then(response => {
        // Dispatch another action
        // to consume data

        dispatch(fetchHockeySuccess(response.data))
      })
      .catch(error => {
        console.log(error)
        throw(error);
      });
  };
};

redurs / hockeyReducers.js (один такой файл для каждого вида спорта - нужно создать этот общий файл)

// reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)

import * as actionTypes from '../actions/actionTypes'

export const hockeyReducer = (state = [], action) => {
  switch (action.type) {
    case actionTypes.FETCH_HOCKEY_SUCCESS:
          return action.hockey;
    default:
          return state;
  }
};

редукторы / index.js

// reducers/index.js

import { combineReducers } from 'redux';
import {hockeyReducer} from './hockeyReducers'
import {footballReducer} from './footballReducers'
import {muffiballReducer} from './muffiballReducers'

export default combineReducers({
  hockey: hockeyReducer,
  football: footballReducer,
  muffiball: muffiballReducer,
  // More reducers for each sport here
});

компоненты / CardsPage.js:

//components/CardsPage.js

import React from 'react';
import { connect } from 'react-redux';

class Cards extends React.Component{
  constructor(props){
    super(props);

    this.state = {
        data: this.props.data,
    }

  }

  componentWillReceiveProps(nextProps){
        this.setState({
                data: nextProps.data,
        })
  }

  render(){

    return(
        {/* cards displayed from this.state.data */}
    )
  }
}

const mapStateToProps = (state, ownProps) => {
  return {
    data: state[ownProps.match.params.val]
  }
};

export default connect(mapStateToProps)(Cards);

Ответы [ 4 ]

0 голосов
/ 12 сентября 2018

сделайте шаг назад и определите типы данных, которые имеют уникальные формы, например, cards и stats. Вы создадите срез магазина для каждого из них с его собственными действиями, редукторами и селекторами. Спорт должен быть просто переменной, которую вы используете в качестве аргумента ваших действий и селекторов. например,

Асинхронное действие

export const fetchCards = (sport) => {
  return (dispatch) => {
    return Axios.get(`/api/${sport}/`)
      .then(response =>
        dispatch(fetchCardSuccess({ sport, data: response.data }))
      )
      .catch(error => {
        console.log(error)
        throw(error);
      });
  };
};

1008 * Переходник *

export const cardReducer = (state = {}, action) => {
  switch (action.type) {
    case actionTypes.FETCH_CARD_SUCCESS:
      return { ...state, [action.sport]: action.data };
    default:
      return state;
  }
};

Селектор карт

export const getSport(state, sport) {
  return state.cards[sport];
}

Возможно, вам понадобится еще один фрагмент для управления списком доступных видов спорта, полученных с сервера, и другими глобальными данными.

0 голосов
/ 12 сентября 2018

Методология, которая набирает популярность для многократно используемых редукционных действий / редукторов: Redux Ducks .Вот хорошая вспомогательная библиотека и пример для реализации этого в вашей кодовой базе.

Создание примера в приведенной выше ссылке, который выглядел бы примерно так:

// remoteObjDuck.js

import Duck from 'extensible-duck'
import axios from 'axios'

export default function createDuck({ namespace, store, path, initialState={} }) {
  return new Duck({
    namespace, store,

    consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },

    types: [
      'UPDATE',
      'FETCH', 'FETCH_PENDING',  'FETCH_FULFILLED',
      'POST',  'POST_PENDING',   'POST_FULFILLED',
    ],

    reducer: (state, action, { types, statuses, initialState }) => {
      switch(action.type) {
        case types.UPDATE:
          return { ...state, obj: { ...state.obj, ...action.payload } }
        case types.FETCH_PENDING:
          return { ...state, status: statuses.LOADING }
        case types.FETCH_FULFILLED:
          return { ...state, obj: action.payload.data, status: statuses.READY }
        case types.POST_PENDING:
        case types.PATCH_PENDING:
          return { ...state, status: statuses.SAVING }
        case types.POST_FULFILLED:
        case types.PATCH_FULFILLED:
          return { ...state, status: statuses.SAVED }
        default:
          return state
      }
    },

    creators: ({ types }) => ({
      update: (fields) => ({ type: types.UPDATE, payload: fields }),
      get:        (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
      post:         () => ({ type: types.POST, payload: axios.post(path, obj) }),
      patch:        () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
    }),

    initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
  })
}

и каждый вид спорта создаст одну утку, которая будет использовать ту же функциональность.

Хоккей:

// hockeyDuck.js

import createDuck from './remoteObjDuck'

export default createDuck({ namespace: 'my-app', store: 'hockeyCards', path: '/cards/hockey' })

Футбол:

// footballDuck.js

    import createDuck from './remoteObjDuck'

    export default createDuck({ namespace: 'my-app', store: 'footballCards', path: '/cards/football' })

Затем объедините редукторы вthe store:

// redurs.js

import { combineReducers } from 'redux'
import footballDuck from './footballDuck'
import hockeyDuck from './hockeyDuck'

export default combineReducers({ [footballDuck.store]: footballDuck.reducer, [hockeyDuck.store]: hockeyDuck.reducer })

Если вы хотите динамически добавлять редукторы к redux на лету, вам придется использовать что-то вроде: https://github.com/ioof-holdings/redux-dynamic-reducer. Тогдавы можете создать утку на лету в зависимости от вашего ответа на вызов API:

//get from API
var sport = "football";
var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
store.attachReducer({ [footballDuck.store]: footballDuck.reducer });
0 голосов
/ 12 сентября 2018

Так что это предполагает, что ваши "общие данные" всегда будут иметь одинаковую форму.

Возможно, у вас есть общий <Results /> компонент. Не уверен, как вы делаете маршрутизацию, но вы можете использовать путь URL-адреса, чтобы определить, какие данные для выборки и отображения.

Компонент маршрута (React Router 4) может выглядеть следующим образом:

<Route path="/cards/:id" render={props => <Results {...props} />}

Тогда в вашем <Results/> компоненте вы можете использовать react-redux, чтобы отобразить ваше состояние притока в реквизиты компонента. В componentDidMount вы могли видеть, есть ли у вас соответствующие данные. Если у вас нет соответствующих данных, отправьте действие из componentDidMount, чтобы получить их. Как то так

import { connect } from 'react-redux';
import React from 'react';
import { fetchDataAction } from './actions';

class Results extends React.Component {
  componentDidMount() {
    // check if results exists, if not then fire off an action to get 
    // data. Use whatever async redux pattern you want
    if (!this.props.results) {
      this.props.fetchData();
    }
  }

  render() { /* DO SOMETHING WITH RESULTS, OR LACK OF */ }
}

const mapStateToProps = (state, ownProps) => ({
  results: state.results[ownProps.match.params.id],
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData() {
    // send path parameter via action to kick off async fetch
    dispatch(fetchDataAction(ownProps.match.params.id));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(Results);

У вас может быть редуктор результатов, который будет просто объектом, отображающим категорию в результаты. Вот как может выглядеть редуктор результатов:

export default (state = {}, action) => {
  switch(action.type) {
    case 'FETCH_LOADED':
      const { payload: { type, results } } = action;
      return {
        ...state,
        [type]: results,
      };
    default:
      return state;
  };
};
0 голосов
/ 10 сентября 2018
// structure (something like...)

/*
./components 
./redux
./redux/actions
./redux/reducers
./redux/sagas
./redux/types
./util
*/

/* ------------------------------------------------- */

/* package.json */

{
  (...)
  "proxy": "http://localhost:3000",
  (...)  
}

/* ------------------------------------------------- */

/* index.js or otherComponent.js */

import React from 'react' 
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import reducers from './redux/reducers/index'
import logger from 'redux-logger'
import createSagaMiddleware from 'redux-saga'
import indexSagas from './redux/sagas/indexSagas'

import { environment } from './util/baseUrl'

const sagaMiddleware = createSagaMiddleware()

const store = 
  environment === 'DEV' ?
    createStore(
      reducers,
      window.__REDUX_DEVTOOLS_EXTENSION__ && 
      window.__REDUX_DEVTOOLS_EXTENSION__(),  
      applyMiddleware(sagaMiddleware, logger)
    ) :
    createStore(
      reducers,
      applyMiddleware(sagaMiddleware)
    ) 

sagaMiddleware.run(indexSagas)

render(
   <Provider store={store}>
      <App />      
   </Provider>,
 document.getElementById('app'))


/* ------------------------------------------------- */

/* baseURL.js */

const DEV = 'DEV'
const PROD = 'PROD'

/*-----------------------------------------*/
/*------*/ export const environment = DEV /* <------- */
/*-----------------------------------------*/

export const baseURL = 
    environment === DEV ?
    '/api/v1/' : 
    'https://abcde.website.net/api/v1/' 

/* ------------------------------------------------- */

/* genericTypes.js */

export const GET_REGISTERS_REQUEST = 'GET_REGISTERS_REQUEST'
export const GET_REGISTERS_SUCCESS = 'GET_REGISTERS_SUCCESS'
export const GENERIC_ERROR_MSG = 'GENERIC_ERROR_MSG'

/* ------------------------------------------------- */

/* actions.js */

export const getRegistersRequest = ( route ) => {
  return {
    type: GET_REGISTERS_REQUEST,
    route,
  }
}
export const getRegistersSuccess = ( data ) => {
  return {
    type: GET_REGISTERS_SUCCESS,
    data,
  }
} 
export const genericErrorMsg = ( errorMsg ) => {
  return {
    type: GENERIC_ERROR_MSG,
    errorMsg,
  }
}

/* ------------------------------------------------- */

/* genericReducer.js */

import { GET_REGISTERS_REQUEST, GET_REGISTERS_SUCCESS, GENERIC_ERROR_MSG } from '../types/genericTypes'

const INITIAL_STATE = {
  data: [],
  isFetching: false,
  isLoaded: false,
  error: false,
  errorMsg: '',
}

const genericReducer = (state = INITIAL_STATE, action) => {
  switch(action.type){
    case GET_REGISTERS_REQUEST:
      return {
        ...state,
        data: [],
        isFetching: true,
        isLoaded: false,
        error: false,
        errorMsg: '',
      }  
    case GET_REGISTERS_SUCCESS:
      return {
        ...state,
        data: action.data,
        isFetching: false,
        isLoaded: true,
      }
    case GENERIC_ERROR_MSG: 
      return {
        ...state,
        isFetching: false,
        error: true,
        errorMsg: action.errorMsg,
      }   
    default:
      return state
  }
}
export default genericReducer  

/* ------------------------------------------------- */

/* yourComponent.js  */

import React, { Component } from "react"
import { connect } from 'react-redux'
import { getRegistersRequest } from '../../redux/actions'   

//(...)
// this.props.getRegistersRequest('cards/hockey')
// this.props.getRegistersRequest('cards/football')
//(...)

const mapStateToProps = (state) => {
  return {
    data: state.genericReducer.data,
    isFetching: state.genericReducer.isFetching,
    isLoaded: state.genericReducer.isLoaded,
    error: state.genericReducer.error,
    errorMsg: state.genericReducer.errorMsg,
  } 
}
const mapDispatchToProps = (dispatch) => {
  return {
    getRegistersRequest: ( route ) => dispatch(getRegistersRequest( route )),
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(yourComponent)

/* ------------------------------------------------- */

/* indexSagas.js */

import { takeLatest } from 'redux-saga/effects'
import axios from 'axios'

import { GET_REGISTERS_REQUEST } from '../types/genericTypes'
import { getRegistersRequest } from './genericSagas'

function* indexSagas() {
  try {
    yield (takeLatest(GET_REGISTERS_REQUEST, getRegistersRequest, axios))  
  }
  catch (e) {
    // (...)
  }
}
export default indexSagas  

/* ------------------------------------------------- */

/* genericSagas.js */

import { put } from 'redux-saga/effects'

import { getRegistersSuccess, genericErrorMsg } from '../actions'

export function* getRegistrosRequest(axios, action) {
  const rest = createRest(axios)
  try {
    let route = ''
    switch (action.route) {
      case 'cards/hockey':
      case 'cards/football':
        route = action.route
        break
      default: {
        yield put(genericErrorMsg('Route [ ' + action.route + ' ] not implemented yet!'))
        return
      }
    }    
    const data = yield rest.get(route)
    yield put(getRegistersSuccess(data))
  }
  catch (e) {
    yield put(genericErrorMsg(e))
  }
}

/* ------------------------------------------------- */

/* createRest */

import { baseURL } from '../../util/baseUrl'
function createRest(axios){
  const token = localStorage.getItem('yourToken')
  const rest = axios.create({
    baseURL: baseURL,
    headers:{
      Authorization: 'Bearer ' + token
    }
  })
  return rest  
}
export default createRest

/* ------------------------------------------------- */

Надеюсь, это поможет!

С уважением.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...