【问题标题】:Generic Reducers/Actions in React/ReduxReact/Redux 中的通用化简器/动作
【发布时间】:2019-02-13 22:01:33
【问题描述】:

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

我在 React/Redux 中看到的每个示例都请求非常具体的数据,并且有 reducer 和 action 来处理该确切类型的数据。但是,我无法找到有关处理更多通用数据的信息。

例如,我的网站上有几个不同的组件(或类别)。其中一个组件是Cards。因此,如果用户单击/cards/hockey 的链接,它应该从 API 请求曲棍球数据(如果它尚未在商店中),并将其显示在卡片页面中。如果用户单击/cards/football 的链接,它应该遵循相同的过程,检查它是否有存储的数据,如果没有从 API 中提取它,并显​​示带有该数据的卡片页面。

另一种组件类型可能是 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 并在运动卡屏幕上显示数据。

根据上面的链接(和其他类似网站),我已经弄清楚如何在动作和减速器部分对特定运动的每个请求进行硬编码,但我无法弄清楚如何做到这一点一般来说,如果我不提前了解这项运动。

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

如果有人将一项名为 MuffiBall 的新运动添加到 API 数据库,我的应用程序需要能够处理它。因此,不能指望我为添加到 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')
);

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

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

组件/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);

【问题讨论】:

  • 你需要克服什么问题? :) 正如您在问题的第一段中所问的那样,您在哪里以及如何获取数据?
  • @devserkan 基本上,我想弄清楚如何使用单个页面/组件/模板来显示不同的数据。在每种情况下,页面的结构都是相同的,但数据会根据用户从 API 中提取的内容而有所不同。所以,我需要一种方法来一般地提取数据并将其显示在页面上。例如,现在我需要执行“FETCH_HOCKEY_CARDS_SUCCESS”或“FETCH_FOOTBALL_CARDS_SUCCESS”之类的操作。我目前正在对这些进行硬编码。这会导致大量重复,并且不允许将来灵活添加其他卡。
  • 我现在需要去,但是当我阅读您的问题时,我无法得出与您在此处解释的相同的结论。也许最好编辑你的问题。人们可以据此给出答案。不知何故,您想要可重用的动作创建者和缩减者?
  • @devserkan 是的,我相信这可能就是我想要的。
  • 对于更通用的组件逻辑,请使用Higher order components。对于更通用的 reducer 逻辑,请使用 Higher order reducers。其他高阶 reducer 库可以在这里找到:github.com/markerikson/redux-ecosystem-links/blob/master/…

标签: javascript reactjs redux


【解决方案1】:
// 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
 
/* ------------------------------------------------- */

【讨论】:

  • 欢迎来到 Stackoverflow。请检查how to ask 并避免仅使用代码的答案。
【解决方案2】:

退后一步,找出具有独特形状的数据类型,例如cardsstats。您将使用它自己的操作、reducers 和选择器为每一个构建一个 store slice。这项运动应该只是一个变量,您可以将其用作操作和选择器的参数。 例如

异步操作

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

卡片选择器

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

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

【讨论】:

  • 使用这种方法会不会有任何混淆,因为所有相似的数据集都将使用相同的操作类型(即 FETCH_CARD_SUCCESS)?
  • action 类型仅由 reducer 用于识别他们需要处理的动作,因此只要 reducer 可以从动作有效负载中确定运动并更新相应地存储。
  • 我不知道您的数据是什么样的,但理想情况下,您在商店中的卡片切片将是嵌套对象的对象,每个对象都具有唯一的 ID 并包含其运动作为属性。例如,您将遍历服务器返回的列表并将每个项目添加到存储卡切片。然后你有一个卡片选择器,它接受一项运动作为参数并返回该运动的对象列表,另一个选择器可以通过 id 返回一张卡片。
【解决方案3】:

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

你可以有一个通用的&lt;Results /&gt; 组件。不确定您是如何进行路由的,但您可以使用 URL 的路径名来确定要获取和显示哪些数据。

路由组件(React Router 4)可能如下所示:

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

然后在您的&lt;Results/&gt; 组件中,您可以使用react-redux 将您的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);

您可以有一个结果缩减器,它只是一个将类别映射到结果的对象。下面是结果 reducer 的样子:

export default (state = {}, action) => {
  switch(action.type) {
    case 'FETCH_LOADED':
      const { payload: { type, results } } = action;
      return {
        ...state,
        [type]: results,
      };
    default:
      return state;
  };
};

【讨论】:

  • 我当前的 index.js 包括一个 &lt;Provider&gt;,包括 &lt;Router&gt; 和诸如 &lt;Route path="/cards/:val" component={Cards} /&gt; 之类的路径。你能告诉我你的render 属性在这里做什么吗?
  • 这里是文档reacttraining.com/react-router/core/api/Route/render-func 的链接 - 基本上只是在路径匹配时呈现组件。它不会每次都重新渲染一个新组件,还会将路由器 props 传递给组件。
  • 这似乎运作良好。我还使用 componentWillReceiveProps 函数让它处理更新的数据,如下所示:if (!nextProps.data){ nextProps.fetchData()}。在使用 Redux 时以这种方式使用 componentWillReceiveProps 是否可以接受?在需要执行状态更改时,我更习惯于使用传统的 React。
  • @kojow7 是的,我认为如果组件永远不会重新安装,您可能想要做类似的事情。我还会进行某种检查,看看您是否正在获取数据,这样就不会发生多次。
【解决方案4】:

在可重用的 redux 操作/reducer 中越来越受欢迎的方法是 Redux Ducks。这是一个 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' })

然后组合 store 中的 reducer:

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

如果你想动态地将 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 });

【讨论】:

  • 这似乎对我不起作用,因为它需要我为每一项存在或将存在的运动创建 footballDuck 和 hockeyDuck 以及一个 JavaScript 文件。这正是我要避免的问题。
  • @kojow7 请注意我回答中的最后一个代码块。如果您使用 redux-dynamic-reducer 库,您应该能够动态添加您想要的任何运动。这意味着您不需要为每项运动创建一个文件,您可以根据需要为运动创建动作/减速器(即从 api 获取运动)
猜你喜欢
  • 2018-11-06
  • 2015-11-23
  • 2021-05-12
  • 2019-07-08
  • 1970-01-01
  • 2018-09-22
  • 1970-01-01
  • 2017-05-09
  • 1970-01-01
相关资源
最近更新 更多