React Redux component not re-rendering on state change - javascript

How do you make sure that you trigger the state change and catch it in the component?
My application, has a action, which is triggered on a click in a filter component. This filter does a search and returns the new products (based on filter).
The action works and using the React Chrome extension, I can see the global state of the products changing. However I can't get the new state in my shopComponent, which will change the displayed products based on the new product state.. :(
class shopProduct extends Component {
constructor(props) {
super(props);
this.state = {...localStateItems }
}
render() {
if(this.props.products != undefined) {
console.log('not undefined: ', this.props.products);
// NOTHING IS SHOWING
}
console.log('shopProducts Render: ', this.props);
// NOTHING IS SHOWING
return (
...Some new products
)
}
}
const mapStateToProps = (state, ownProps) => ({
cart: state.cart,
products: state.products
});
shopProduct.propTypes = {
productSearch: PropTypes.func.isRequired,
products: PropTypes.object
};
export default connect(mapStateToProps, { productSearch, products })(shopProduct);
Am I mapping this to the shopProduct component correctly?

I think you misunderstand what the second parameter passed to connect should be. According to the docs it should mapDispatchToProps. You are trying to pass in your propTypes, which is not what that second param is expecting. You may want to read about what mapDispatchToProps is for.

OK. So the reason why it wasn't rendering is that we were using immutable JS.
So for anyone else with the problem. Check if your app is using that.
Just reference the object with state.get('objectName')
RIGHT:
const mapStateToProps = (state) => ({
products: state.get('products')
});
-
WRONG:
const mapStateToProps = (state, ownProps) => ({
cart: state.cart,
products: state.products
});

Related

In React / Redux, how to call the same fetch twice in componentDidMount, setting 2 state variables with results

The title is wordy, however a short / simple example will go a long ways in explaining my question. I have the following start to a component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchGames } from '../../path-to-action';
class TeamsApp extends Component {
constructor(props) {
super(props);
this.state = {
oldGames: [],
newGames: []
};
}
componentDidMount() {
this.props.dispatch(fetchGames('1617'));
this.setState({ oldGames: this.props.teamGameData });
this.props.dispatch(fetchGames('1718'));
this.setState({ newGames: this.props.teamGameData });
}
...
...
}
function mapStateToProps(reduxState) {
return {
teamGameData: reduxState.GamesReducer.sportsData
};
}
export default connect(mapStateToProps)(TeamsApp);
I would like the action / reducer that corresponds with fetchGames() and gamesReducer to be called twice when the component mounts. This action / reducer grabs some sports data, and I am trying to grab data for two separate seasons (the '1617' season and the '1718' season). The fetchGames() is built correctly to handle the season parameter.
With the current setup, the states aren't being set, and my linter is throwing an error Do not use setState in componentDidMount.
Can I pass a callback to this.props.dispatch that takes the results of the fetchGames() (the teamGameData prop), and sets the oldGames / newGames states equal to this object?
Any help with this is appreciated!
Edit: if i simply remove the this.setState()'s, then my teamGameData prop simply gets overridden with the second this.props.dispatch() call...
Edit 2: I'm not 100% sure at all if having the 2 state variables (oldGames, newGames) is the best approach. I just need to call this.props.dispatch(fetchGames('seasonid')) twice when the component loads, and have the results as two separate objects that the rest of the component can use.
Edit 3: I have the following part of my action:
export const fetchSportsDataSuccess = (sportsData, season) => ({
type: FETCH_NBA_TEAM_GAME_SUCCESS,
payload: { sportsData, season }
});
and the following case in my reducer:
case FETCH_NBA_TEAM_GAME_SUCCESS:
console.log('payload', action.payload);
return {
...state,
loading: false,
sportsData: action.payload.sportsData
};
and the console.log() looks like this now:
payload
{ sportsData: Array(2624), season: "1718" }
but i am not sure how to use the season ID to create a key in the return with this season's data....
Edit 4: found solution to edit 3 - Use a variable as an object key in reducer - thanks all for help on this, should be able to take it from here!
Copying data from the redux store to one's component state is an anti-pattern
Instead, you should modify your redux store, for example using an object to store data, so you'll be able to store datas for multiples seasons :
sportsData: {
'1617': { ... },
'1718': { ... },
}
This way you'll be able to fetch both seasons in the same time :
componentDidMount() {
const seasons = ['1718', '1617'];
const promises = seasons.map(fetchGames);
Promise.all(promises).catch(…);
}
And connect them both :
// you can use props here too
const mapStateToProps = (reduxState, props) => ({
// hardcoded like you did
oldGames: reduxState.GamesReducer.sportsData['1617'],
// or using some props value, why not
newGames: reduxState.GamesReducer.sportsData[props.newSeason],
};
Or connect the store as usual and go for the keys:
const mapStateToProps = (reduxState, props) => ({
games: reduxState.GamesReducer.sportsData,
};
…
render() {
const oldGame = this.props.games[1718];
const newGame = this.props.games[1718];
…
}
Redux is you single source of truth, always find a way to put everything you need in Redux instead of copying data in components

searching/filtering a list with react/redux

I'm currently learning react and redux and stumbled into a problem i can't really get my head around. Trying to implement the same functionality
as in this article: https://medium.com/#yaoxiao1222/implementing-search-filter-a-list-on-redux-react-bb5de8d0a3ad but even though the data request from the rest api i'm working with is successfull i can't assign the local state in my component to my redux-state, in order to be able to filter my results. Heres my component:
import React from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as fetchActions from '../../actions/fetchActions'
import Stafflist from './Stafflist'
class AboutPage extends React.Component {
constructor(props) {
super(props)
this.state = {
search: '',
currentlyDisplayed: this.props.store.posts
}
this.updateSearch = this.updateSearch.bind(this)
}
updateSearch(event) {
let newlyDisplayed = this.state.currentlyDisplayed.filter(
(post) => { 
return (
post.name.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
|| post.role.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
)}
)
console.log(newlyDisplayed)
this.setState({
search: event.target.value.substr(0, 20),
currentlyDisplayed: newlyDisplayed
})
}
render() {
return (
<div className="about-page">
<h1>About</h1>
<input type="text"
value={this.state.search}
onChange={this.updateSearch}
/>
//component for rendering my list of posts.
<Stafflist posts={this.state.currentlyDisplayed} />
</div>
)
}
}
// this is here i assign my api data to this.props.store.posts
function mapStateToProps(state, ownProps) {
return {
store: state
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(fetchActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AboutPage)
Comparing how i assign my stores state to my local component with how it works in the article, it seems to be done in the same way. Mine:
this.state = {
search: '',
currentlyDisplayed: this.props.store.posts
}
article:
this.state = {
searchTerm: '',
currentlyDisplayed: this.props.people
}
within react devtools i can see my data in as it should be in the store, but it won't work to assign it to my local state within the component in order to perform the filtering, and i don't really know how to debug this. My state in the local component just says
State
currentlyDisplayed: Array[0]
Empty array
also if i change
<Stafflist posts={this.state.currentlyDisplayed} />
to
<Stafflist posts={this.props.store.posts} />
the list renders as it should :)
Reducer:
import * as types from '../actions/actionTypes'
import initialState from './initialState'
export default function postReducer(state = initialState.posts, action) {
switch(action.type) {
case types.FETCH_POSTS_SUCCESS:
return action.posts.data.map(post => {
return {
id: post.id,
name: post.acf.name,
role: post.acf.role
}
})
default:
return state
}
}
Any ideas?
The problem with your code is that you do not handle how to get newly received props to your state. This means that when you receive the data from your api call only the props are updated while component state is not.
If you look carefully at the referenced article, in the onInputChange method they recalculate the state from the props whereas your updateState method only filters from the local state.
Setting the state in the constructor only ensures that the props are copied on the initial mount of the component. At that point in time the store only contains the initial state (initialState.posts in your reducer code). This is the price of keeping both component state and store; you must think of how to keep the two in sync after the initial load.
One solution is to update the state in componentWillReceiveProps:
componentWillReceiveProps(nextProps){
const nextFiltered = nextProps.store.posts.filter(your filtering code here);
this.setState({currentlyDisplayed: nextFiltered});
}
This will update your state whenever the component receives new props. Note react has phased out componentWillReceiveProps and you should use getDerivedStateToProps as of react 16.3. Refer here for more details.

Updating redux state doesn't call componentWillReceiveProps

I'm working on an app at the moment and although I'm facing the same issue as here, Updating Redux state does not trigger componentWillReceiveProps.
I've read through this answer and am not mutating the state and I can see the different state when I log in mapStateToProps, but my componentWillReceiveProps is not being fired.
My code is as follows:
const mapReducer = (state = {}, action) => {
switch (action.type) {
case 'SET_MARKER':
return action.selectedMarker;
default:
return state
}
}
export default mapReducer
//mapActions.js
export const setMarker = (selectedMarker) => {
//Used to set the one that the user has selected.
return {
type: 'SET_MARKER',
selectedMarker
}
}
//InformationScreen.js
const mapStateToProps = (state) => {
console.log('returning in mapStateToProps');
console.log(state.mapReducer);
//Here I see that state.mapReducer is different everytime.
return {
marker: state.mapReducer,
user: state.userReducer,
completed: state.completedReducer
}
}
class InformationScreen extends React.Component {
componentWillReceiveProps(nextProps) {
//No logs in here.
console.log('Receiving props and the marker is');
console.log(nextProps.marker);
}
render() {
const { marker } = this.props;
console.log(marker);
// Here the marker does update.
return(<Text> Hello world </Text>);
}
}
export default connect(mapStateToProps)(InformationScreen);
import app from './reducers';
let store = createStore(app);
export default class App extends React.Component {
state = {
isLoadingComplete: false,
};
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<ActionSheetProvider>
<Provider store={store}>
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
{Platform.OS === 'android' &&
<View style={styles.statusBarUnderlay} />}
<RootNavigation />
</View>
</Provider>
</ActionSheetProvider>
);
}
}
}
//index.js
import { combineReducers } from 'redux'
import mapReducer from './mapReducer'
const app = combineReducers({
mapReducer,
//other reducers
})
export default app
//Dispatching the action from
import { connect } from 'react-redux';
import { setMarker } from '../actions/mapActions';
import { Container, Header, Tab, Tabs, TabHeading, List, ListItem, Left, Thumbnail, Body, Separator, Badge, Right} from 'native-base';
import _ from 'lodash';
import GLOBALS from '../constants/Globals';
const mapStateToProps = (state) => {
return {
user: state.userReducer,
challenges: state.allChallengesReducer
}
}
class MyChallengesScreen extends React.Component {
static navigationOptions = {
title: 'My Challenges',
headerTintColor: 'rgb(255, 255, 255)',
headerStyle: { backgroundColor: 'rgba(77, 90, 139, 1)'}
};
componentDidMount() {
this.handleRefresh();
}
componentWillReceiveProps(nextProps) {
if (nextProps.user.facebookId) {
this.handleRefresh(nextProps);
}
}
constructor (props) {
super(props);
this.state = {
refreshing: false,
}
}
markerPressed = (marker) => {
//setChallenge.
marker.coordinate = {latitude : +marker.latitude, longitude: +marker.longitude};
console.log('Setting the marker');
this.props.dispatch(setMarker(marker));
this.props.navigation.navigate('InformationScreen');
}
render() {
return (
<Button onPress={() => this.markerPressed()}></Button>
)
}
}
export default connect(mapStateToProps)(MarkersScreen);
I hope someone else has seen something similar to this before. Thanks in advice for any help with this.
Edit: So unfortunately I still haven't been able to solve this yet. But I have found something pretty interesting when using the Redux debugger. componentWillReceiveProps is called when after dispatching the action I then 'skip' that action. Seems pretty strange, but at least it's something. Time to continue digging.
connect will shallow compare the output of mapStateToProps to the previous output of mapStateToProps. If there are no changes, it will not re-render the connected component, i.e. InformationScreen. As you said that you are "definitely mutating the state" the shallow compare will find no difference between the outputs of mapStateToProps.
You can override this behaviour of avoiding re-render by passing in the correct options. connect accepts options as the 4th argument, which is an object for which you will need to set pure: false.
refer to https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options
[pure] (Boolean): If true, connect() will avoid re-renders and calls to mapStateToProps, mapDispatchToProps, and mergeProps if the relevant state/props objects remain equal based on their respective equality checks. Assumes that the wrapped component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. Default value: true
Your reducer should only return a new object representing the state (not mutating the current state).
Your reducer should look like this
const mapReducer = (state = {selectedMarker: null}, action) => {
switch (action.type) {
case 'SET_MARKER':
return Object.assign({}, state, {
selectedMarker: action.selectedMarker
});
default:
return state
}
}
Object.assign mutates the first argument by adding all attributes that exist in all arguments after. Meaning it will mutate the {} by first adding the attributes in state then adding the attribute selectedMarker: action.selectedMarker. If state already has a selectedMarker then that will be overwritten in the new object.
and in your mapStateToProps
const mapStateToProps = (state) => {
return {
marker: state.mapReducer.selectedMarker,
...
}
}
Object Mutation
With the console logs showing different values after mapStateToProps problem is that you cannot visually tell if it's one mutated object or not. What is happening in connect is that new marker prop is tested for strict equality (===) against previous marker prop. Thing is that it doesn't matter how does the object look like, what properties it has etc. Only thing that is checked is if the object reference is the same
https://redux.js.org/basics/reducers#handling-actions
https://redux.js.org/basics/reducers#note-on-object.assign
The real problem is here:
markerPressed = (marker) => {
//setChallenge.
marker.coordinate = {latitude : +marker.latitude, longitude: +marker.longitude};
console.log('Setting the marker');
this.props.dispatch(setMarker(marker));
this.props.navigation.navigate('InformationScreen');
}
You are modifying the existing marker object, and dispatching an action to put that into the store. The reducer simply returns the marker object it was given.
We always emphasize that reducers need to be pure functions that update data immutably, but you actually need to create your new data immutably wherever you are creating it. If you have a reducer that just does return action.someValue, then the logic that created someValue needs to create it immutably.
So, for your case, your markerPressed() class method needs to make a copy of the marker and give it a new coordinate value.
Wow! I'm glad to say the issue has been fixed. It seems that the reason was that, since InformationScreen is in a StackNavigator, a new instance is created every time. Since the first time an object is created componentWillReceiveProps isn't called, this explains it. Additionally T
thanks to #markerikson, for pointing out that I should be recreating a marker object with each action dispatch. Thanks again, and so happy to be able to get back to work! It's probably a rookie error.
Reference: https://shripadk.github.io/react/tips/componentWillReceiveProps-not-triggered-after-mounting.html

accessing props inside react constructor

I am unable to get props inside constructor that I have implemented using redux concept.
Code for container component
class UpdateItem extends Component{
constructor(props) {
super(props);
console.log(this.props.item.itemTitle) // output: undefined
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
//If the input fields were directly within this
//this component, we could use this.refs.[FIELD].value
//Instead, we want to save the data for when the form is submitted
let state = {};
state[e.target.name] = e.target.value.trim();
this.setState(state);
}
handleSubmit(e) {
//we don't want the form to submit, so we pritem the default behavior
e.preventDefault();
let errors = {};
errors = this._validate();
if(Object.keys(errors).length != 0) {
this.setState({
errors: errors
});
return;
}
let itemData = new FormData();
itemData.append('itemTitle',this.state.itemTitle)
this.props.onSubmit(itemData);
}
componentDidMount(){
this.props.getItemByID();
}
componentWillReceiveProps(nextProps){
if (this.props.item.itemID != nextProps.item.itemID){
//Necessary to populate form when existing item is loaded directly.
this.props.getItemByID();
}
}
render(){
let {item} = this.props;
return(
<UpdateItemForm
itemTitle={this.state.itemTitle}
errors={this.state.errors}
/>
);
}
}
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
function mapStateToProps(state, ownProps){
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
function mapDispatchToProps (dispatch, ownProps) {
return {
getItemByID:()=>dispatch(loadItemByID(ownProps.params.id)),
onSubmit: (values) => dispatch(updateItem(values))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(UpdateItem);
Inside render() method am able to get the props i.e. item from the redux but not inside constructor.
And code for the actions to see if the redux implementation correct or not,
export function loadItemByID(ID){
return function(dispatch){
return itemAPI.getItemByID(ID).then(item => {
dispatch(loadItemByIDSuccess(item));
}).catch(error => {
throw(error);
});
};
}
export function loadItemByIDSuccess(item){
return {type: types.LOAD_ITEM_BY_ID_SUCCESS, item}
}
Finally my reducer looks as follows,
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
I have googled to get answers with no luck, I don't know where i made a mistake. If some one point out for me it would be a great help. Thanks in advance.
The reason you can't access the props in the constructor is that it is only called once, before the component is first mounted.
The action to load the item is called in the componentWillMount function, which occurs after the constructor is called.
It appears like you are trying to set a default value in the mapStateToProps function but aren't using it at all
function mapStateToProps(state, ownProps){
// this is never used
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
The next part I notice is that your are taking the state from redux and trying to inject it into the component's local state
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
Mixing redux state and component state is very rarely a good idea and should try to be avoided. It can lead to inconsistency and and hard to find bugs.
In this case, I don't see any reason you can't replace all the uses of this.state.itemTitle with this.props.items.itemTitle and remove it completely from the component state.
Observations
There are some peculiar things about your code that make it very difficult for me to infer the intention behind the code.
Firstly the reducer
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
You haven't shown the initialState object, but generally it represents the whole initial state for the reducer, so using initialState.item stands out to me. You may be reusing a shared initial state object for all of the reducers so I'm not too concerned about this.
What is very confusing the Object.assign call. I'm not sure it the intention is to output an object replacing item in the state, or if it is to append action.item to an array, or to have an array with a single item as the resulting state. The state = action.item part is also particularly puzzling as to it's intention in the operation.
This is further confused by the PropTypes for UpdateItem which requires item to be an array
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
But the usage in the component treats it like and object
this.state = {
// expected some kind of array lookup here |
// V---------------
itemTitle: this.props.item.itemTitle,
errors: {}
};
Update from comments
Here is a example of what I was talking about in the comments. It's a simplified version of your code (I don't have all your components. I've also modified a few things to match my personal style, but hopefully you can still see what's going on.

React-Redux connect() not updating component even when the state is changed without mutation

I have this codepen, where store.subscribe() works and connect() doesn't work. Specifically, the component doesn't get updated with the new props. I suspected state mutation as I thought that connect()'s shallow equality check might be ignoring the change. But, I'm using Immutable.js for the state change in the reducer, and I also did my own ref check in my subscribe method and it is even shallowly different for every update. I feel like something obvious must be missing here...
Component:
class App extends React.Component{
...
}
const mapStateToProps = state => ({
alerts: state.alerts
});
const mapDispatchToProps = dispatch => ({
onAlert: (type, autoHide, message) =>
dispatch({ type: 'SHOW_ALERT', payload: { message, type, autoHide } })
});
const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);
Reducer:
const alertsReducer = (alerts=Immutable.List(), { type, payload }) => {
switch (type){
case 'SHOW_ALERT':
if (!payload.message || R.trim(payload.message).length === 0){
throw new Error('Message cannot be empty.');
}
return alerts.push(payload);
default:
return alerts;
}
};
Store:
const store = createStore(combineReducers({ alerts: alertsReducer }), applyMiddleware(ReduxThunk.default));
Render:
//** THIS DOESN'T WORK
// ReactDOM.render(<Provider store={store}><ConnectedApp /></Provider>, document.getElementById('main'));
//** THIS WORKS
store.subscribe(()=>{
render();
});
const render = () => {
ReactDOM.render(<App {...store.getState()} onAlert={
(type, autoHide, message) => store.dispatch({ type: 'SHOW_ALERT', payload: { message, type, autoHide } })
}/>, document.getElementById('main'));
};
render();
Is this because the top level state object still has the same reference? I tried removing Immutable.js and made the entire state the array with the reducer returning a new array every time. That still didn't work.
Versions:
react-redux#4.4.5
redux#3.5.2
react#15.3.1
if you open the console , there will be the error
addComponentAsRefTo(...): Only a ReactOwner can have refs. You might
be adding a ref to a component that was not created inside a
component's render method, or you have multiple copies of React
loaded
to solve it you should choose between https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-with-addons.min.js
and
https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.js, because React should be added to page once.
after that you need to add Provider from react-redux. Also you need to add key props in your lists.
changed pen http://codepen.io/anon/pen/dXLQLv?editors=0010

Categories