简体   繁体   English

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

[英]React-Redux state in the component differs from the state in the store

I'm building an application with React and Redux. 我正在用React和Redux构建一个应用程序。

I have an Account component that fetches data from the server in the componentWillMount method. 我有一个Account组件,该componentWillMountcomponentWillMount方法中的服务器中获取数据。 While the data is being fetched, the component must show the "loading" text, so I've added the "isFetching" property to the account reducer. 在提取数据时,该组件必须显示“正在加载”文本,因此我已将“ isFetching”属性添加到帐户精简器中。 This property is set to true while data is fetching from the server. 从服务器获取数据时,此属性设置为true。

The problem is, that while data is being fetched, the value of the "isFetching" property in the render method is false, while at the same time the value of store.getState().account.isFetching is true (as it must be). 问题在于,在获取数据时, render方法中“ isFetching”属性的值为false,而同时store.getState().account.isFetching值为true(因为必须)。 This causes the exception, because this.props.isFetching is false, so the code is trying to show the this.props.data.protectedString while the data is still being loaded from the server (so it is null). 这将导致异常,因为this.props.isFetching为false,因此代码试图在仍从服务器加载data同时显示this.props.data.protectedString (因此为null)。

I assume that the mapStateToProps bind some wrong value (maybe the initial state), but I cannot figure out why and how can I fix it. 我假设mapStateToProps绑定了一个错误的值(也许是初始状态),但是我无法弄清楚为什么以及如何解决它。

Here is my AccountView code: 这是我的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);

Account reducer: 帐户减少器:

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;
  }
}

Actions: 动作:

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
        });
    };
}

This is how I'm creating the store: 这是我创建商店的方式:

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);
}

And in index.js: 在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;

My code is based on this example - https://github.com/joshgeller/react-redux-jwt-auth-example 我的代码基于此示例-https://github.com/joshgeller/react-redux-jwt-auth-example

The code looks the same, but I've changed some places because of new versions of some modules. 代码看起来相同,但是由于某些模块的新版本,我更改了一些位置。

You should always ask yourself these two questions when you are fetching data with react & redux: 使用react&redux获取数据时,您应该始终问自己两个问题:

  1. Are my data still valid ? 我的数据仍然有效吗?
  2. Am I currently fetching data ? 我当前正在获取数据吗?

You have already answered the second question by using the isFetching but the first question remains and that is what causing your problem. 您已经通过使用isFetching回答了第二个问题,但是第一个问题仍然存在,这就是导致您出现问题的原因。 What you should do is to use the didInvalidate in your reducer ( https://github.com/reactjs/redux/blob/master/docs/advanced/AsyncActions.md ) 您应该做的是在化didInvalidate中使用didInvalidatehttps://github.com/reactjs/redux/blob/master/docs/advanced/AsyncActions.md

With didInvalidate you can easily check if your data are valid and invalidate them if needed by dispatching an action like INVALIDATE_ACCOUNT . 使用didInvalidate您可以通过分派INVALIDATE_ACCOUNT类的操作轻松检查数据是否有效,并在需要时使它们无效。 As you haven't fetched your data yet, your data are invalid by default. 由于您尚未提取数据,因此默认情况下您的数据无效。

(Bonus) Some examples of when you might invalidate your data: (奖励)何时可能使数据无效的一些示例:

  • The last fetch date is > X minutes 最后获取日期是> X分钟
  • You have modified some data and need to fetch this data again 您已修改了一些数据,需要再次获取此数据
  • Someone else has modified this data, you receive the invalidation action through Websockets 有人修改了此数据,您通过Websockets收到无效操作

Here is how your render should look like: 渲染结果如下所示:

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>
    );
  }
}

Here is your Account reducer with didInvalidate : 这是您使用didInvalidate的帐户减少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;
  }
}

Below your new lifecycle: 在您的新生命周期以下:

1. First render 1.首先渲染

  • Description: No fetch happened yet 描述:尚未发生
  • Reducers: { isFetching: false, didInvalidate: true, data: null } 减速器: { isFetching: false, didInvalidate: true, data: null }
  • Render: <Loading /> 渲染: <Loading />

2. componentDidMount 2. componentDidMount

  • Description: The data is invalidated && no fetching --> go fetch data 说明:数据无效&&无法获取->去获取数据

3. Function called: accountFetchData (1) 3.函数调用:accountFetchData(1)

  • Decription: Notify reducers that you are currently fetching and then fetch the data asynchronously 解密:通知减速器您当前正在获取,然后异步获取数据
  • Dispatch: { type: ACCOUNT_FETCH_DATA_REQUEST } 发货: { type: ACCOUNT_FETCH_DATA_REQUEST }

4. Account Reducer 4.帐户减少器

  • Description: Reducers are notified of the dispatch and modifies their values accordingly 说明:减速器已收到调度通知,并相应地修改了它们的值
  • Reducers: { isFetching: true, didInvalidate: false, data: null } { isFetching: true, didInvalidate: false, data: null }{ isFetching: true, didInvalidate: false, data: null }

5. Second render 5.第二次渲染

  • Description: Goes a second in the render because the Account reducer changed 说明:由于Account reducer已更改,因此在渲染中耗时一秒
  • Reducers: { isFetching: true, didInvalidate: false, data: null } { isFetching: true, didInvalidate: false, data: null }{ isFetching: true, didInvalidate: false, data: null }
  • Render: <Loading /> 渲染: <Loading />

6. Function called: accountFetchData (2) 6.调用的函数:accountFetchData(2)

  • Description: Data are finally fetched from the step 3 说明:数据最终从步骤3中获取
  • Dispatch: { type: ACCOUNT_RECEIVE_DATA, payload: { data } } 调度:{类型:ACCOUNT_RECEIVE_DATA,有效载荷:{数据}}

7. Account Reducer 7.帐户减少器

  • Description: Reducers are notified of the dispatch and modifies their values accordingly 说明:减速器已收到调度通知,并相应地修改了它们的值
  • Reducers: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } } 减速器: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }

8. Third render 8.第三渲染

  • Description: Data are fetched and ready to be displayed 说明:已获取数据并准备显示
  • Reducers: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } } 减速器: { isFetching: false, didInvalidate: false, data: { protectedString: '42: The answer to life' } }
  • Render: <div>42: The answer to life</div> 渲染: <div>42: The answer to life</div>

Hope it helps. 希望能帮助到你。


Edit: Let me answer your question in one of your comment in the other answer 编辑:让我在另一个答案中的一个评论中回答您的问题

@Icepickle I'm not sure it's a clean way to do that. @Icepickle我不确定这是一种干净的方法。 Suppose the user will go to /account URL. 假设用户将转到/ account URL。 Then to some other URL. 然后转到其他URL。 Then back to the /account. 然后回到/ account。 While the data will be loading from the server for the second time, the isFetching will be true and the "loading" text must be shown, but the "data" variable will not be null, because it will contain the data from the previous request. 虽然将第二次从服务器加载数据,但是isFetching将为true,并且必须显示“正在加载”文本,但是“ data”变量将不为null,因为它将包含来自先前请求的数据。 So, instead of "loading", the old data will be shown. 因此,将显示旧数据,而不是“加载”。

With the didInvalidate value, there is no risk of unlimited refetching as the component will know wether your data are valid or not. 使用didInvalidate值,因为组件将知道您的数据是否有效,所以没有无限重访的风险。

In the componentDidMount , the condition to refetch will be false as the values are the following { isFetching: false, didInvalidate: false } . componentDidMount ,要重新获取的条件将为false,因为其值为以下{ isFetching: false, didInvalidate: false } No refetch then. 然后不重新引用。

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

Bonus: However be careful of data caching issues with the didInvalidate . 奖励:但是,请注意didInvalidate的数据缓存问题。

People don't talk much about this issue but you will start asking this question "Starting when my data are invalid ?" 人们对此问题的讨论不多,但是您将开始问这个问题:“我的数据无效时开始吗?” (= When should I refetch ?) (=我什么时候应该提取?)

Reducers 减速器

If I may, let me refactor your reducer code for the long run. 如果可以的话,从长远来看,让我重构您的reducer代码。

Your reducers will be way more modular and easy to maintain this way. 您的异径管将更加模块化,并且易于维护。

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;
  }
}

Actions 动作

I will just add the invalidation function so it will be easier to understand which function I call in the component. 我将仅添加失效函数,以便更容易理解我在组件中调用的函数。 (Note: I did not rename your functions but you should definitely pay attention at the naming) (注意:我没有重命名您的函数,但您一定要注意命名)

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
      });
  };
}

Component 零件

You will have to invalidate your data at some point. 您将不得不在某些时候使数据无效。 I considered that your account data would not be valid anymore after 60 minutes. 我认为您的帐户数据将在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 code can be cleaner but it is clearer to read that way :) 这段代码可以更简洁,但更容易阅读:)

Isn't your ternary statement switched? 您的三元陈述不转换吗? Your render function has this: 您的渲染函数具有以下功能:

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

and your initialState in your reducer is this: 而您的化简器中的initialState是这样的:

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

That would default to this.props.data.protectedString immediately on mount. 挂载时将默认为this.props.data.protectedString

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

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