Redux 依次发送两个分派然后第一个分派丢失

Redux sending two dispatch after each other then the first dispatch get lost

我学习了 React Javascript 和 Redux,现在我遇到了这个问题。

这是一个codesandbox

这样试试:

我一个接一个地发送两个 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>