componentWillReceiveProps is not called on redux props change - reactjs

My component is straight forward:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as instanceActions from '../../../store/instances/instancesActions';
class InstanceDetailsPage extends Component {
componentWillReceiveProps(nextProps) {
console.log('will receive props');
if (nextProps.id !== this.props.id){
this.updateInstanceDetails();
}
}
updateInstanceDetails = () => {
this.props.actions.loadInstance(this.props.instance);
};
render() {
return (
<h1>Instance - {this.props.instance.name}</h1>
);
}
}
function getInstanceById(instances, instanceId) {
const instance = instances.filter(instance => instance.id === instanceId);
if (instance.length) return instance[0];
return null;
}
function mapStateToProps(state, ownProps) {
const instanceId = ownProps.match.params.id;
let instance = {id: '', name: ''};
if (instanceId && state.instances.length > 0) {
instance = getInstanceById(state.instances, instanceId) || instance;
}
return {
instance,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(instanceActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InstanceDetailsPage);
My reducer which I'm pretty sure I'm not mutating the state:
import * as types from '../actionTypes';
import initialState from '../initialState';
export default function instancesReducer(state = initialState.instances, action) {
switch (action.type){
case types.LOAD_INSTANCES_SUCCESS:
return action.instances.slice(); // I probably don't event need the .slice() here, but just to be sure.
default:
return state;
}
}
I know for sure that the state change which triggers props change because I logged this.props on the render method, and the props changed couple of times!
With that, the componentWillReceiveProps wasn't called even once.
What can cause that?

there are a few reasons why componentWillReceiveProps will not be called,
if its not receiving a new props object from the redux store
additionally these methods are not called if a component is mounted. So you may see your component update, but it is really just mounting and unmounting. to fix this, look into the parent rendering this component and check to see if it is rendering the component with a different key or if there is some sort of conditional render that may return false and cause react to unmount it.

Related

Dispatched action received by reducer, but not appearing in action list and not rerendering

I have a Comment component that has a delete button. When the button is clicked, it called the action, makes the axios call, and when the call returns it dispatches the update to the reducer. The only problem is that it's not triggering the rerender of the parent component. The action, although it is updating the state, does not appear in the list of dispatched actions in the Redux DevTools. All other actions work and display in the DevTools, but for some reason this one doesn't.
My thought after reading the comment section below is that it's because I'm making a shallow copy of my object. Am I wrong to think that making a shallow copy, modifying a deeper object, and returning the shallow copy wouldn't trigger a rerender? Isn't the fact that the shallow object is a different reference enough to trigger it? I'm confident I'm doing this the same way in other places and I havenn't have a problem elsewhere. It
This is the action list in Redux DevTools after deleting the comment. I would expect that it would have "delete_comment" near the bottom somewhere, but it's not:
The data is passed from the parent components CommentList -> CommentThread -> Comment.
Is this truly dispatching?
This is a simplified component:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {
deleteComment,
} from "../../actions/comment_actions";
class Comment extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const {comment, data} = this.props;
if (!data) {
//console.log("mir_data doesnt exist");
return <div/>;
}
return (
<div key={"comment" + comment.id} id={"c" + comment.id}>
<button onClick={() => this.props.deleteComment(comment.id)}>Delete</button>
</div>
);
}
}
function mapStateToProps(state) {
return {
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
deleteComment,
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Comment);
Here's a simplified action file:
export const DELETE_COMMENT = 'delete_comment';
export function deleteComment(id, callback = null) {
return (dispatch, getState) => {
axiosInstance.post('/delete-comment/', {comment_id: id}, getState().api.axios).then(
response => {
dispatch(dispatchDeleteComment(id));
//dispatch(dispatchViewUserInfo(response.data));
if (response.status === 200)
callback();
}
);
}
}
export function dispatchDeleteComment(id) {
return {
type: DELETE_COMMENT,
payload: id
};
}
Here's the simplified reducer:
import {DELETE_COMMENT} from "../actions/comment_actions";
export default function(state = {}, action){
let newState = {...state};
switch(action.type){
case DELETE_COMMENT:
//some logic
delete newState.comments[action.payload];
return newState;
default:
return state;
}
}

How do I allow connect to rerender an object based on state change

In the below example, I would like component to rerender when list is updated. But even though connect is passed new state, it doesn't rerender the component.
I know that connect performs shallow compare, but don't know how to make it compare the values of object. I couldn't find any example of connect with the options enabled.
I have seen How does a redux connected component know when to re-render? and some more but it doesn't help either.
I have tried
const ConnectList = connect(mapStateToProps,null,null,{areStatesEqual : () => false})(List)
to just try to make it rerender for any change. That doesn't seem to be working as well.
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import thunk from 'redux-thunk'
import {connect, Provider} from 'react-redux'
function testReducer (state=null,action) {
console.log(`Reducer: Reducer received action ${action.type}. ${action.comment}`)
switch(action.type){
case 'LIST': {
return ({ ...state, list: action.list })
}
case 'OTHER': {
return ({ ...state, other: action.other })
}
default:
return state
}
}
function testAction() {
return {
type: 'LIST',
list: ['first','second'],
comment: `This will trigger both connect() and mount Component List mount because, both reducer and connect changes state after this action`
}
}
function testActionChange() {
return {
type: 'LIST',
list: ['first','second','third'],
comment: `This will trigger both connect() and mount Component List mount because, both reducer and connect changes state after this action`
}
}
function testOther() {
return {
type: 'OTHER',
other: `some other value`,
comment: `This will trigger connect(), but not mount Component List because the return from connect() doesn't change`
}
}
function inertAction() {
return {
type: 'INERT',
comment: 'This action should not trigger either connect() or mount Component List , because reducer returs the same state'
}
}
const store = createStore(testReducer, [thunk])
store.dispatch(testAction())
//Dispatch an action after 2 secs
setTimeout(store.dispatch.bind(null,testOther()),2000)
setTimeout(store.dispatch.bind(null,inertAction()),4000)
setTimeout(store.dispatch.bind(null,testActionChange()),6000)
class List extends Component {
componentDidMount(){
console.log(`Component List mounted`)
}
render(){
const {list} = this.props
return(
<div>
{list.map((element) => {
return(<Element key={element} element={element} />)
})}
</div>
)
}
}
function mapStateToProps({list}){
console.log(`connect() triggered`)
return( {
list
})
}
const ConnectList = connect(mapStateToProps)(List)
class Element extends Component {
render(){
const {element} = this.props
return(
<div>{element}</div>
)
}
}
ReactDOM.render(<Provider store={store}>
<ConnectList />
</Provider>,
document.getElementById('root')
);
Output
Added console.log in connect.
I don't recognize the syntax your using in mapStateToProps
try:
function mapStateToProps(state){
console.log(`connect() triggered`)
const list = state.list;
return { list };
}
I figured out that connect in fact calls the component. But only the render method. So I had to move my action creator calls at componentDidMount to a middleware in redux to add appropriate dispatcher when state changes.

How to trigger callback function in Redux?

React and Redux experts.
I am new to React and Redux. My question is related to trigger callback (function) invocation when a Redux state is changed. I am stuck into this implementation. In my understanding, the presenter/view is updated via the props. Let me illustrate more in the following example.
<ParentComponent>
<Child1Component/>
<Child2Component/>
</ParentComponent>
class ParentComponent extends Component {
onChild1Click() {
this.props.dispatch(setTool(Tools.CHILD1TOOL))
}
render() {
return (
<div>
<Child1Component onChild1Click={this.onChild1Click.bind(this)}/>
<Child2Component/>
</div>
)
}
}
const mapStateToProps = state => {
return {state}
}
export default connect(
mapStateToProps
)(ParentComponent)
class Child1Component extends Component {
componentDidUpdate() {
// Question: How to get the Redux state here?
}
render() {
return (
<button onClick={this.props.onPencilClick}>Pencil</button>
)
}
}
Suppose a button is present in the Child1Component and a onclick is attached to such button. In my understanding of Redux, an action should be attached to this onclick and it should be dispatched. Such state will be modified in the ParentComponent and trigger props update. Afterwards, the UI/Presenter of Child1Component will be updated via props instead of any callback of Child1Component.
Is it possible to trigger a callback in Child1Component when a state is altered? The reason I need to make such implementation is that a 3rd party library is adopted. It requires to trigger callback. Actually, the onclick can trigger the function (callback) directly. However, the state cannot be maintained.
Could any expert advise it, please? Thanks a million.
P.
As I understand, this is not directly related to redux. You can use the react life cycle methods for this purpose. In your case, I think you need the componentDidUpdate or componentWillUpdate methods.
You can read more about life cycle methods here,
https://reactjs.org/docs/react-component.html
Explanation
First, make sure that you have connected the components to the Redux store using the react-redux bindings. Then, if you have correctly defined the mapStateToProps function, your child component will update whenever the state changes. Thus, whenever the component is updated, the componentWillUpdate and componentDidUpdate methods will be called.
Example in ES6 style
First, we'll bind the full redux state to the child component. Note: Generally you would not bind the full state, but only a branch of it.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import ChildComponent from './ChildComponent';
function mapStateToProps(state) {
return {
// this will bind the redux state to the props of the child component
reduxState: state
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
// some action creators
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChildComponent);
Then we can access the redux state from the child component.
class ChildComponent extends React.Component {
componentWillMount(nextProps, nextState) {
// Do something here
console.log(this.props.reduxState)
// this.props.reduxState is accessible from anywhere in the component
}
render() {
return <div>{/*Some jsx here*/}</div>
}
}
I strongly recommend you to read about redux usage with react section from redux docs and about smart-dumb component separation
First off, thank you for the replies. I came up the solution eventually. Here it is.
// actions.js
export const SET_TOOL = 'SET_TOOL'
export const Tools = {
CHILD1TOOL: 'child1tool',
DEFAULT: 'default'
}
export function setTool(tool) {
return {
type: SET_TOOL,
tool
}
}
// reducers.js
import { combineReducers } from 'redux'
import { SET_TOOL, Tools } from './actions'
const { DEFAULT } = Tools
function currentTool(state = DEFAULT, action) {
switch(action.type) {
case SET_TOOL:
return action.tool
default:
return state
}
}
const myApp = combineReducers({
currentTool
})
export default myApp
// ParentComponent.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Tools, setTool } from './actions'
import Child1Component from './Child1Component.jsx'
class ParentComponent extends Component {
render() {
return (
<div>
<Child1Component onChild1Click={this.props.onChild1Click'}/>
</div>
)
}
}
const mapStatesToProps = state => {
return {state}
}
const mapDispatchToProps = dispatch => {
return {
onChild1Click: () => {
dispatch(setTool(Tools.CHIDL1TOOL))
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ParentComponent)
// Child1Component.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Tools } from './actions'
class Child1Component extends Component {
componentDidUpdate() {
if (this.props.state.currentTool === Tools.CHILD1TOOL) {
this.callbackHandleClick()
}
}
render() {
return <button onClick={this.props.onChild1Click}>Child 1 Button</button>
}
callbackHandleClick() {
/* callback implementation */
}
}
const mapStateToProps = state => {
return {state}
}
export default connect(
mapStateToProps
)(Child1Component)

Conditionally populate React Container From URL Parameters

I don't know exactly how to approach this problem, and my research is turning up conflicting/confusing information. I'm using react, redux, saga, and react-router.
I have basically a master/detail scenario. I present the user with a list of stories and when they click on one, it loads a detail component with the details read from the state. However, I want users to be able to deep-link to the detail page if they know the detail ID. http://localhost:3000/story/1496620859347.
I am dispatching an action triggered in the component constructor, but the state that is mapped to the component props never changes. What am I doing wrong? (I've added some of the component code, but I'm not sure it is relevant.)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './FullStory.css';
import he from 'he';
import { downloadSelectedStoryByID } from '../store/actions/selected_story';
const mapStateToProps = (state) => {
return {
story: state.selected_story
}
}
const mapDispatchToProps = dispatch => {
return {
getStoryByID(id) {
dispatch(downloadSelectedStoryByID(id));
}
}
}
class FullStory extends Component {
constructor(props) {
super(props)
if (props.story.id == undefined) {
this.props.getStoryByID(props.match.params.id);
}
}
render() {
return (
)
Reducer
import c from '../../constants';
export const selected_story = (state={}, action) => {
switch(action.type) {
case c.DOWNLOAD_STORY_BY_ID_SUCCEEDED:
return action.payload;
case c.SET_SELECTED_STORY:
return action.payload;
default:
return state;
}
}
I put in the reducer.

React / Redux wait for store to update

I have a problem that a react component is rendering before the redux store has any data.
The problem is caused by the React component being rendered to the page before the existing angular app has dispatched the data to the store.
I cannot alter the order of the rendering or anything like that.
My simple React component is
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
// *** at this point I have the store in state prop
//but editorFlow array is not yet instanced, it's undefined
const tasks = this.props.state.editorFlow[0].flow.tasks
return (
<div>
Flow editor react component in main container
</div>
);
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)
So how can I hold off the rendering until editorFlow array has elements ?
You can use Conditional Rendering.
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
// *** at this point I have the store in state prop
//but editorFlow array is not yet instanced, it's undefined
const { editorFlow } = this.props.state;
let tasks;
if (typeof editorFlow === 'object' && editorFlow.length > 0) {
tasks = editorFlow[0].flow.tasks;
}
return (
{tasks &&
<div>
Flow editor react component in main container
</div>
}
);
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)
As far as I know, you can't.
the way redux works is that it first renders everything, then actions take place with some async stuff(such as loading data), then the store gets populated, and then redux updates the components with the new state(using mapStateToProps).
the lifecycle as I understand it is this :
render the component with the initial state tree that's provided when you create the store.
Do async actions, load data, extend/modify the redux state
Redux updates your components with the new state.
I don't think mapping the entire redux state to a single prop is a good idea, the component should really take what it needs from the global state.
Adding some sane defaults to your component can ensure that a "loading" spinner is displayed until the data is fetched.
In response to Cssko (I've upped your answer) (and thedude) thanks guys a working solution is
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {addBot} from './actions';
class FlowsContainer extends React.Component {
componentDidMount() {
this.props.initStoreWithBot();
}
render() {
const { editorFlow } = this.props.state;
let tasks;
if (typeof editorFlow === 'object' && editorFlow.length > 0) {
tasks = editorFlow[0].flow.tasks;
}
if(tasks){
return (
<div>
Flow editor react component in main container
</div>
)
}
else{
return null;
}
}
}
const mapStateToProps = (state) => ({
state : state
})
const mapDispatchToProps = (dispatch) => {
return {
initStoreWithBot : () => dispatch(addBot("test 123"))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(FlowsContainer)

Resources