Component isn't re-rendering after state change in reducer - javascript

Let's say I have a link that, when clicked, adds a random number to an array in my state object called queue. The entire state.queue array is displayed on my page and gets updated as we click the link. I currently have a link that is supposed to be doing something similar but after my reducer alters the state (adds a new item to the array), my page isn't (re-?)rendering my component and so the page does not update.
What should I be doing to show the entirety of state.queue on my page, including dynamically updating as I click the link? Here's what I have:
Channel.js
class Channel extends Component {
constructor(props) {
super(props);
this.state = {
queue: [],
// more initializations
};
...
}
...
render() {
return (
<div>
{/*This part is never called because this.state.queue never has anything in it! */}
{this.state.queue &&
this.state.queue.length > 0 &&
<div>
<ul>
{this.state.queue.map((queuedItem, i) =>
<li key={i}>{queuedItem}</li>
)}
</ul>
</div>}
<div>
<QResults
allQueryResults={this.state.queryState}
requestHandler={queueNewRequest}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const{queue} = state
return {queue}
}
function mapDispatchToProps(dispatch) {
return ({
queueNewRequest: (newRequestData) => { dispatch({type: newRequestData}) }
})
}
export default withRouter(connect(mapStateToProps , mapDispatchToProps )(Channel))
QResults.js
export default class QResults extends Component {
render() {
const {requestHandler} = this.props
return (
<ul>
{this.props.allQueryResults.items.map((trackAlbum, i) =>
<li key={i}>
<a href='#'
onClick={
() => requestHandler(trackAlbum.name)}>
Some link
</a>
</li>
)}
</ul>
)
}
}
actions.js
export const QUEUE_NEW_REQUEST = 'QUEUE_NEW_REQUEST'
export function queueNewRequest(newRequestInfo) {
return dispatch => {
dispatch({
type: QUEUE_NEW_REQUEST,
payload: newRequestInfo
})
}
}
reducers.js
import { combineReducers } from 'redux'
function reducer1(state = {}, action) {
...
}
function reducer2(state = {}, action) {
switch (action.type) {
case QUEUE_NEW_REQUEST:
return {
...state,
/*queue: [...state.queue,action.payload],*/ //compiler complains that 'TypeError: state.queue is not iterable'
queue:[action.payload]
}
default:
return state
}
}
const rootReducer = combineReducers({
reducer1,
reducer2
})
export default rootReducer

Once you connected the state object to the props with connect you will get the returned objects in the props. See this: https://react-redux.js.org/using-react-redux/connect-mapstate#return.
So, you just need to change this.state.queue to this.props.queue in your Channel.js file.
And your component is not rendering because it's not dependent on the changed props. So, doing the above-suggested change should solve the issue.

this.state.queue refers to the state which is local to your Channel component. It is not the redux state. You initialize this.state.queue to an empty array, and then you never change it (ie, you never call this.setState).
If you want to interact with the state in redux, that's where mapStateToProps comes in. Since you've got a mapStateToProps that looks like this:
function mapStateToProps(state) {
const{queue} = state
return {queue}
}
The Channel component will get a prop named queue, and you can interact with it via this.props.queue.
So, the fix is to update Channel to interact with the prop. Most likely you'll want to delete this.state.queue, as it doesn't seem to serve a purpose and is causing confusion due to its name.
{this.props.queue &&
this.props.queue.length > 0 &&
<div>
<ul>
{this.state.queue.map((queuedItem, i) =>
<li key={i}>{queuedItem}</li>
)}
</ul>
</div>
}
Additionally, the way you've set up the root reducer has your redux state looking something like this:
{
reducer1: {}, // not sure what this contains, since reducer 1 was omitted
reducer2: {
queue: [],
}
}
So if that's really what you want your redux state to look like, your map state to props will need to be updated to this:
function mapStateToProps(state) {
const {queue} = state.reducer2;
return {queue};
}

In addition to all the responses about changing this.state.queue to this.props.queue, I needed to do a few things. Initialize queue in my reducer2:
function reducer2(state = {queue:[]}, action) {
switch (action.type) {
case QUEUE_NEW_REQUEST:
return {
...state,
queue: [...state.queue,action.payload],
}
default:
return state
}
}
Change mapStateToProps in Channel.js:
function mapStateToProps(state) {
const{queue} = state.reducer2
return {queue}
}

Related

Prevent component from re-rendering when property not used in render function React Redux

I am struggling with a component that re-renders.
I'm using Redux to manage my states. In a component, I'm using a property (isPlaying: bool) from my state with mapStateToProps in some methods of my component (class) except for the render function, and I use some action creators to dispatch a change for isPlaying. By the way, I am using isPlaying in another child component(Music Player) using connect(react-redux).
This is what I'm expecting: When isPlaying change, the parent component doesn't re-renders, and the Music Bar re-render and I keep using isPlaying in my parent component's methods.
What's the problem:
The parent component re-renders even if I'm not using isPlaying in the render method.
Sorry if I did confuse you while reading I am not an English native speaker.
Thanks in advance.
Edit: Add a simplified version of the code.
My Initial state:
export default {
// Some Irrelevant Properties
...
playlist: {
isPlaying: false,
playingTrack: null,
nextTrack: null,
prevTrack: null,
views: null,
totalListens: null,
tracks: []
}
};
Here's my parent component. (App.js)
import ...
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
widgets: [],
links: [],
clickedTrack: null
}
this.musicBarRef = React.createRef()
this._isMounted = false;
// Binding this to the methods used in this Component
...
}
// All my methods here
// Example of one function
playTrack() {
let {isPlaying} = this.props
if (isPlaying) {
// Pause
} else {
// Play
}
}
render() {
<>
// Some irrelevant components (don't use isPlaying)
<TracksPlaylist tracks={this.props.tracks} and some the methods created above />
<MusicBar ref={this.musicBarRef} and some methods created above />
// Example of one irrelevant component that re-renders
<Footer />
</>
}
}
const mapStateToProps = (state) => {
return {
isPlaying: state.playlist.isPlaying,
selectedTrack: state.playlist.playingTrack,
nextTrack: state.playlist.nextTrack,
prevTrack: state.playlist.prevTrack,
tracks: state.playlist.tracks,
}
}
const mapDispatchToProps = {
setPlaying: startPlaying, // Set isPlaying: true
setNotPlaying: stopPlaying, // Set isPlaying: false
// Some Irrelevent Action Creators
...
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
Here's my reducer
import * as types from "../actions/actionTypes";
import initialState from "./initialState";
export default function musicPlayerReducer(state = initialState.playlist, action) {
switch (action.type) {
// Some irrelevant cases
...
case types.STOP_PLAYING:
return {...state, isPlaying: false}
case types.START_PLAYING:
return {...state, isPlaying: true}
case types.NEXT_TRACK_FOUND:
return {...state, nextTrack: action.track}
case types.PREV_TRACK_FOUND:
return {...state, prevTrack: action.track}
case types.CURRENT_TRACK_FOUND:
return {...state, playingTrack: action.track}
default:
return state
}
}
TracksPlaylist mentioned in App.js
import ...
class TracksPlaylist extends React.Component {
constructor(props) {
super(props)
}
render() {
let {tracks, onPlayClick ... and other methods from App.js} = this.props
return (
<div className="songs">
{
Object.values(tracks).length > 0 ?
Object.values(tracks).map((item, index) => {
return <Track onPlayClick={onPlayClick} key={index} item={item} />
}) :
''
}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
tracks: state.playlist.tracks,
}
}
const mapDispatchToProps = {
setPlaying: startPlaying,
setNotPlaying: stopPlaying,
// Some irrelevant functions
...
}
export default connect(mapStateToProps, mapDispatchToProps)(Tracks)
the Track component
import ...
class Track extends React.Component {
constructor(props) {
super(props)
}
hasInteraction(interactionType, trackId) {
return false
}
render() {
let {onPlayClick, item} = this.props
return (
<div key={item.track_id} className="track" id={item.track_id} data-category={item.category_name}>
// A lot of irrelevant JSX :)
<TrackAction onPlayClick={onPlayClick} item={item} />
</div>
</div>
)
}
}
export default Track
Here's the TrackAction (it uses the isPlaying):
import ...
function TrackAction({item, isPlaying, playingTrack, onPlayClick}) {
return (
<div className="status action play-track" onClick={onPlayClick}>
<i id={item.track_id} className={isPlaying && playingTrack.track_id === item.track_id ? 'fas fa-pause' : 'fas fa-play'} />
</div>
)
}
const mapStateToProps = (state) => {
return {
isPlaying: state.playlist.isPlaying,
playingTrack: state.playlist.playingTrack
}
}
export default connect(mapStateToProps)(TrackAction)
I did use TrackAction to make just this component re-render because isPlaying changing and it is registered to this component and used in its render().
Thanks again.
Although you are not using isPlaying inside render method, you are still subscribing to the change due to mapStateToProps binding.
Whenever the State || Props changes, React will just re-render the new updated state || props with just shallow comparison.
And this is the reason why your parent is getting re-rendered.
One potential solution is to override shouldcomponentupdate
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Use shouldComponentUpdate() to let React know if a component’s output
is not affected by the current change in state or props. The default
behavior is to re-render on every state change.
Defaults to true. If you override and returns false, then UNSAFE_componentWillUpdate(), render(), and componentDidUpdate() will
not be invoked.
Sample code :
shouldComponentUpdate(nextProps, nextState) {
return <enter_your_condition_to_be_true>; // else false
}

Updating React/Redux State from Window function

I have a scenario where I'm trying to update a React/Redux state from a function that's placed on the Window. The function on the window is unable to access the function that's in the React component. Any idea how to bind that function in this kind of setup? This snippet just has a console log where the Redux call would go.
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = function(a) {
this.updateRedux(a)
}
}
render() {
return (
<Stuff />
)
}
}
this is not accessible inside your function, you need to bind it.
Try with:
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = function(a) {
this.updateRedux(a)
}.bind(this)
}
render() {
return (
<Stuff />
)
}
}
if you meant that you want to update Redux state with some action (this is the only way to update Redux state by design), then you need to make this action and its functions available to your Component with connect(mapStateToProps, mapDispatchToProps)(Component)
One of the comments above about converting the windowFunction to an arrow function resolved the issue. Thanks!
class MyComponent extends Component {
updateRedux = a => {
console.log(a)
}
componentDidMount() {
window.windowFunction = a => {
this.updateRedux(a)
}.bind(this)
}
render() {
return (
<Stuff />
)
}
}
What you could do is separate the concerns using a presenter and a connected
component, using react-redux. I am assuming you know of this library, comment
if you need more details.
// Simple "presenter", the getComponentData is used to get the data for the
// redux store.
class MyComponentPresenter extends Component {
// returns data for redux
getComponentData () {}
componentDidMount() {
this.props.updateRedux(this); // update Redux
}
render() {
return (
<Stuff />
)
}
}
// This component has the exact same interface, but comes with a updateRedux
// props which automatically dispatches an action
export const MyComponent = connect(null, {
updateRedux(componentInstance) {
return {
type: "updateRedux"
};
}
});
// in the reducer
//
function reducer (state, action) {
switch (action.type) {
case "updateRedux":
return ...
}
}
No more need for globally available function (which in your example is redefined for each instance of MyComponents which is probably not what you want).

React/Redux: Can't map over state [object Object]

Trying to orient through the dark depths of Redux-React-API jungle - managed to fetch data from API and console.log it - but neither me nor my Google skills have managed to find out why it doesn't render.
React Components
Parent Component:
class Instagram extends Component {
componentWillMount(){
this.props.fetchInfo();
}
render() {
return (
<div className="container">
<div className="wrapper">
<InstagramPost />
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ fetchInfo }, dispatch);
}
export default connect(null, mapDispatchToProps)(Instagram);
Child Component:
class InstagramPost extends Component {
render() {
console.log(this.props.info);
this.props.info.map((p,i) => {
console.log("PROPS ID: " + p.id);
})
return (
<div>
<h1>POSTS</h1>
<ul className="uls">
{
this.props.info.map((inf, i) =>
<li key={i}>{inf.id}</li>
)
}
</ul>
</div>
)
}
}
const mapStateToProps = ({ info }) => {
return { info }
}
export default connect(mapStateToProps)(InstagramPost);
Redux Action method:
const ROOT_URL = 'https://jsonplaceholder.typicode.com/posts';
export const fetchInfo = () => {
const request = axios.get(ROOT_URL);
return {
type: types.FETCH_INFO,
payload: request
};
}
Redux Reducer method:
export default function(state = [], action) {
switch (action.type) {
case FETCH_INFO:
return action.payload.data;
default:
return state;
}
}
The JSON file looks like this:
In the console - it works and I get my Objects:
The state is also updated:
But when I map over this.props.info, trying to render this.props.info.id, nothing is rendered on the page.. Incredibly thankful for any input!
Looks like your props aren't set on the initial render. I'm guessing your API call hasn't finished.
Try checking the the variable is set or is an array first:
Something like this:
class InstagramPost extends Component {
render() {
if(!this.props.info) return null
return (
<div>
<h1>POSTS</h1>
<ul className="uls">
{
this.props.info.map((inf, i) => {
return <li key={i}>{inf.id}</li>
})
}
</ul>
</div>
)
}
}
const mapStateToProps = ({ info }) => {
return { info }
}
export default connect(mapStateToProps)(InstagramPost);
Or you may want to check the length this.props.info.length > 0.
There were two problems. As Mayank Shukla pointed out, nothing was returned from the map callback because of the block braces ({}) without a return statement.
The other problem was in the reducer. As the redux state for info is an array of users, you need to replace the old state on FETCH_INFO rather than add the fetched array to the beginning of it. Otherwise, you're maintaining an array of arrays of users, which will grow by one on each fetch.
Note that you don't need any checks on this.props.info, as it will be initialized to [] by your reducer and [].map(f) == [].
For redux debugging I can very much recommend installing the Redux DevTools extension, as it will allow you to inspect all updates to the store. It needs a little setup per project, but that's well worth it.
Oh, and in the future, you might want to refrain from updating your question with suggestions from the comments/answers, as the question will no longer make sense to other visitors :-)

Component calling action creator before data from reducer can be sent

I have a a react component which is supposed to render a list of users from an API. I am calling my action creator in my lifecycle hook method like so:
componentWillMount() {
this.props.fetchUsers();
}
I have checked and data is being returned and dispatched to the reducers in the correct format. But when I try to render these users in my component, nothing is being shown on the page:
renderUser(user) {
return (
<div className="card card-block">
<h4 className="card-title">{user.name}</h4>
<p className="card-text">{user.company.name}</p>
<a className="btn btn-primary" href={user.website}>
Website
</a>
</div>
);
}
render() {
return (
<div className="user-list">{this.props.users.map(user => this.renderUser)}</div>
);
}
I get no errors or warnings, and my component is being rendered on the page after inspecting it, but is empty of content. When I try to do a few console logs, it seems that the componentWillMount() is being called before the reducers, and I don't know why. This means that my array of users after calling the fetchUsers() action creator is empty.
EDIT: Mapping the state and action creators to props
function mapStateToProps(state) {
return { users: state.users };
}
export default connect(mapStateToProps, actions)(UserList);
I import all my actions at the top of the component. My reducer adds the payload (array) from the action creator to the app state.
export default function(state = [], action) {
switch (action.type) {
case FETCH_USERS:
return [...state, action.payload.data];
}
return state;
}
My action creator simply makes a get request and sends it off to the reducers:
export function fetchUsers() {
const request = axios.get("https://jsonplaceholder.typicode.com/users");
return {
type: FETCH_USERS,
payload: request
};
}
EDIT 2:
Example of JSON data received:
You are not passing user data to the function
<div className="user-list">{this.props.users.map(user => this.renderUser(user))}</div>
You can try to insert a boolean like
loaded: false,
to your initialState in your reducer and put them in your action
case USERS_LOAD :
loaded: true,
users: action.users,
... users,
Then You can insert something liket that in your component
const Users = ({ loaded, users }) => {
if (!loaded) {
return <div>Is loading ...</div>
}
return (
<p>Voici les utilisateurs :</p>
{users.map(user => {
<li>{user}</li>}
);
};
Users.propTypes = {
users: PropTypes.object.isRequired,
loaded: PropTypes.bool.isRequired,
};
to give a start to call your datas. I hope it can help you ;-)
If u look at the lifecycle the first one is componentwillmount().
It is called before render.
refer

How to avoid using setProps in React?

I'm working on application using react & redux and I need setProps but it's deprecated.
Take a look the error below:
Warning: setProps(...) and replaceProps(...) are deprecated. Instead,
call render again at the top level.
Uncaught Invariant Violation: setProps(...): You called setProps on
a component with a parent. This is an anti-pattern since props will
get reactively updated when rendered. Instead, change the owner's
render method to pass the correct value as props to the component
where it is created.
So how can I set the props? basically is to manage some tabs, take a look below my code:
export default React.createClass({
render: function() {
return (
<div className="demo-tabs">
<Tabs activeTab={this.props.activeTab}
onChange={(tabId) => this.setProps({ activeTab: tabId })} ripple>
<Tab>Stack</Tab>
<Tab>GitHub</Tab>
<Tab>Twitter</Tab>
</Tabs>
<section>
<div className="content">Content for the tab: {this.props.activeTab}</div>
</section>
</div>
);
}
});
Container
import React from 'react';
import TimeLine from './timeline';
import { connect } from 'react-redux';
import { getStackoverflow } from 'api/timeline';
const TimeLineContainer = React.createClass({
componentWillMount: function() {
getStackoverflow();
},
render: function() {
return (
<TimeLine {...this.props} />
)
}
});
const stateToProps = function(state) {
return {
timeline: state.timelineReducer.timeline
}
}
const dispatchToProps = function(dispatch) {
return {
onClick: () => {console.log('timeline was clicked')}
}
}
export default connect(stateToProps, dispatchToProps)(TimeLineContainer)
Reducer
var timelineInitialState = {
github: [],
gist: [],
stackoverflow: [],
twitter: [],
activeTab: 2
}
export default function(state = timelineInitialState, action) {
switch (action.type) {
case 'GET_GITHUB':
//TODO: implement.
break;
case 'GET_GIST':
//TODO: implement.
break;
case 'GET_STACKOVERFLOW':
var stackoverflowState = Object.assign({}, state)
stackoverflowState.stackoverflow = action.stackoverflow;
return stackoverflowState;
break;
case 'GET_TWITTER':
//TODO: implement.
break;
default:
return state;
}
}
It looks like you dived into using Redux without first getting a firm grip of React. I wouldn’t recommend doing this because you appear to be somewhat confused now.
Thinking in React is a great guide and I recommend you to go through it first and get comfortable with the idea of state ownership in React, before using Redux.
In React, things that change over time (“state”) are always “owned” by some component. Sometimes it’s the same component that uses this state for rendering. Sometimes many components need to be synchronized so the state gets “hoisted” to some parent component that can manage them all, and pass that information via props. Whenever state changes, they all get updated.
The important part here is that props are meant to be received by parent. If a component wants to change its own props, it is a symptom of a problem:
Either you should have used state for this component, so you can call setState
Or your component needs to access a callback prop like onChange so it can “ask” the value to be changed
In the first case, your code would look like this, and it would be valid React:
export default React.createClass({
getInitialState: function() {
return { activeTab: 0 }
},
render: function() {
return (
<div className="demo-tabs">
<Tabs activeTab={this.state.activeTab}
onChange={(tabId) => this.setState({ activeTab: tabId })} ripple>
<Tab>Stack</Tab>
<Tab>GitHub</Tab>
<Tab>Twitter</Tab>
</Tabs>
<section>
<div className="content">Content for the tab: {this.state.activeTab}</div>
</section>
</div>
);
}
});
However, you can continue the pattern you are already using with <Tabs> component inside, and hoist the state even higher. Then your component would need to accept an onChange prop which it would pass down to <Tabs>. Now it doesn’t know how the state is updated, and doesn’t hold the state:
export default React.createClass({
render: function() {
return (
<div className="demo-tabs">
<Tabs activeTab={this.props.activeTab}
onChange={this.props.onChange} ripple>
<Tab>Stack</Tab>
<Tab>GitHub</Tab>
<Tab>Twitter</Tab>
</Tabs>
<section>
<div className="content">Content for the tab: {this.props.activeTab}</div>
</section>
</div>
);
}
});
Neither approach is better or worse, they are used for different purposes. The first one is more convenient when the state is irrelevant to the rest of the app, the second one is convenient when other distant components happen to need it too. Make sure you understand the tradeoffs of both approaches.
Even with the second approach, something’s got to hold the state. It could be another component, or (and this is where Redux comes in) it could be a separate data storage like a Redux store. In this case, rather than supply onChange from a parent component, you would use connect() to bind an onChange prop it injects to dispatching a Redux action:
const mapStateToProps = (state) => {
return {
activeTabId: state.activeTabId
}
}
const mapDispatchToProps = (dispatch) => {
return {
onChange: (tabId) => dispatch({ type: 'SET_ACTIVE_TAB', tabId })
}
}
And in your reducers, you can handle this action:
const activeTabId = (state = 0, action) => {
switch (action.type) {
case 'SET_ACTIVE_TAB':
return action.tabId
default:
return state
}
}
const reducer = combineReducers({
activeTabId,
// other reducers
})
const store = createStore(reducer)
In neither case we need setProps. You can either:
let the component own the state and use setState,
let it accept the activeTabId and onChange props and manage them from a component higher in the tree that would use setState, or
you can move the state handling completely out of the components into something like Redux, but your components would still accept activeTabId and onChange as props.
Provide onTabChange callback to Timeline component from Container.
Container:
const dispatchToProps = function(dispatch) {
return {
onClick: () => {console.log('timeline was clicked')},
onTabChange: (tabId) => { setActiveTabAction(tabId) }
}
}
const stateToProps = function(state) {
return {
timeline: state.timelineReducer.timeline,
activeTab: state.timelineReducer.activeTab
}
}
Timeline component:
export default React.createClass({
render: function() {
return (
<div className="demo-tabs">
<Tabs activeTab={this.props.activeTab}
onChange={this.props.onTabChange} ripple>
<Tab>Stack</Tab>
<Tab>GitHub</Tab>
<Tab>Twitter</Tab>
</Tabs>
<section>
<div className="content">Content for the tab: {this.props.activeTab}</div>
</section>
</div>
);
}
});
Presentational components (like your Timeline component) usually shouldn't manage application state. All data and callbacks they must receive by props.
I suggest you to read article about Presentational and Container components:
https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.sijqpzk93

Categories