简体   繁体   English

反应redux架构表动作和减速器

[英]react redux architecting table actions and reducers

I have server driven data being displayed in tables on multiple pages. 我将服务器驱动的数据显示在多个页面的表中。

I currently have the following table actions 我目前有以下表格动作

pageChange
pageSizeChange
sort
load
loaded

I have filters on some pages which need to trigger the load. 我在一些需要触发负载的页面上有过滤器。

I have multiple entities which use this functionality, which will share most the above logic, but will need separate load functionality defining. 我有多个使用此功能的实体,它们将共享大部分上述逻辑,但需要单独的加载功能定义。

My thought was to have actions which take the table ID as a parameter, and then have a createTableReducer function which would also take this ID and would mount table nodes within entities, similarly to createModelReducer in react-redux-form 我的想法是让操作以表ID作为参数,然后有一个createTableReducer函数,该函数也将获取此ID并将表节点挂载到实体中,类似于react-redux-form中的 createModelReducer

How could I trigger the entity specific load actions from my generic actions without using something like redux saga? 如何在不使用redux saga之类的东西的情况下从我的通用操作触发实体特定的加载操作?

I was wondering about creating a higher order component and passing it the load function but I don't think this would solve my issue. 我想知道创建一个更高阶的组件并传递它的加载函数,但我认为这不会解决我的问题。 I could also call both the change action and the load action from the table component itself, but this doesn't' seem like a nice solution. 我也可以从表组件本身调用更改操作和加载操作,但这似乎不是一个很好的解决方案。

So, this is a pretty open-ended question and possibilities for implementing this are almost limitless... but, I'll take a shot at it anyhow. 所以,这是一个非常开放的问题,实现这个问题的可能性几乎是无限的......但是,无论如何我都会对它进行一次研究。 This answer isn't doctrine, it's just one possible way to do it. 这个答案不是学说,只是一种可行的方法。

Let's start by wishing we could plop our tables wherever we want. 让我们首先希望我们可以随时随地摆放桌子。 To differentiate one table from another, all we have to do is pass a tableId property that links a Table to the corresponding data in the store. 要将一个表与另一个表区分开来,我们所要做的就是传递一个tableId属性,该属性将Table链接到商店中的相应数据。

<Table tableId='customers' />
<Table tableId='products' />
<Table tableId='orders' />
<Table tableId='transactions' />

Next, let's define a state shape to work with. 接下来,让我们定义一个要使用的状态形状。 Each top-level key matches a tableId we wish to store data for. 每个顶级键与我们希望存储数据的tableId匹配。 You can make this shape be whatever you'd like, but I'm providing this so you have to do less mental visualization as I reference things in later code. 你可以将这个形状变成你想要的任何形状,但是我提供这个,所以当我在后面的代码中引用时,你必须做更少的心理可视化。 You can also name the top-level tableId keys whatever you want, just so long as you reference them consistently. 您也可以根据需要为顶级tableId键命名,只要您一致地引用它们即可。

{
   customers: {
     page: 0,
     sortBy: 'id',
     cols: [
       {id: 'id',         name: 'name'},
       {id: 'username',   name: 'Username'},
       {id: 'first_name', name: 'First Name'},
       {id: 'last_name',  name: 'Last Name'},
       ...
     ],
     rows: [
       {id: 1, username: 'bob', first_name: 'Bob', last_name: 'Smith', ...},
       ...
     ]
  },
  products: {
    ...
  },
  orders: {
    ...
  }
  ...
}

Now, let's get the hard part out of the way: the Table container. 现在,让我们解决困难的问题: Table容器。 It's a lot to digest all at once, but don't worry, I'll break down the important bits individually. 一次消化所有这一切都很多,但不要担心,我会单独分解重要的部分。

containers/Table.js 集装箱/ Table.js

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

const _Table = ({cols, rows, onClickSort, onClickNextPage}) => (
  <div>
    <table>
      <thead>
        <tr>
         {cols.map((col,key) => (
           <Th key={key} onClickSort={onClickSort(col.id)}>{col.name}</Th>
         )}
        </tr>
      </thead>
      <tbody>
        {rows.map((row,key) => ...}
      </tbody>
    </table>
    <button onClick={onClickNextPage}>Next Page</button>
  </div>
);

const Table = connect(
  ({table}, {tableId}) => ({    // example: if tableId = "customers" ...
    cols: table[tableId].cols,  // state.table.customers.cols
    rows: table[tableId].rows   // state.table.customers.rows
  }),
  (dispatch, {tableId}) => ({
    onClickSort: columnId => event => {
      dispatch(actions.tableSortColumn(tableId, columnId));
      // example: if user clicks 'last_name' column in customers table
      // dispatch(actions.tableSortColumn('customers', 'last_name'));
    },
    onClickNextPage: event => {
      dispatch(actions.tableNextPage(tableId))
    }
  })
)(_Table);

export default Table;

If you only learn one thing from this post, let it be this: 如果你只从这篇文章中学到一件事,就这样吧:

The thing to notice here is that mapStateToProps and mapDispatchToProps accepts a second argument called ownProps . 这里要注意的是mapStateToPropsmapDispatchToProps 接受第二个名为ownProps 参数

// did you know you can pass a second arg to these functions ?
const MyContainer = connect({
  (state, ownProps) => ...
  (dispatch, ownProps) => ...
})(...);

In the container I wrote above, I'm destructuring each of the vars we care about. 在我上面写的容器中,我正在解构我们关心的每个变量。

// Remember how I used the container ?
// here ownProps = {tableId: "customers"}
<Table tableId="customers" />

Now look at how I used connect 现在看看我如何使用connect

const Table = connect(
  // mapStateToProps
  ({table}, {tableId}) => ...
    // table = state.table
    // tableId = ownProps.tableId = "customers"


  // mapDispatchToProps
  (dispatch, {tableId}) => ...
    // dispatch = dispatch
    // tableId = ownProps.tableId = "customers"
)(_Table);

This way, when we're creating the dispatch handlers for the underlying component, ( _Table ), we'll have the tableId available to us within the handler. 这样,当我们为底层组件( _Table )创建调度处理程序时,我们将在处理程序中使用tableId It's actually kinda nice that the _Table component itself doesn't even have to concern itself with the tableId prop if you don't want it to. 如果你不想要它,那么_Table组件本身甚至不必关心tableId prop,这实在是太好了。

Next notice the way the onClickSort functions is defined. 接下来请注意定义onClickSort函数的方式。

onClickSort: columnId => event => {
  dispatch(actions.tableSortColumn(tableId, columnId));
}

The _Table component passes this function to Th using _Table组件使用此函数将此函数传递给Th

<Th key={key} onClickSort={onClickSort(col.id)}>{col.name}</Th>

See how it only sends in the columnId to the handler here ? 看看它如何columnId发送到处理程序? Next, we'll see how Th is the one to send the event , which finally dispatches the action. 接下来,我们将看到Th是如何发送event ,最终调度该事件。

components/Table/Th.js 组件/表/ Th.js

import React from 'react';

const Th = ({onClickSort, children}) => (
  <th>
    <a href="#sort" onClickSort={event => {
      event.preventDefault();
      onClickSort(event);
    }}>{children}</a>
  </th>
);

export default Th;

It's not necessary to keep the event if you don't want it, but I figured I'd show you how to attach it in case you wanted to utilize it for something. 如果你不想要它,没有必要保留event ,但我想我会告诉你如何附加它,以防你想用它来做某事。

Moving along ... 继续......

actions/index.js 动作/ index.js

Your actions file is going to look pretty standard. 您的操作文件看起来非常标准。 Notice we provide access to tableId in each of the table actions. 请注意,我们在每个表操作中提供对tableId访问。

export const TABLE_SORT_COLUMN = 'TABLE_SORT_COLUMN';
export const TABLE_NEXT_PAGE = 'TABLE_NEXT_PAGE';

export const tableSortColumn = (tableId, columnId) => ({
  type: TABLE_SORT_COLUMN, payload: {tableId, columnId}
});

export const tableNextPage = (tableId) => ({
  type: TABLE_NEXT_PAGE, payload: {tableId}
});

...

Lastly, your tableReducer might look something like this. 最后,您的tableReducer可能看起来像这样。 Again, nothing too special here. 再说一遍,这里没什么特别的。 You'll do a regular switch on the action type and then update state accordingly. 您将定期switch操作类型,然后相应地更新状态。 You can affect the appropriate part of the state using action.payload.tableId . 您可以使用action.payload.tableId影响状态的相应部分。 Just remember the state shape I proposed. 记住我提出的状态。 If you choose a different state shape, you'll have to change this code to match 如果选择其他状态形状,则必须更改此代码才能匹配

const defaultState = {
  customers: {
    page: 0,
    sortBy: null,
    cols: [],
    rows: []
  }
};

// deep object assignment for nested objects
const tableAssign = (state, tableId, data) =>
  Object.assign({}, state, {
    [tableId]: Object.assign({}, state[tableId], data)
  });

const tableReducer = (state=defaultState, {type, payload}) => {
  switch (type) {
    case TABLE_SORT_COLUMN:
      return tableAssign(state, payload.tableId, {
        sortBy: payload.columnId
      });
    case TABLE_NEXT_PAGE:
      return tableAssign(state, payload.tableId, {
        page: state[payload.tableId].page + 1
      });
    default:
      return state;
  }
};

Remarks: 备注:

I'm not going to go into the async loading of the table data. 我不打算进入表数据的异步加载。 Such a workflow is already nicely detailed in the redux docs: Async Actions . 在redux文档中已经很好地详细说明了这样的工作流程: Async Actions Your choice of redux-thunk, redux-promise, or redux-saga is up to you. 您选择的redux-thunk,redux-promise或redux-saga取决于您。 Choose whichever you understand best! 选择你最了解的! The key to implementing TABLE_DATA_FETCH properly is making sure you dispatch the tableId (along with any other parameters you need) like I did in the other onClick* handlers. 正确实现TABLE_DATA_FETCH的关键是确保调度tableId (以及您需要的任何其他参数),就像我在其他onClick*处理程序中一样。

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

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