Updating redux state doesn't call componentWillReceiveProps - javascript

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

Related

useEffect with redux prop updates

I have a component that I can use multiple times on a page. What it does is make an external call and save a value from that external call to redux store in a key object. I only want the component to do this once so I was using the componentDidMount. Now if the same component gets used again on the page I don't want it to do the external call again. This works correctly using Classes but when I try to use function hooks this no longer works.
Let me start with showing you the Class based code.
class MyComponent extends Component {
componentDidMount() {
setTimeout(() => this.wait(), 0);
}
wait() {
const { key, map } = this.props;
if (map[key] === undefined) {
saveKey(key);
console.log('LOAD EXTERNAL ID ONLY ONCE');
externalAPI(this.externalHandler.bind(this));
}
}
externalHandler(value) {
const { key, setValue } = this.props;
setValue(key, value);
}
render() {
const { key, map children } = this.props;
return (
<>
{children}
</>
);
}
}
mapStateToProps .....
export default connect(mapStateToProps, { saveKey, setValue })(MyComponent);
Reducer.js
export default (state = {}, action = null) => {
switch (action.type) {
case SAVE_KEY: {
return {
...state,
[action.id]: 'default',
};
}
case SET_VALUE: {
const { id, value } = action;
return {
...state,
[id]: value,
};
}
default: return state;
}
};
Page.js
Calls each component like below.
import React from 'react';
const Page = () => {
return (
<>
<MyComponent key='xxxxx'>First Component</MyComponent>
<MyComponent key='xxxxx'>Second Component</MyComponent>
</>
);
};
export default Page;
The above all works. So when the first component mounts i delay a call to redux, not sure why this works but it does. Can someone tell me why using the setTimeout works??? and not using the setTimeout does not. By works I mean with the timeout, first component mounts sets key because map[key] === undefined. Second component mounts map[key] is no longer undefined. But without the timeout map[key] is always === undefined ?
It stores the passed key prop in redux. The Second component mounts and sees the same key is stored so it doesn't need to call the external API getExternalID again. If a third component mounted with a different key then it should run the external API call again and so on.
As I said the above all works except I'm not sure why I needed to do a setTimout to get it to work.
Second question turning this into a function and hooks instead of a Class. This does not work for some reason.
import React, { useEffect } from 'react';
const MyComponent = ({ children, key, map, saveKey, setValue }) => {
useEffect(() => {
setTimeout(() => delay(), 0);
}, [map[key]]);
const delay = () => {
if (map[key] === undefined) {
saveKey(key);
console.log('LOAD VARIANT ONLY ONCE');
externalAPI(externalHandler);
}
};
const externalHandler = (value) => {
setValue(key, value);
};
return (
<>
{children}
</>
);
};
export default MyComponent;
First question:
Javascript works with a single thread, so even when you use delay 0 ms, the method is called after React's render method exit. Here is an experiment that I believe explains that for that :
function log(msg){
$("#output").append("<br/>"+msg);
}
function render(){
log("In render");
log("Delayed by 0 ms without setTimeout...")
setTimeout(() =>log("Delayed by 0 ms with setTimeout..."), 0);
for(var i = 0;i<10;i++) log("Delay inside render "+i);
log("Render finish");
}
render();
https://jsfiddle.net/hqbj5xdr/
So actually all the components render once before they start checking if map[key] is undefined.
Second question:
In your example, it is not really clear where map and saveKey come from. And how map is shared between components...
Anyway, a naive solution is to directly update the map. And make sure that all component
refers to this map instance (not a copy).
if (map[key] === undefined) {
map[key]=true;
saveKey(key);
console.log('LOAD VARIANT ONLY ONCE');
externalAPI(externalHandler);
}
But that is a little bad (sharing reference). So a better design may be to use a cache. Don't pass the map, but a getter and a setter. getKey and saveKey. Underlying those methods may use a map to persist which key has been set.

React memo keeps rendering when props have not changed

I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
The problem is coming from useContext. Whenever any value changes in your context, the component will re-render regardless of whether the value you're using has changed.
The solution is to create a HOC (i.e. withMyContext()) like so;
// MyContext.jsx
// exported for when you really want to use useContext();
export const MyContext = React.createContext();
// Provides values to the consumer
export function MyContextProvider(props){
const [state, setState] = React.useState();
const [otherValue, setOtherValue] = React.useState();
return <MyContext.Provider value={{state, setState, otherValue, setOtherValue}} {...props} />
}
// HOC that provides the value to the component passed.
export function withMyContext(Component){
<MyContext.Consumer>{(value) => <Component {...value} />}</MyContext.Consumer>
}
// MyComponent.jsx
const MyComponent = ({state}) => {
// do something with state
}
// compares stringified state to determine whether to render or not. This is
// specific to this component because we only care about when state changes,
// not otherValue
const areEqual = ({state:prev}, {state:next}) =>
JSON.stringify(prev) !== JSON.stringify(next)
// wraps the context and memo and will prevent unnecessary
// re-renders when otherValue changes in MyContext.
export default React.memo(withMyContext(MyComponent), areEqual)
Passing context as props instead of using it within render allows us to isolate the changing values we actually care about using areEqual. There's no way to make this comparison during render within useContext.
I would be a huge advocate for having a selector as a second argument similar to react-redux's new hooks useSelector. This would allow us to do something like
const state = useContext(MyContext, ({state}) => state);
Who's return value would only change when state changes, not the entire context.
But I'm just a dreamer.
This is probably the biggest argument I have right now for using react-redux over hooks for simple apps.

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.

Redux isn't mapping state to props in a way I can use the object

This is my index (App component) file:
const mapStateToProps = (state, ownProps = {}) => {
console.log('this is from statetoprops: ', state); // state
console.log('from state to props, own Props: ', ownProps); // undefined
return {
myValue: state,
};
};
const AppWrapper = styled.div`
max-width: calc(768px + 16px * 2);
margin: 0 auto;
margin-top: 64px;
display: flex;
min-height: 100%;
padding: 0 16px;
flex-direction: column;
`;
class App extends React.Component {
render() {
return (
<div>
<Header />
<AppWrapper>
<Switch>
<Route exact path="/main/" component={HomePage} />
<Route path="/main/aboutme" component={AboutMe} />
<Route path="/main/models" component={Models} />
<Route path="/main/landscapes" component={Landscapes} />
</Switch>
</AppWrapper>
{console.log(`This is the component App props: ${this.props.myValue}`)}
<Footer />
</div>
);
}
}
export default connect(mapStateToProps)(App);
The console log of this.props.myValue is:
This is the component App props: Map { "route": Map { "location": Map { "pathname": "/main", "search": "", "hash": "", "state": undefined, "key": "gftdcz" } }, "global": Map { "myValue": 0 }, "language": Map { "locale": "en" } }
If I do this.props.myValue.global, I get undefined. I need to access and manipulate the myValue value.
This is the reducer:
import { fromJS } from 'immutable';
// The initial state of the App
const initialState = fromJS({
myValue: 0,
});
function appReducer(state = initialState, action) {
console.log(`The reducer is being found and here is the state: ${state}`, ' and the action: ', action);
switch (action.type) {
case 'Name':
return {
myValue: action.payload,
};
default:
return state;
}
}
export default appReducer;
And this is the global reducer file...
/**
* Combine all reducers in this file and export the combined reducers.
*/
import { fromJS } from 'immutable';
import { combineReducers } from 'redux-immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import globalReducer from 'containers/App/reducer';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
/*
* routeReducer
*
* The reducer merges route location changes into our immutable state.
* The change is necessitated by moving to react-router-redux#5
*
*/
// Initial routing state
const routeInitialState = fromJS({
location: null,
});
/**
* Merge route into the global application state
*/
function routeReducer(state = routeInitialState, action) {
switch (action.type) {
/* istanbul ignore next */
case LOCATION_CHANGE:
return state.merge({
location: action.payload,
});
default:
return state;
}
}
/**
* Creates the main reducer with the dynamically injected ones
*/
export default function createReducer(injectedReducers) {
return combineReducers({
route: routeReducer,
global: globalReducer,
language: languageProviderReducer,
...injectedReducers,
});
}
I'm using a react/redux boilerplate and I'm trying to learn redux at the same time so it's an interesting experience. I'm hoping someone can point me in the right direction here.
Selecting values from the Redux store
You're setting this.props.myValue to the entire contents of the Redux store, when it looks like you want to select one specific value.
mapStateToProps receives the entire store state, not just the chunk from one specific reducer. So you need to first access the state from the specific reducer you want. For this reason, in many examples of mapStateToProps, the first line uses destructuring to get the relevant the piece of state, like this:
const { global } = state;
Note - this gives you a variable named global which is going to be very confusing to anyone reading your code. In JS the "global context" is a commonly used term; though this probably won't break anything, it's not good for readability. See below for a suggestion about your variable names.
And from that subset of the root state, you want just the myValue property.
So your mapStateToProps should look like this:
const mapStateToProps = (rootState) => { // renamed from state to rootState - this does not change the behavior, just more accurately describes what will be passed in
const { global } = rootState;
return {
myValue: global.myValue,
};
};
This could also be more concisely written as
const mapStateToProps = (rootState) => ({ myValue: rootState.global.myValue});
With Immutable maps
Since your state is an Immutable map, as far as I know destructuring won't work. So in your mapStateToProps you'll have to do something like:
const global = rooState.get('global');
const myValue = global.get('myValue');
Variable naming
Your code uses the reducer name global for the reducer that is called appReducer inside its own code file. And that reducer isn't the root reducer, so there's nothing "global" about it. The term "global" doesn't really mean anything in the context of the redux store - although the term "root" does.
It will help reduce confusion (for others reading your code, and probably for yourself too) if you avoid using the term global for anything, and match up your reducer names in the root reducer with the name of the file the reducer is defined in (or the name it is given inside its code file). So I suggest using the name app instead of global in your combineReducers call. Even better would be to name it to something more meaningful, although I get that you are in the proof-of-concept stage here.
It is a little hard to know exactly what is going wrong with your code without running it, but a few things stand out to me:
When you console.log(this.props.myValue), you get what looks to be a JavaScript object.
You cannot key into that object, you get undefined.
My first instinct is that your action.payload is not a plain JavaScript object, but is actually a string (or potentially some other type of object).
One quick way to test this would be to console.log(this.props.myValue.constructor).

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.

Categories