React / Redux中的通用Reduce /动作

时间:2018-09-09 04:29:56

标签: javascript reactjs redux

我正在尝试确定如何提取多个数据以在同一组件中使用。

我看到的每个使用React / Redux的示例都要求非常特定的数据,并具有简化器和操作以处理该确切类型的数据。但是,我无法找到有关处理更多通用数据的信息。

例如,我的网站上有几个不同的组件(或类别)。这些组件之一是Cards。因此,如果用户单击/cards/hockey的链接,则应从API请求曲棍球数据(如果尚未存储在商店中),然后在“卡”页面中显示。如果用户单击/cards/football的链接,则它应遵循相同的步骤,检查是否已存储数据(如果未从API中提取数据),并显示包含该数据的Cards页面。

另一种组件类型可能是stats,其中包含有关不同运动队的统计信息。

我并不总是会提前知道哪些类型的卡,因此我无法在应用程序中对特定的运动类型进行硬编码。

因此,在这种情况下,我只想创建两个组件:卡和统计信息,但是已经动态加载了数据以填充这些组件。

现在我正在进行太多重复,并且已经过硬编码。这意味着我将来无法动态添加新类型,除非创建新代码以处理这些类型中的每一个。

例如,现在我有/actions/footballCardActions.js和/actions/hockeyCardActions.js。然后,我有/reducers/footballCardReducers.js和/reducers/hockeyCardReducers.js。 Stats组件也可能有类似的组件。

我还指定了诸如FETCH_HOCKEY_CARDS_SUCCESSFETCH_FOOTBALL_CARDS_SUCCESS之类的状态。

这些都是硬编码,因此很难扩展。

我要遵循的一个示例是https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks-但它再次使用了非常具体的数据请求,而不是通用的数据请求。

我可以做些什么来使我的代码更通用地工作,从而使我无需对特定的数据集进行硬编码。有没有很好的教程来解决类似情况?

更多说明

我的一个组件(屏幕)是运动牌屏幕。菜单系统(带有链接)是在站点加载时从API自动生成的,因此我并不总是知道哪些链接可用。因此,可能存在曲棍球,足球以及我没有想到的其他许多运动的链接。单击菜单链接后,它将调用该运动类型的API,并在运动卡屏幕上显示数据。

基于上面的链接(和其他类似的网站),我已经找到了如何在“动作和减速器”部分中对每个特定运动的要求进行硬编码的方法,但是我仍无法弄清楚如何做到这一点。一般来说,如果我不提前知道运动项目。

基于当前答案的进一步说明

如果有人向API数据库(称为MuffiBall)添加了一项新运动,则我的应用程序需要能够处理该运动。因此,不能期望我为添加到API的每个新运动添加新的JavaScript代码。

从数据库中检索到的所有运动卡都遵循相同的结构。

我当前代码的概述

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')
);

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

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

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

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

components / 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 :(得分:5)

退后一步,确定具有唯一形状的数据类型,例如server.host=bharat.corp.com server.user=kumar server.pwd=password123 emp.name=M.W.Khan emp.id=230989 emp.address=Birsingpur cards。您将使用它们自己的动作,reduces和选择器为每个构建一个商店切片。运动应该只是一个变量,您可以将其用作操作和选择器的参数。 例如

异步操作

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

卡选择器

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

您可能需要另一个分片来管理从服务器获取的可用运动列表以及其他全局数据。

答案 1 :(得分:4)

Soo假设您的“通用数据”始终具有相同的形状。

您可能有一个通用的<Results />组件。不知道如何进行路由,但是可以使用URL的路径名来确定要提取和显示的数据。

路由组件(反应路由器4)可能如下所示:

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

然后在<Results/>组件中,您可以使用react-redux将redux状态映射到组件props。在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;
  };
};

答案 2 :(得分:2)

Redux Ducks是可重用的Redux动作/减少器越来越流行的一种方法。这是一个good helper library and example,可在您的代码库中实现。

构建以上链接中的示例,对您而言,它看起来像这样:

// 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' })

然后在商店中组合减速器:

// reducers.js

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

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

如果您想动态地将reducered添加到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 });

答案 3 :(得分:-2)

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

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

希望对您有帮助!

最诚挚的问候。