组件中的 React-Redux 状态与 store 中的状态不同

IT技术 javascript reactjs redux react-redux
2021-05-10 12:52:27

我正在使用 React 和 Redux 构建应用程序。

我有一个 Account 组件,它从componentWillMount方法中的服务器获取数据在获取数据时,组件必须显示“正在加载”文本,因此我已将“isFetching”属性添加到帐户缩减程序中。从服务器获取数据时,此属性设置为 true。

问题是,在获取数据时,该render方法中“isFetching”属性的值为 false,而同时 的值为store.getState().account.isFetchingtrue(必须如此)。这导致例外,因为this.props.isFetching是假的,那么代码是试图展示this.props.data.protectedString,而data仍然被从服务器加载(所以它为空)。

我假设 mapStateToProps 绑定了一些错误的值(可能是初始状态),但我不知道为什么以及如何修复它。

这是我的 AccountView 代码:

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

class AccountView extends React.Component {
    componentWillMount() {
        const token = this.props.token;
        // fetching the data using credentials
        this.props.actions.accountFetchData(token);
    }

    render() {
        console.log("store state", window.store.getState().account); // isFetching == true
        console.log("componentState", window.componentState); // isFetching == false
        return (
            <div>
                {this.props.isFetching === true ? <h3>LOADING</h3> : <div>{this.props.data.protectedString}</div> }
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    window.componentState = state.account;
    return {
        data: state.account.data,
        isFetching: state.account.isFetching
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        actions: bindActionCreators(actionCreators, dispatch)
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(AccountView);

帐户减少器:

const initialState = {
    data: null,
    isFetching: false
};

export default function(state = initialState, action) {
    switch (action.type) {
    case ACCOUNT_FETCH_DATA_REQUEST:
        return Object.assign({}, state, {
            isFetching: true
        });
    case ACCOUNT_RECEIVE_DATA:
        return Object.assign({}, state, {
            data: action.payload.data,
            isFetching: false
        });
    default:
      return state;
  }
}

行动:

export function accountReceiveData(data) {
    return {
        type: ACCOUNT_RECEIVE_DATA,
        payload: {
            data
        }
    };
}

export function accountFetchDataRequest() {
    return {
        type: ACCOUNT_FETCH_DATA_REQUEST
    };
}

export function accountFetchData(token) {
    return (dispatch, state) => {
        dispatch(accountFetchDataRequest());

        axios({
            // axios parameters to fetch data from the server
        })
        .then(checkHttpStatus)
        .then((response) => {
            dispatch(accountReceiveData(response.data));
        })
        .catch((error) => {
            //error handling
        });
    };
}

这就是我创建商店的方式:

import { applyMiddleware, compose, createStore } from 'redux';
import { routerMiddleware } from 'react-router-redux';

import rootReducer from '../reducers';

export default function configureStore(initialState, history) {
    // Add so dispatched route actions to the history
    const reduxRouterMiddleware = routerMiddleware(history);

    const middleware = applyMiddleware(thunk, reduxRouterMiddleware);

    const createStoreWithMiddleware = compose(
        middleware
    );

    return createStoreWithMiddleware(createStore)(rootReducer, initialState);
}

在 index.js 中:

import { createBrowserHistory } from 'history';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore from './store/configureStore';

const initialState = {};
const newBrowserHistory = createBrowserHistory();
const store = configureStore(initialState, newBrowserHistory);
const history = syncHistoryWithStore(newBrowserHistory, store);

// for test purposes
window.store = store;

我的代码基于这个例子 - https://github.com/joshgeller/react-redux-jwt-auth-example

代码看起来相同,但由于某些module的新版本,我更改了一些地方。

2个回答

当您使用 react & redux 获取数据时,您应该始终问自己这两个问题:

  1. 我的数据还有效吗?
  2. 我目前正在获取数据吗?

您已经使用 the 回答了第二个问题,isFetching但第一个问题仍然存在,这就是导致您出现问题的原因。你应该做的是didInvalidate在你的减速器(https://github.com/reactjs/redux/blob/master/docs/advanced/AsyncActions.md)中使用

有了它,didInvalidate您可以轻松地检查您的数据是否有效,并在需要时通过调度类似INVALIDATE_ACCOUNT. 由于您尚未获取数据,因此默认情况下您的数据无效。

(奖励)您可能会使数据无效的一些示例:

  • 上次提取日期 > X 分钟
  • 您修改了一些数据,需要再次获取这些数据
  • 其他人修改了此数据,您通过 Websockets 收到失效操作

以下是您的渲染图的外观:

class AccountView extends React.Component {
  componentDidMount() { // Better to call data from componentDidMount than componentWillMount: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
    const token = this.props.token;
    // fetching the data using credentials
    if (this.props.didInvalidate && !this.props.isFetching) {
      this.props.actions.accountFetchData(token);
    }
  }

  render() {
    const {
      isFetching,
      didInvalidate,
      data,
    } = this.props;

    if (isFetching || (didInvalidate && !isFetching)) {
      return <Loading />; // You usually have your loading spinner or so in a component
    }

    return (
      <div>
        {data.protectedString}
      </div>
    );
  }
}

这是您的帐户减速器didInvalidate

const initialState = {
  isFetching: false,
  didInvalidate: true,
  data: null,
};

export default function(state = initialState, action) {
  switch (action.type) {
    case INVALIDATE_ACCOUNT:
      return { ...state, didInvalidate: true };
    case ACCOUNT_FETCH_DATA_REQUEST:
      return {
        ...state,
        isFetching: true,
      };
    case ACCOUNT_RECEIVE_DATA:
      return {
        ...state,
        data: action.payload.data,
        isFetching: false,
        didInvalidate: false,
      };
    default:
      return state;
  }
}

在您的新生命周期下方:

1. 先渲染

  • 描述:尚未发生提取
  • 减速机: { isFetching: false, didInvalidate: true, data: null }
  • 使成为: <Loading />

2.componentDidMount

  • 说明:数据失效&&不取数据-->去取数据

3. 函数调用:accountFetchData (1)

  • 描述:通知reducers你当前正在获取,然后异步获取数据
  • 派遣: { type: ACCOUNT_FETCH_DATA_REQUEST }

4. 账户缩减器

  • 描述:Reducers 收到调度通知并相应地修改它们的值
  • 减速机: { isFetching: true, didInvalidate: false, data: null }

5.二次渲染

  • 描述:由于 Account reducer 改变了,所以在渲染中运行了一秒钟
  • 减速机: { isFetching: true, didInvalidate: false, data: null }
  • 使成为: <Loading />

6. 函数调用:accountFetchData (2)

  • 描述:最终从步骤3中获取数据
  • 调度:{ 类型:ACCOUNT_RECEIVE_DATA,有效载荷:{ 数据}}

7. 账户缩减器

  • 描述:Reducers 收到调度通知并相应地修改它们的值
  • 减速机: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }

8. 第三次渲染

  • 描述:数据已获取并准备显示
  • 减速机: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }
  • 使成为: <div>42: The answer to life</div>

希望能帮助到你。


编辑:让我在另一个答案的评论中回答您的问题

@Icepickle 我不确定这是一种干净的方法。假设用户将转到 /account URL。然后到其他一些 URL。然后回到/帐户。当第二次从服务器加载数据时,isFetching 将为真,并且必须显示“正在加载”文本,但“数据”变量不会为空,因为它将包含来自前一个请求的数据. 因此,将显示旧数据而不是“加载”。

有了这个didInvalidate值,就没有无限制重新获取的风险,因为组件会知道您的数据是否有效。

在 中componentDidMount,重新获取的条件将为假,因为值如下{ isFetching: false, didInvalidate: false }那就不用refetch了。

if (this.props.didInvalidate && !this.props.isFetching)

奖励:但是请注意didInvalidate.

人们很少谈论这个问题,但你会开始问这个问题“从我的数据无效时开始?” (= 我什么时候应该重新获取?)

减速机

如果可以的话,让我从长远来看重构您的减速器代码。

您的减速器将更加module化且易于维护。

import { combineReducers } from 'redux';

export default combineReducers({
  didInvalidate,
  isFetching,
  lastFetchDate,
  data,
  errors,
});

function lastFetchDate(state = true, action) {
  switch (action.type) {
    case 'ACCOUNT_RECEIVE_DATA':
      return new Date();
    default:
      return state;
  }
}

function didInvalidate(state = true, action) {
  switch (action.type) {
    case 'INVALIDATE_ACCOUNT':
        return true;
    case 'ACCOUNT_RECEIVE_DATA':
      return false;
    default:
      return state;
  }
}

function isFetching(state = false, action) {
  switch (action.type) {
    case 'ACCOUNT_FETCH_DATA_REQUEST':
      return true;
    case 'ACCOUNT_RECEIVE_DATA':
      return false;
    default:
      return state;
  }
}

function data(state = {}, action) {
  switch (action.type) {
    case 'ACCOUNT_RECEIVE_DATA': 
      return {
        ...state,
        ...action.payload.data,
      };
    default:
      return state;
  }
}

function errors(state = [], action) {
  switch (action.type) {
    case 'ACCOUNT_ERRORS':
      return [
        ...state,
        action.error,
      ];
    case 'ACCOUNT_RECEIVE_DATA':
      return state.length > 0 ? [] : state;
    default:
      return state;
  }
}

行动

我将只添加失效函数,以便更容易理解我在组件中调用的函数。(注意:我没有重命名你的函数,但你一定要注意命名)

export function invalidateAccount() {
  return {
      type: INVALIDATE_ACCOUNT
  };
}

export function accountReceiveData(data) {
  return {
      type: ACCOUNT_RECEIVE_DATA,
      payload: {
          data
      }
  };
}

export function accountFetchDataRequest() {
  return {
      type: ACCOUNT_FETCH_DATA_REQUEST
  };
}

export function accountFetchData(token) {
  return (dispatch, state) => {
      dispatch(accountFetchDataRequest());

      axios({
          // axios parameters to fetch data from the server
      })
      .then(checkHttpStatus)
      .then((response) => {
          dispatch(accountReceiveData(response.data));
      })
      .catch((error) => {
          //error handling
      });
  };
}

零件

您将不得不在某个时候使您的数据无效。我认为您的帐户数据在 60 分钟后将不再有效。

import isBefore from 'date-fns/is_before';
import addMinutes from 'date-fns/add_minutes'

const ACCOUNT_EXPIRATION_MINUTES = 60;

class AccountView extends React.Component {
  componentDidMount() {
    const token = this.props.token;
    // fetching the data using credentials
    if (this.props.didInvalidate && !this.props.isFetching) {
      this.props.actions.accountFetchData(token);
    }

    // Below the check if your data is expired or not
    if (
      !this.props.didInvalidate && !this.props.isFetching &&
      isBefore(
        addMinutes(this.props.lastFetchDate, ACCOUNT_EXPIRATION_MINUTES), new Date()
      )
    ) {
      this.props.actions.invalidateAccount();
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.didInvalidate && !nextProps.isFetching) {
      nextProps.actions.accountFetchData(token);
    }
  }

  render() {
    const {
      isFetching,
      didInvalidate,
      lastFetchDate,
      data,
    } = this.props;

    /*
    * Do not display the expired data, the componentDidMount will invalidate your data and refetch afterwars
    */
    if (!didInvalidate && !isFetching && 
      isBefore(addMinutes(lastFetchDate, ACCOUNT_EXPIRATION_MINUTES), new Date())
    ) {
      return <Loading />;
    }

    if (isFetching || (didInvalidate && !isFetching)) {
      return <Loading />; // You usually have your loading spinner or so in a component
    }

    return (
      <div>
        {data.protectedString}
      </div>
    );
  }
}

这段代码可以更清晰,但这样阅读更清晰:)

你的三元语句不是切换了吗?你的渲染函数有这个:

{this.props.isFetching === true ? <h3>LOADING</h3> : <div>{this.props.data.protectedString}</div> }

你的减速器中的初始状态是这样的:

const initialState = {
  data: null,
  isFetching: false
};

这将默认为this.props.data.protectedString立即安装。