Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
  }
}
like image 922
gon250 Avatar asked Apr 14 '16 21:04

gon250


1 Answers

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:

  1. Either you should have used state for this component, so you can call setState
  2. 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.
like image 123
Dan Abramov Avatar answered Sep 23 '22 20:09

Dan Abramov