简体   繁体   中英

React/Redux: State is updated in Redux object, but React component doesn't re-render

Tried to look through similar questions, but didn't find similar issues.

I am trying to implement sorts by name and amount in my app, this event is triggered in this component:

 import React, { Component } from 'react'; import { connect } from 'react-redux'; import { sortByExpenseName, sortByExpenseAmount } from '../actions/expensesFilters'; class ExpensesListFilter extends Component { onSortByExpenseName = () => { this.props.sortByExpenseName(); }; onSortByExpenseAmount = () => { this.props.sortByExpenseAmount(); } render() { return ( <div> <span>Expense Name</span> <button onClick={this.onSortByExpenseName}>Sort me by name</button> <button onClick={this.onSortByExpenseAmount}>Sort me by amount</button> </div> ) } } const mapDispatchToProps = (dispatch) => ({ sortByExpenseName: () => dispatch(sortByExpenseName()), sortByExpenseAmount: () => dispatch(sortByExpenseAmount()), }); export default connect(null, mapDispatchToProps)(ExpensesListFilter); 

for that I am using following selector:

 export default (expenses, { sortBy }) => { return expenses.sort((a, b) => { if (sortBy === 'name') { return a.name < b.name ? 1 : -1; } else if (sortBy === 'amount') { return parseInt(a.amount, 10) < parseInt(b.amount, 10) ? 1 : -1; } }); }; 

I run this selector in mapStateToProps function for my ExpensesList component here:

 import React from 'react'; import { connect } from 'react-redux'; import ExpensesItem from './ExpensesItem'; // my selector import sortExpenses from '../selectors/sortExpenses'; const ExpensesList = props => ( <div className="content-container"> {props.expenses && props.expenses.map((expense) => { return <ExpensesItem key={expense.id} {...expense} />; }) } </div> ); // Here I run my selector to sort expenses const mapStateToProps = (state) => { return { expenses: sortExpenses(state.expensesData.expenses, state.expensesFilters), }; }; export default connect(mapStateToProps)(ExpensesList); 

This selector updates my filter reducer, which causes my app state to update:

 import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from '../actions/types'; const INITIAL_EXPENSE_FILTER_STATE = { sortBy: 'name', }; export default (state = INITIAL_EXPENSE_FILTER_STATE, action) => { switch (action.type) { case SORT_BY_EXPENSE_NAME: return { ...state, sortBy: 'name', }; case SORT_BY_EXPENSE_AMOUNT: return { ...state, sortBy: 'amount', }; default: return state; } }; 

Sort event causes my state to update, the expenses array in my expenses reducer below is updated and sorted by selector, BUT the ExpensesList component doesn't re-render after my expenses array in state is updated. What I want my ExpensesList component to do, is to re-render with sorted expenses array and sort ExpensesItem components in list. What could be the reason why it fails? Pretty sure I am missing out something essential, but can't figure out what. My expenses reducer:

 import { FETCH_EXPENSES } from '../actions/types'; const INITIAL_STATE = {}; export default (state = INITIAL_STATE, action) => { switch (action.type) { case FETCH_EXPENSES: return { ...state, expenses: action.expenses.data, }; default: return state; } }; 

All these components are childs to this parent component:

 import React from 'react'; import ExpensesListFilter from './ExpensesListFilter'; import ExpensesList from './ExpensesList'; const MainPage = () => ( <div className="box-layout"> <div className="box-layout__box"> <ExpensesListFilter /> <ExpensesList /> </div> </div> ); export default MainPage; 

App.js file (where I run startExpenseFetch)

 import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import 'normalize.css/normalize.css'; import AppRouter, { history } from './routers/AppRouter'; import configureStore from './store/configureStore'; import LoadingPage from './components/LoadingPage'; import { startExpenseFetch } from './actions/expensesData'; import './styles/styles.scss'; const store = configureStore(); const jsx = ( <Provider store={store}> <AppRouter /> </Provider> ); let hasRendered = false; const renderApp = () => { if (!hasRendered) { ReactDOM.render(jsx, document.getElementById('app')); hasRendered = true; } }; store.dispatch(startExpenseFetch()).then(() => { renderApp(); }); ReactDOM.render(<LoadingPage />, document.getElementById('app')); 

Rest of files:

ExpenseItem Component:

 import React from 'react'; const ExpenseItem = ({ amount, name }) => ( <div> <span>{name}</span> <span>{amount}</span> </div> ); export default ExpenseItem; 

Action creators:

expensesData.js

 import axios from 'axios'; import { FETCH_EXPENSE } from './types'; // no errors here const ROOT_URL = ''; export const fetchExpenseData = expenses => ({ type: FETCH_EXPENSE, expenses, }); export const startExpenseFetch = () => { return (dispatch) => { return axios({ method: 'get', url: `${ROOT_URL}`, }) .then((response) => { dispatch(fetchExpenseData(response)); console.log(response); }) .catch((error) => { console.log(error); }); }; }; 

expensesFilters.js

 import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from './types'; export const sortByExpenseName = () => ({ type: SORT_BY_EXPENSE_NAME, }); export const sortByExpenseAmount = () => ({ type: SORT_BY_EXPENSE_AMOUNT, }); 

configureStores.js file

 import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import expensesDataReducer from '../reducers/expensesData'; import expensesFilterReducer from '../reducers/expensesFilters'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; export default () => { const store = createStore( combineReducers({ expensesData: expensesDataReducer, expensesFilters: expensesFilterReducer, }), composeEnhancers(applyMiddleware(thunk)) ); return store; }; 

AppRouter.js file

 import React from 'react'; import { Router, Route, Switch, Link, NavLink } from 'react-router-dom'; import createHistory from 'history/createBrowserHistory'; import MainPage from '../components/MainPage'; import NotFoundPage from '../components/NotFoundPage'; export const history = createHistory(); const AppRouter = () => ( <Router history={history}> <div> <Switch> <Route path="/" component={MainPage} exact={true} /> <Route component={NotFoundPage} /> </Switch> </div> </Router> ); export default AppRouter; 

Don't you have a typo on your call to your selector? :)

// Here I run my selector to sort expenses
const mapStateToProps = (state) => {
  return {
    expenses: sortExpenses(state.expensesData.expenses, state.expnsesFilters),
  };
};

state.expnsesFilters look like it should be state. expenses Filters

Which is one of the reasons you should make your sortExpenses selector grab itself the parts of the state it needs and do it's job on its own. You could test it isolation and avoid mistakes like this.

I found a reason why it happens, in my selector I was mutating my app's state. I wasn't returning a new array from it, and was changing the old one instead, that didn't trigger my vue layer to re-render. Fixed it and it works now.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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