简体   繁体   English

React / Redux中的通用Reducers / Actions

[英]Generic Reducers/Actions in React/Redux

I am trying to determine how to pull in multiple pieces of data to use in the same component. 我试图确定如何在同一组件中引入多个数据。

Every example I see with React/Redux requests very specific data and has reducers and actions to handle that exact type of data. 我在React / Redux中看到的每个例子都要求非常具体的数据,并且还有减少器和动作来处理这种确切类型的数据。 However, I have not been able to find information about handling more generic data. 但是,我无法找到有关处理更多通用数据的信息。

For example, I have a few different components (or categories) on my site. 例如,我的网站上有一些不同的组件(或类别)。 One of those components is Cards . 其中一个组件是Cards So, if a user clicks on the link for /cards/hockey it should request the hockey data from the API (if it isn't in the store already), and display it in the Cards page. 因此,如果用户点击/cards/hockey的链接,它应该从API请求曲棍球数据(如果它已经不在商店中),并将其显示在卡片页面中。 If a user clicks the link for /cards/football , it should follow the same procedure, checking to see if it has the data in store yet, if not pulling it from the API, and displaying the Cards page with that data. 如果用户单击/cards/football的链接,它应该遵循相同的过程,检查它是否还有商店中的数据,如果没有从API中提取数据,并显示带有该数据的卡片页面。

Another component type might be stats with stats about different sports teams. 另一种组件类型可能是包含不同运动队stats的统计数据。

I will not always know what types of cards are available ahead of time, so I cannot hardcode the specific sports types in my application. 我不会总是知道提前有哪些类型的卡,所以我不能在我的应用程序中硬编码特定的运动类型。

So in this case, I'd like to only create two components: cards and stats, but have dynamically loaded data to populate those components. 所以在这种情况下,我只想创建两个组件:卡片和统计信息,但是有动态加载的数据来填充这些组件。

Right now I have too much repetition going on and it is hard coded. 现在我有太多的重复,它是硬编码的。 This means that I cannot dynamically add new types in the future without creating new code to handle each of these types. 这意味着我将来无法动态添加新类型,而无需创建新代码来处理这些类型。

So, for example, right now I have /actions/footballCardActions.js and /actions/hockeyCardActions.js. 所以,例如,现在我有/actions/footballCardActions.js和/actions/hockeyCardActions.js。 I then have /reducers/footballCardReducers.js and /reducers/hockeyCardReducers.js. 然后我有/reducers/footballCardReducers.js和/reducers/hockeyCardReducers.js。 I might have similar components for the Stats component as well. 我也可能有Stats组件的类似组件。

I'm also specifying status such as FETCH_HOCKEY_CARDS_SUCCESS or FETCH_FOOTBALL_CARDS_SUCCESS . 我还指定了状态,例如FETCH_HOCKEY_CARDS_SUCCESSFETCH_FOOTBALL_CARDS_SUCCESS

Again these are all hard coded, which makes scalability difficult. 这些都是硬编码的,这使得可扩展性变得困难。

One example I am trying to follow is https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks - but again it uses very specific data requests, rather than generic ones. 我试图遵循的一个例子是https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks-但它再次使用非常具体的数据请求,而不是通用的数据请求。

What can I do to make my code work more generically so that I do not need to hard code specific datasets. 我可以做些什么来使我的代码更通用,以便我不需要硬编码特定的数据集。 Are there any good tutorials out there that deal with a similar situation? 是否有任何处理类似情况的好教程?

More clarification 更多澄清

One of my components (screens) is a sports card screen. 我的一个组件(屏幕)是运动卡屏幕。 The menu system (with links) is automatically generated on site load from an API so I do not always know what links are available. 菜单系统(带链接)是在API上自动生成的,因此我并不总是知道可用的链接。 So, there may be links for hockey, football, as well as a number of other sports that I have not thought of. 因此,曲棍球,足球,以及其他一些我没有想过的运动可能会有链接。 When the menu link is clicked, it will call the API for that sport type and display the data on the sports card screen. 单击菜单链接后,它将调用该运动类型的API并在运动卡屏幕上显示数据。

Based on the above link (and other similar sites) I've figured out how to hard-code each request for a specific sport in the actions and reducers section, but I have not been able to figure out how to do this generically if I do not know the sports ahead of time. 基于上面的链接(和其他类似的网站),我已经想出如何在动作和减速器部分对特定运动的每个请求进行硬编码,但是如果我一直无法弄清楚如何做到这一点不提前知道运动。

Further clarification based on current answers 根据目前的答案进一步澄清

If someone adds a new sport to the API database called MuffiBall, my application needs to be able to handle it. 如果有人在名为MuffiBall的API数据库中添加了新的运动,我的应用程序需要能够处理它。 So, I cannot be expected to add new JavaScript code for each new sport that is added to the API. 因此,我不能指望为添加到API的每项新运动添加新的JavaScript代码。

All sports cards retrieved from the database follow the same structure. 从数据库检索的所有体育卡都遵循相同的结构。

An outline of my current code 我当前代码的概述

index.js 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 存储/ 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 动作/ 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 (one such file for every sport - need to make this one generic file): 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 (one such file for every sport - need to make this one generic file) 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 减速器/ 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: 组件/ 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);

take a step back and identify the data types that have unique shapes, eg cards and stats . 退后一步,确定具有独特形状的数据类型,例如cardsstats You will build a store slice for each of these with it's own actions, reducers, and selectors. 您将使用它自己的动作,缩减器和选择器为每个构建一个存储片。 The sport should just be a variable you use as an argument to your actions and selectors. 这项运动应该只是你用作你的行动和选择者的一个变量。 eg 例如

Async Action 异步行动

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

Reducer 减速器

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

Card Selector 卡选择器

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

You'll probably want another slice for managing a list of the available sports, fetched from the server, and other global data. 您可能需要另一个切片来管理从服务器获取的可用运动列表以及其他全局数据。

Soo this assumes your "generic data" always will have the same shape. 因此,假设您的“通用数据”始终具有相同的形状。

You could have a generic <Results /> component. 您可以拥有通用的<Results />组件。 Not sure how you are doing routing, but you can use the path name of the URL to determine which data to fetch and display. 不确定如何进行路由,但您可以使用URL的路径名来确定要获取和显示的数据。

The route component (React Router 4) could look like this: 路由组件(React Router 4)可能如下所示:

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

Then in your <Results/> component you can use react-redux to map your redux state to the component props. 然后在<Results/>组件中,您可以使用react-redux将redux状态映射到组件props。 In componentDidMount you could see if you have the appropriate data. componentDidMount您可以看到您是否拥有适当的数据。 If you do not have the appropriate data then dispatch an action from componentDidMount to fetch it. 如果您没有相应的数据,则从componentDidMount调度操作以获取它。 Something like this 像这样的东西

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

You could have a results reducer that would just be an object that maps category to results. 你可以有一个结果减少器,它只是一个将类别映射到结果的对象。 Here is what the results reducer could look like: 以下是结果缩减器的外观:

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

A methodology that is picking up popularity for reusable redux actions/reducers is Redux Ducks . Redux Ducks是一种在可重复使用的redux动作/减速器中越来越受欢迎的方法。 Here's a good helper library and example to implement this in your codebase. 这是一个很好的帮助程序库和示例,可以在您的代码库中实现它。

Building off the example in the above link that would look something like this for you: 在上面的链接中建立一个看起来像这样的例子:

// 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: [] })
  })
}

and each sport would create a single duck that will reuse the same functionality. 每项运动都会创造一只能重复使用相同功能的鸭子。

Hockey: 曲棍球:

// hockeyDuck.js

import createDuck from './remoteObjDuck'

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

Football: 足球:

// footballDuck.js

    import createDuck from './remoteObjDuck'

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

Then combine the reducers in the store: 然后结合商店中的reducer:

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

If you want to dynamically add reducers to redux on the fly you will have to use something like: https://github.com/ioof-holdings/redux-dynamic-reducer . 如果你想动态地将reducer添加到redux中,你必须使用类似的东西: https//github.com/ioof-holdings/redux-dynamic-reducer Then you can create the duck on the fly depending on your API call response: 然后,您可以根据API调用响应动态创建鸭子:

//get from API
var sport = "football";
var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
store.attachReducer({ [footballDuck.store]: footballDuck.reducer });
// 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

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

I hope it helps! 我希望它有所帮助!

Best regards. 最好的祝福。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM