Redux 依次发送两个分派然后第一个分派丢失
Redux sending two dispatch after each other then the first dispatch get lost
我学习了 React Javascript 和 Redux,现在我遇到了这个问题。
这是一个codesandbox
这样试试:
- 搜索书名“dep”
- 观察日志显示“搜索 url 是:”,当它应该显示“开始图书搜索..”时
我一个接一个地发送两个 Redux dispatch
,第一个永远不会到达低谷。
基本上是这样的:
我有一个用于学习 React 的图书搜索应用程序,当按下搜索一本书时,我会在该应用程序中发送 dispatch
这是class发送状态:
import { booksActionTypes } from "./books.types";
export const showLog = () => ({
type: booksActionTypes.SHOWLOG,
});
export const setLogMessage = (log) => ({
type: booksActionTypes.LOG_MESSAGE,
payload: log,
});
export const clearPosts = () => ({
type: booksActionTypes.CLEAR_POSTS,
});
export const requestStart = () => ({
type: booksActionTypes.REQUEST_START,
});
export const requestSuccess = (booksList) => ({
type: booksActionTypes.REQUEST_SUCCESS,
payload: booksList,
});
export const requestFailure = (errMsg) => ({
type: booksActionTypes.REQUEST_FAILURE,
payload: errMsg,
});
export const actionCreators = {
// "applicationUrl": "http://localhost:51374", http://erikswed.ddns.net:8965/api/BooksXml/getbooks/fromall/?title=dep&author=&genre=&price=
// "sslPort": 44378
requestBooks: (book) => async (dispatch, getState) => {
dispatch(requestStart());
dispatch(
setLogMessage(() => ({
message: "Starting books search..",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
var queryString = Object.keys(book)
.map((key) => {
return encodeURIComponent(key) + "=" + encodeURIComponent(book[key]);
})
.join("&");
var url =
"htftp://erikswed.ddns.net:8965/api/BooksXml/getbooks/fromall/?" +
queryString;
dispatch(
setLogMessage(() => ({
message: "Search url is: ",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
dispatch(
setLogMessage(() => ({
message: "Sdsadsasada: ",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
await fetch(url)
.then((res) => res.json())
.then((xml) => {
dispatch(requestSuccess(xml));
})
.catch((errMsg) => {
console.log(errMsg);
dispatch(requestFailure(errMsg));
fetch("books.xml")
.then((res) => res.text())
.then((xmlString) => getFromLocalDatabas(dispatch, xmlString, book))
.catch((err) => {
dispatch(requestFailure(errMsg));
});
});
},
};
function getFromLocalDatabas(dispatch, xmlFile, book) {
var parser = new window.DOMParser();
var xmlDoc = parser.parseFromString(xmlFile, "text/xml");
var books = xmlDoc.documentElement.childNodes;
var map = [];
for (var i = 0; i < books.length; i++) {
var author = books[i]
.getElementsByTagName("author")[0]
.childNodes[0].nodeValue.toLowerCase();
var title = books[i]
.getElementsByTagName("title")[0]
.childNodes[0].nodeValue.toLowerCase();
var genre = books[i]
.getElementsByTagName("genre")[0]
.childNodes[0].nodeValue.toLowerCase();
var price = books[i]
.getElementsByTagName("price")[0]
.childNodes[0].nodeValue.toLowerCase();
var publish_date = books[i]
.getElementsByTagName("publish_date")[0]
.childNodes[0].nodeValue.toLowerCase();
var description = books[i]
.getElementsByTagName("description")[0]
.childNodes[0].nodeValue.toLowerCase();
if (book.author && author.indexOf(book.author.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.title && title.indexOf(book.title.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.genre && genre.indexOf(book.genre.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.price && price.indexOf(book.price.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (
book.publish_date &&
publish_date.indexOf(book.publish_date.toLowerCase()) >= 0
) {
sendBackThisBook(map, books[i]);
}
if (
book.description &&
description.indexOf(book.description.toLowerCase()) >= 0
) {
sendBackThisBook(map, books[i]);
}
}
dispatch(requestSuccess(map));
}
function sendBackThisBook(map, book) {
var index = map.findIndex((x) => x.id == book.getAttribute("id"));
if (index != -1) {
console.log(`exist skipping `, book.getAttribute("id"));
return;
}
map.push({
id: book.getAttribute("id"),
title: book.getElementsByTagName("title")[0].childNodes[0].nodeValue,
author: book.getElementsByTagName("author")[0].childNodes[0].nodeValue,
genre: book.getElementsByTagName("genre")[0].childNodes[0].nodeValue,
price: book.getElementsByTagName("price")[0].childNodes[0].nodeValue,
publish_date: book.getElementsByTagName("publish_date")[0].childNodes[0]
.nodeValue,
description: book.getElementsByTagName("description")[0].childNodes[0]
.nodeValue,
});
}
这是 Class 接收 Redux Store 状态:
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { connect } from "react-redux";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import { Icon } from "@material-ui/core";
import {
CallMade,
CallReceived,
FiberManualRecord,
Error,
} from "@material-ui/icons";
function createData(message, timestamp, type) {
return { message, timestamp, type };
}
const rows = [];
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
icons: {
fontSize: 18,
},
connecting: {
color: "#11FF0C",
},
connected: {
color: "#11FF0C",
},
disconnecting: {
color: "#FF5050",
},
disconnected: {
color: "#FF5050",
},
error: {
color: "#FF5050",
},
request: {
// color: "#11FF0C"
},
response: {
// color: "#11FF0C"
},
}));
function Logger(props) {
const { v4: uuidv4 } = require("uuid");
const classes = useStyles();
const [theLog, addLog] = useState([]);
useEffect(() => {
if (typeof props.log === "function") {
const values = props.log();
addLog([
...theLog,
createData(values.message, values.timestamp, values.type),
]);
}
}, [props.log]);
function createData(message, timestamp, type) {
console.log("s");
return { message, timestamp, type };
}
return (
<div>
{props.showLog ? (
<div className={classes.root}>
<Table aria-labelledby="tableTitle" size="small">
<TableBody>
{theLog.map((row, index) => {
return (
<TableRow
key={uuidv4()}
hover
role="checkbox"
tabIndex={-1}
key={row.timestamp}
>
<TableCell padding="checkbox">
<Icon>
{row.type === "connecting" && (
<FiberManualRecord
className={`${classes.connecting} ${classes.icons}`}
/>
)}
{row.type === "connected" && (
<FiberManualRecord
className={`${classes.connected} ${classes.icons}`}
/>
)}
{row.type === "disconnecting" && (
<FiberManualRecord
className={`${classes.disconnecting} ${classes.icons}`}
/>
)}
{row.type === "disconnected" && (
<FiberManualRecord
className={`${classes.disconnected} ${classes.icons}`}
/>
)}
{row.type === "error" && (
<Error
className={`${classes.error} ${classes.icons}`}
/>
)}
{row.type === "request" && (
<CallMade
className={`${classes.request} ${classes.icons}`}
/>
)}
{row.type === "response" && (
<CallReceived
className={`${classes.response} ${classes.icons}`}
/>
)}
</Icon>
</TableCell>
<TableCell>{row.message}</TableCell>
<TableCell>{row.timestamp}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : null}
</div>
);
}
function mapStateToProps(state) {
return {
log: state.reducer.log,
showLog: state.reducer.showLog,
};
}
export default connect(mapStateToProps, null)(Logger);
首先,您的代码有很多问题;你在 redux 状态下保存函数,函数 cannot be serialized 任何使用 Redux 的人都会认为这是一个很大的代码味道。
另一件事是您只将当前消息保存在 Redux 状态,但您想要所有消息,因此您的 Logger 组件正在尝试将日志保存在本地状态,这就是您出错的地方。
当你dispatch the action requestBooks react-redux will batch synchronous state updates so Logger is not rendered twice for each dispatched setLogMessage so the effect in logger不是第一次执行时
你的效果也有一些问题(缺少依赖),你可以通过这样的效果来解决它们:
const {log} = props
useEffect(() => {
if (typeof log === "function") {
const values = log();
addLog(theLog=>[//no dependency on theLog and no stale closure
...theLog,
createData(values.message, values.timestamp, values.type),
]);
}
}, [log]);
然后在你的 thunk 中,你应该给 react-redux 一些时间来渲染,以便 React 可以再次 运行 效果:
dispatch(
setLogMessage(() => ({
message: "Starting books search..",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting"
}))
);
//React will re render so effect in Logger will run
await Promise.resolve();
这充其量是一个有问题的解决方案,我建议首先不要在 redux 状态下保存函数,而只是在状态下保存所有日志,这样您就不必在 Logger 组件中创建本地状态。
更新
这是一个示例,说明如何对 redux 状态更新进行批处理,因此您的效果不会 运行 每次分派,因为 redux 正在对状态更新进行批处理并跳过渲染。
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 1 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
//thunk action that adds twice (dispatch ADD twice) synchronously
// because redux will batch the App component will not render
// between dispatches causing the effect to not run twice
const syncAdd = () => (dispatch) => {
dispatch(add());
dispatch(add());
};
//Same thunk but async so when first add is dispatched it will put
// the rest of the code on the event loop and give react time to
// render, this is buggy and unpredictable and why you should not
// copy redux state in local state when redux state changes
const asyncAdd = () => (dispatch) => {
dispatch(add());
//async syntax does not work on SO snippet (babel too old)
Promise.resolve().then(() => dispatch(add()));
};
const reducer = (state, { type }) => {
if (type === ADD) {
console.log(
`ADD dispatched, count before reducer updates: ${state.count}`
);
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
//improvised thunk middleware
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const count = useSelector(selectCount);
const dispatch = useDispatch();
React.useEffect(
() => console.log(`Effect run with count: ${count}`),
[count]
);
return (
<div>
{count}
<button onClick={() => dispatch(syncAdd())}>
sync add
</button>
<button onClick={() => dispatch(asyncAdd())}>
async add
</button>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
我学习了 React Javascript 和 Redux,现在我遇到了这个问题。
这是一个codesandbox
这样试试:
- 搜索书名“dep”
- 观察日志显示“搜索 url 是:”,当它应该显示“开始图书搜索..”时
我一个接一个地发送两个 Redux dispatch
,第一个永远不会到达低谷。
基本上是这样的:
我有一个用于学习 React 的图书搜索应用程序,当按下搜索一本书时,我会在该应用程序中发送 dispatch
这是class发送状态:
import { booksActionTypes } from "./books.types";
export const showLog = () => ({
type: booksActionTypes.SHOWLOG,
});
export const setLogMessage = (log) => ({
type: booksActionTypes.LOG_MESSAGE,
payload: log,
});
export const clearPosts = () => ({
type: booksActionTypes.CLEAR_POSTS,
});
export const requestStart = () => ({
type: booksActionTypes.REQUEST_START,
});
export const requestSuccess = (booksList) => ({
type: booksActionTypes.REQUEST_SUCCESS,
payload: booksList,
});
export const requestFailure = (errMsg) => ({
type: booksActionTypes.REQUEST_FAILURE,
payload: errMsg,
});
export const actionCreators = {
// "applicationUrl": "http://localhost:51374", http://erikswed.ddns.net:8965/api/BooksXml/getbooks/fromall/?title=dep&author=&genre=&price=
// "sslPort": 44378
requestBooks: (book) => async (dispatch, getState) => {
dispatch(requestStart());
dispatch(
setLogMessage(() => ({
message: "Starting books search..",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
var queryString = Object.keys(book)
.map((key) => {
return encodeURIComponent(key) + "=" + encodeURIComponent(book[key]);
})
.join("&");
var url =
"htftp://erikswed.ddns.net:8965/api/BooksXml/getbooks/fromall/?" +
queryString;
dispatch(
setLogMessage(() => ({
message: "Search url is: ",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
dispatch(
setLogMessage(() => ({
message: "Sdsadsasada: ",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting",
}))
);
await fetch(url)
.then((res) => res.json())
.then((xml) => {
dispatch(requestSuccess(xml));
})
.catch((errMsg) => {
console.log(errMsg);
dispatch(requestFailure(errMsg));
fetch("books.xml")
.then((res) => res.text())
.then((xmlString) => getFromLocalDatabas(dispatch, xmlString, book))
.catch((err) => {
dispatch(requestFailure(errMsg));
});
});
},
};
function getFromLocalDatabas(dispatch, xmlFile, book) {
var parser = new window.DOMParser();
var xmlDoc = parser.parseFromString(xmlFile, "text/xml");
var books = xmlDoc.documentElement.childNodes;
var map = [];
for (var i = 0; i < books.length; i++) {
var author = books[i]
.getElementsByTagName("author")[0]
.childNodes[0].nodeValue.toLowerCase();
var title = books[i]
.getElementsByTagName("title")[0]
.childNodes[0].nodeValue.toLowerCase();
var genre = books[i]
.getElementsByTagName("genre")[0]
.childNodes[0].nodeValue.toLowerCase();
var price = books[i]
.getElementsByTagName("price")[0]
.childNodes[0].nodeValue.toLowerCase();
var publish_date = books[i]
.getElementsByTagName("publish_date")[0]
.childNodes[0].nodeValue.toLowerCase();
var description = books[i]
.getElementsByTagName("description")[0]
.childNodes[0].nodeValue.toLowerCase();
if (book.author && author.indexOf(book.author.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.title && title.indexOf(book.title.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.genre && genre.indexOf(book.genre.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (book.price && price.indexOf(book.price.toLowerCase()) >= 0) {
sendBackThisBook(map, books[i]);
}
if (
book.publish_date &&
publish_date.indexOf(book.publish_date.toLowerCase()) >= 0
) {
sendBackThisBook(map, books[i]);
}
if (
book.description &&
description.indexOf(book.description.toLowerCase()) >= 0
) {
sendBackThisBook(map, books[i]);
}
}
dispatch(requestSuccess(map));
}
function sendBackThisBook(map, book) {
var index = map.findIndex((x) => x.id == book.getAttribute("id"));
if (index != -1) {
console.log(`exist skipping `, book.getAttribute("id"));
return;
}
map.push({
id: book.getAttribute("id"),
title: book.getElementsByTagName("title")[0].childNodes[0].nodeValue,
author: book.getElementsByTagName("author")[0].childNodes[0].nodeValue,
genre: book.getElementsByTagName("genre")[0].childNodes[0].nodeValue,
price: book.getElementsByTagName("price")[0].childNodes[0].nodeValue,
publish_date: book.getElementsByTagName("publish_date")[0].childNodes[0]
.nodeValue,
description: book.getElementsByTagName("description")[0].childNodes[0]
.nodeValue,
});
}
这是 Class 接收 Redux Store 状态:
import React, { useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { connect } from "react-redux";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import { Icon } from "@material-ui/core";
import {
CallMade,
CallReceived,
FiberManualRecord,
Error,
} from "@material-ui/icons";
function createData(message, timestamp, type) {
return { message, timestamp, type };
}
const rows = [];
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
icons: {
fontSize: 18,
},
connecting: {
color: "#11FF0C",
},
connected: {
color: "#11FF0C",
},
disconnecting: {
color: "#FF5050",
},
disconnected: {
color: "#FF5050",
},
error: {
color: "#FF5050",
},
request: {
// color: "#11FF0C"
},
response: {
// color: "#11FF0C"
},
}));
function Logger(props) {
const { v4: uuidv4 } = require("uuid");
const classes = useStyles();
const [theLog, addLog] = useState([]);
useEffect(() => {
if (typeof props.log === "function") {
const values = props.log();
addLog([
...theLog,
createData(values.message, values.timestamp, values.type),
]);
}
}, [props.log]);
function createData(message, timestamp, type) {
console.log("s");
return { message, timestamp, type };
}
return (
<div>
{props.showLog ? (
<div className={classes.root}>
<Table aria-labelledby="tableTitle" size="small">
<TableBody>
{theLog.map((row, index) => {
return (
<TableRow
key={uuidv4()}
hover
role="checkbox"
tabIndex={-1}
key={row.timestamp}
>
<TableCell padding="checkbox">
<Icon>
{row.type === "connecting" && (
<FiberManualRecord
className={`${classes.connecting} ${classes.icons}`}
/>
)}
{row.type === "connected" && (
<FiberManualRecord
className={`${classes.connected} ${classes.icons}`}
/>
)}
{row.type === "disconnecting" && (
<FiberManualRecord
className={`${classes.disconnecting} ${classes.icons}`}
/>
)}
{row.type === "disconnected" && (
<FiberManualRecord
className={`${classes.disconnected} ${classes.icons}`}
/>
)}
{row.type === "error" && (
<Error
className={`${classes.error} ${classes.icons}`}
/>
)}
{row.type === "request" && (
<CallMade
className={`${classes.request} ${classes.icons}`}
/>
)}
{row.type === "response" && (
<CallReceived
className={`${classes.response} ${classes.icons}`}
/>
)}
</Icon>
</TableCell>
<TableCell>{row.message}</TableCell>
<TableCell>{row.timestamp}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : null}
</div>
);
}
function mapStateToProps(state) {
return {
log: state.reducer.log,
showLog: state.reducer.showLog,
};
}
export default connect(mapStateToProps, null)(Logger);
首先,您的代码有很多问题;你在 redux 状态下保存函数,函数 cannot be serialized 任何使用 Redux 的人都会认为这是一个很大的代码味道。
另一件事是您只将当前消息保存在 Redux 状态,但您想要所有消息,因此您的 Logger 组件正在尝试将日志保存在本地状态,这就是您出错的地方。
当你dispatch the action requestBooks react-redux will batch synchronous state updates so Logger is not rendered twice for each dispatched setLogMessage so the effect in logger不是第一次执行时
你的效果也有一些问题(缺少依赖),你可以通过这样的效果来解决它们:
const {log} = props
useEffect(() => {
if (typeof log === "function") {
const values = log();
addLog(theLog=>[//no dependency on theLog and no stale closure
...theLog,
createData(values.message, values.timestamp, values.type),
]);
}
}, [log]);
然后在你的 thunk 中,你应该给 react-redux 一些时间来渲染,以便 React 可以再次 运行 效果:
dispatch(
setLogMessage(() => ({
message: "Starting books search..",
timestamp:
new Date().getHours() +
"-" +
new Date().getMinutes() +
"-" +
new Date().getSeconds(),
type: "connecting"
}))
);
//React will re render so effect in Logger will run
await Promise.resolve();
这充其量是一个有问题的解决方案,我建议首先不要在 redux 状态下保存函数,而只是在状态下保存所有日志,这样您就不必在 Logger 组件中创建本地状态。
更新
这是一个示例,说明如何对 redux 状态更新进行批处理,因此您的效果不会 运行 每次分派,因为 redux 正在对状态更新进行批处理并跳过渲染。
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 1 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
//thunk action that adds twice (dispatch ADD twice) synchronously
// because redux will batch the App component will not render
// between dispatches causing the effect to not run twice
const syncAdd = () => (dispatch) => {
dispatch(add());
dispatch(add());
};
//Same thunk but async so when first add is dispatched it will put
// the rest of the code on the event loop and give react time to
// render, this is buggy and unpredictable and why you should not
// copy redux state in local state when redux state changes
const asyncAdd = () => (dispatch) => {
dispatch(add());
//async syntax does not work on SO snippet (babel too old)
Promise.resolve().then(() => dispatch(add()));
};
const reducer = (state, { type }) => {
if (type === ADD) {
console.log(
`ADD dispatched, count before reducer updates: ${state.count}`
);
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
//improvised thunk middleware
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const count = useSelector(selectCount);
const dispatch = useDispatch();
React.useEffect(
() => console.log(`Effect run with count: ${count}`),
[count]
);
return (
<div>
{count}
<button onClick={() => dispatch(syncAdd())}>
sync add
</button>
<button onClick={() => dispatch(asyncAdd())}>
async add
</button>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>