MrH*_*tch 34 javascript reactjs redux
我目前有一种情况需要Redux Actions连续运行.我已经看过各种各样的中间件,比如redux-promise,如果你知道连续的动作是在根源(缺乏一个更好的术语)触发的动作的话,这似乎很好.
从本质上讲,我想维护一个可以随时添加的动作队列.每个对象在其状态中具有该队列的实例,并且相关的动作可以相应地排队,处理和出列.我有一个实现,但是这样做我在我的动作创建者中访问状态,这感觉就像一个反模式.
我将尝试给出一些用例和实现的背景信息.
假设您要创建一些列表并将其保留在服务器上.在列表创建时,服务器使用该列表的id进行响应,该ID用于与列表相关的后续API端点:
http://my.api.com/v1.0/lists/ // POST returns some id
http://my.api.com/v1.0/lists/<id>/items // API end points include id
Run Code Online (Sandbox Code Playgroud)
想象一下,客户希望对这些API点进行乐观更新,以增强用户体验 - 没有人喜欢看微调器.因此,当您创建列表时,会立即显示新列表,并在添加项目时添加选项:
+-------------+----------+
| List Name | Actions |
+-------------+----------+
| My New List | Add Item |
+-------------+----------+
Run Code Online (Sandbox Code Playgroud)
假设有人试图在初始创建调用的响应之前添加一个项目.items API依赖于id,因此我们知道在拥有该数据之前我们无法调用它.但是,我们可能希望乐观地显示新项目并将对项目API的调用排入队列,以便在创建调用完成后触发它.
我目前用来解决这个问题的方法是给每个列表一个动作队列 - 也就是说,将连续触发的Redux动作列表.
列表创建的reducer功能可能如下所示:
case ADD_LIST:
return {
id: undefined, // To be filled on server response
name: action.payload.name,
actionQueue: []
}
Run Code Online (Sandbox Code Playgroud)
然后,在动作创建者中,我们将行动排队而不是直接触发它:
export const createListItem = (name) => {
return (dispatch) => {
dispatch(addList(name)); // Optimistic action
dispatch(enqueueListAction(name, backendCreateListAction(name));
}
}
Run Code Online (Sandbox Code Playgroud)
为简洁起见,假设backendCreateListAction函数调用一个fetch API,它会在成功/失败时将消息分派到列表中.
这里让我担心的是enqueueListAction方法的实现.这是我访问状态以控制队列进度的地方.它看起来像这样(忽略名称上的匹配 - 实际上这实际上使用了clientId,但我试图保持示例简单):
const enqueueListAction = (name, asyncAction) => {
return (dispatch, getState) => {
const state = getState();
dispatch(enqueue(name, asyncAction));{
const thisList = state.lists.find((l) => {
return l.name == name;
});
// If there's nothing in the queue then process immediately
if (thisList.actionQueue.length === 0) {
asyncAction(dispatch);
}
}
}
Run Code Online (Sandbox Code Playgroud)
这里,假设enqueue方法返回一个普通操作,该操作将异步操作插入到列表actionQueue中.
整个事情感觉有点不利于谷物,但我不确定是否还有另一种方式可以解决它.另外,由于我需要在我的asyncActions中调度,我需要将调度方法传递给它们.
该方法中有类似的代码从列表中出列,如果存在则触发下一个操作:
const dequeueListAction = (name) => {
return (dispatch, getState) => {
dispatch(dequeue(name));
const state = getState();
const thisList = state.lists.find((l) => {
return l.name === name;
});
// Process next action if exists.
if (thisList.actionQueue.length > 0) {
thisList.actionQueue[0].asyncAction(dispatch);
}
}
Run Code Online (Sandbox Code Playgroud)
一般来说,我可以忍受这个,但我担心它是一个反模式,并且可能有一个更简洁,惯用的方式在Redux中这样做.
任何帮助表示赞赏.
我有满足您需求的完美工具。当您需要对 redux 进行大量控制(尤其是任何异步操作)并且需要按顺序发生 redux 操作时,没有比Redux Sagas更好的工具了。它构建在 es6 生成器之上,为您提供了很多控制权,因为从某种意义上说,您可以在某些点暂停代码。
您描述的操作队列就是所谓的saga。现在,由于它是为与 redux 一起使用而创建的,因此可以通过在组件中分派来触发这些传奇来运行。
由于 Sagas 使用生成器,您还可以确定地确保您的调度按特定顺序发生,并且仅在特定条件下发生。这是他们文档中的一个示例,我将引导您完成它以说明我的意思:
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if (token) {
yield call(Api.storeItem, {token})
yield take('LOGOUT')
yield call(Api.clearItem, 'token')
}
}
}
Run Code Online (Sandbox Code Playgroud)
好吧,乍一看有点令人困惑,但这个传奇定义了登录序列需要发生的确切顺序。由于生成器的性质,无限循环是允许的。当您的代码达到收益时,它将停在该行并等待。在您告诉它之前,它不会继续到下一行。所以看看它说的地方yield take('LOGIN_REQUEST')
。传奇将在此时屈服或等待,直到您分派“LOGIN_REQUEST”,之后传奇将调用授权方法,并继续直到下一个屈服。next 方法是异步的yield call(Api.storeItem, {token})
,因此在该代码解析之前它不会转到下一行。
现在,这就是奇迹发生的地方。这个传奇将再次停止,yield take('LOGOUT')
直到您在应用程序中发送 LOGOUT 消息。这一点至关重要,因为如果您在 LOGOUT 之前再次调度 LOGIN_REQUEST,则不会调用登录过程。现在,如果您调度 LOGOUT,它将循环回到第一个yield 并等待应用程序再次调度 LOGIN_REQUEST。
到目前为止,Redux Sagas 是我最喜欢与 Redux 一起使用的工具之一。它为您提供了对应用程序的强大控制权,任何阅读您的代码的人都会感谢您,因为现在所有内容一次只读取一行。
Pie*_*scy -2
您不必处理排队操作。它将隐藏数据流,并使您的应用程序调试起来更加繁琐。
我建议您在创建列表或项目时使用一些临时 ID,然后在您实际从商店收到真实 ID 时更新这些 ID。
也许是这样的?(没有测试,但你得到了 id):
编辑:我一开始不明白保存列表时需要自动保存项目。我编辑了createList
动作创建者。
/* REDUCERS & ACTIONS */
// this "thunk" action creator is responsible for :
// - creating the temporary list item in the store with some
// generated unique id
// - dispatching the action to tell the store that a temporary list
// has been created (optimistic update)
// - triggering a POST request to save the list in the database
// - dispatching an action to tell the store the list is correctly
// saved
// - triggering a POST request for saving items related to the old
// list id and triggering the correspondant receiveCreatedItem
// action
const createList = (name) => {
const tempList = {
id: uniqueId(),
name
}
return (dispatch, getState) => {
dispatch(tempListCreated(tempList))
FakeListAPI
.post(tempList)
.then(list => {
dispatch(receiveCreatedList(tempList.id, list))
// when the list is saved we can now safely
// save the related items since the API
// certainly need a real list ID to correctly
// save an item
const itemsToSave = getState().items.filter(item => item.listId === tempList.id)
for (let tempItem of itemsToSave) {
FakeListItemAPI
.post(tempItem)
.then(item => dispatch(receiveCreatedItem(tempItem.id, item)))
}
)
}
}
const tempListCreated = (list) => ({
type: 'TEMP_LIST_CREATED',
payload: {
list
}
})
const receiveCreatedList = (oldId, list) => ({
type: 'RECEIVE_CREATED_LIST',
payload: {
list
},
meta: {
oldId
}
})
const createItem = (name, listId) => {
const tempItem = {
id: uniqueId(),
name,
listId
}
return (dispatch) => {
dispatch(tempItemCreated(tempItem))
}
}
const tempItemCreated = (item) => ({
type: 'TEMP_ITEM_CREATED',
payload: {
item
}
})
const receiveCreatedItem = (oldId, item) => ({
type: 'RECEIVE_CREATED_ITEM',
payload: {
item
},
meta: {
oldId
}
})
/* given this state shape :
state = {
lists: {
ids: [ 'list1ID', 'list2ID' ],
byId: {
'list1ID': {
id: 'list1ID',
name: 'list1'
},
'list2ID': {
id: 'list2ID',
name: 'list2'
},
}
...
},
items: {
ids: [ 'item1ID','item2ID' ],
byId: {
'item1ID': {
id: 'item1ID',
name: 'item1',
listID: 'list1ID'
},
'item2ID': {
id: 'item2ID',
name: 'item2',
listID: 'list2ID'
}
}
}
}
*/
// Here i'm using a immediately invoked function just
// to isolate ids and byId variable to avoid duplicate
// declaration issue since we need them for both
// lists and items reducers
const lists = (() => {
const ids = (ids = [], action = {}) => ({
switch (action.type) {
// when receiving the temporary list
// we need to add the temporary id
// in the ids list
case 'TEMP_LIST_CREATED':
return [...ids, action.payload.list.id]
// when receiving the real list
// we need to remove the old temporary id
// and add the real id instead
case 'RECEIVE_CREATED_LIST':
return ids
.filter(id => id !== action.meta.oldId)
.concat([action.payload.list.id])
default:
return ids
}
})
const byId = (byId = {}, action = {}) => ({
switch (action.type) {
// same as above, when the the temp list
// gets created we store it indexed by
// its temp id
case 'TEMP_LIST_CREATED':
return {
...byId,
[action.payload.list.id]: action.payload.list
}
// when we receive the real list we first
// need to remove the old one before
// adding the real list
case 'RECEIVE_CREATED_LIST': {
const {
[action.meta.oldId]: oldList,
...otherLists
} = byId
return {
...otherLists,
[action.payload.list.id]: action.payload.list
}
}
}
})
return combineReducers({
ids,
byId
})
})()
const items = (() => {
const ids = (ids = [], action = {}) => ({
switch (action.type) {
case 'TEMP_ITEM_CREATED':
return [...ids, action.payload.item.id]
case 'RECEIVE_CREATED_ITEM':
return ids
.filter(id => id !== action.meta.oldId)
.concat([action.payload.item.id])
default:
return ids
}
})
const byId = (byId = {}, action = {}) => ({
switch (action.type) {
case 'TEMP_ITEM_CREATED':
return {
...byId,
[action.payload.item.id]: action.payload.item
}
case 'RECEIVE_CREATED_ITEM': {
const {
[action.meta.oldId]: oldList,
...otherItems
} = byId
return {
...otherItems,
[action.payload.item.id]: action.payload.item
}
}
// when we receive a real list
// we need to reappropriate all
// the items that are referring to
// the old listId to the new one
case 'RECEIVE_CREATED_LIST': {
const oldListId = action.meta.oldId
const newListId = action.payload.list.id
const _byId = {}
for (let id of Object.keys(byId)) {
let item = byId[id]
_byId[id] = {
...item,
listId: item.listId === oldListId ? newListId : item.listId
}
}
return _byId
}
}
})
return combineReducers({
ids,
byId
})
})()
const reducer = combineReducers({
lists,
items
})
/* REDUCERS & ACTIONS */
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
7261 次 |
最近记录: |