How to avoid using setProps in React? - javascript

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

Related

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

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}
}

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.

Redux component not updating on state change?

Update: Apparently the bug is fixed. I never pushed a solution, so I'm still not sure what the problem/solution.
Essentially what's going on is that I have a child component that is being passed state from the main application component. I know that works fine, as I see the default value of the state showing up properly.
When the child mounts, it fires an ajax call to fetch some data, and then fires an action to update the state value accordingly (Other packages use this fetch call and it works fine as well). I can see all of this is working as expected by taking a look at the Redux chrome devtool. It shows the action being fired, and that the state has changed from the default value to the value it fetched.
The problem is that the page still shows that default value and not the new state value. So I'm wondering if there's an issue with calling that fetch request/state update and then expecting the component to properly update. Should I pass the state as a prop one level lower and have a component that only focuses on displaying that value? It's clear that everything is working as expected, the page is just not updating when the new state value is set.
Here's the code for child component that is not updating (had to modify for privacy purposes)
import { bindActionCreators, Component, connect, createElement, PropTypes } from 'somePackage';
import { getStatus } from 'somedirectory';
class ChildComponent extends Component {
constructor(props) {
super(props);
this.state = {
irrelevantState: false,
};
}
componentDidMount() {
this.fetchMyData();
}
fetchMyData() {
const {
boundNavActions,
} = this.props;
boundNavActions.getStatus();
}
render() {
const {
**stateImLookingAt**,
irrelevantString,
irrelevantString,
} = this.props;
return (
<div>
<div styleName="irrelevantString">
<div styleName="irrelevantString">
<a
href={ irrelevantString }
aria-label={ irrelevantString }
>
<div
spriteSheetType="irrelevantString"
name={ irrelevantString }
/>
//Would making this it's own component help?
<div styleName="thisDoesntUpdate">
{ **stateImLookingAt** }
</div>
//Would making this it's own component help?
</a>
</div>
</div>
</div>
);
}
}
ChildComponent.propTypes = {
boundNavActions: PropTypes.object,
cartCount: PropTypes.number,
};
const mapDispatchToProps = dispatch => ({
boundNavActions: bindActionCreators({
getStatus,
}, dispatch),
});
export default connect(null, mapDispatchToProps)(ChildComponent);
There's not a lot going on pertaining to this state in the parent but here's a snippet
import { connect, createElement, PropTypes } from 'somedirectory';
import ChildComponent from 'ChildComponentPackage';
import './app.css';
const AppContainer = (props) => {
const {
**stateImLookingAt**,
} = props;
return (
<div styleName="root">
<ChildComponent
**stateImLookingAt**={ **stateImLookingAt** }
/>
</div>
);
};
const mapStateToProps = state => ({
**stateImLookingAt**: state.moo.cow.**stateImLookingAt**,
});
AppContainer.propTypes = {
**stateImLookingAt**: PropTypes.number.isRequired,
};
export default connect(mapStateToProps)(AppContainer);

Push state updates directly to conditionally rendered React component?

I'm building a React app and have a tab section, where clicking on a tab will render a specific component.
First, my parent component:
class Interface extends Component {
constructor(props) {
super(props);
this.chooseTab = this.chooseTab.bind(this);
this.state = {
current: 'inventory',
inventory: [],
skills: [],
friends: [],
notifications: {}
};
}
chooseTab(tabID) {
this.setState({ current: tabID });
chooseComponent(tabID) {
if (tabID === 'skills') return Skills;
else if (tabID === 'inventory') return Inventory;
else if (tabID === 'friends') return FriendsList;
}
render() {
const tabID = this.state.current;
const CustomComponent = this.chooseComponent(tabID);
return (
<div className='column' id='interface'>
<div className='row' id='tabs'>
<ActiveTab
current={this.state.current}
tabID='skills'
chooseTab={this.chooseTab}
/>
<ActiveTab
current={this.state.current}
tabID='inventory'
chooseTab={this.chooseTab}
/>
<ActiveTab
current={this.state.current}
tabID='friends'
chooseTab={this.chooseTab}
/>
</div>
<TabBody>
<CustomComponent
data={this.state[tabID]}
notifications={this.state.notifications}
/>
</TabBody>
</div>
);
}
}
Which renders three ActiveTab's and one TabBody:
const ActiveTab = (props) => {
const isActive = props.tabID === props.current ? 'active' : 'inactive';
return (
<button
className={`active-tab ${isActive}`}
onClick={() => props.chooseTab(props.tabID)}
>{props.tabID}
</button>
);
};
const TabBody = (props) => {
return (
<div className='tab-body'>
{props.children}
</div>
);
};
This works fine, and it's clearly an intended way of handling this issue. However, I'd like to be able to move the notifications state object into my FriendsList component (since it's unique to friends) and also trigger a setState in it from another component even if FriendsList is not the component currently rendered by the TabBody (i.e., unmounted).
I'm currently triggering remote state changes using a globally available actions closure where a specific action and setState is defined in the ComponentWillMount() lifecycle method of the target element, and it's executed from whatever component is activating the remote state change. I've left those out of Interface for brevity.
How would you handle this? Is my only option to leave notifications in Interface, define actions there, and let React handle passing props down? Or is there a way to build my tab components and conditional rendering so I can trigger state changes from a separate component to a non-displayed component in one of the tabs, i.e move notifications and its corresponding action to FriendsList?
I've passed through a problem similar than yours weeks ago, if you are not decided to adopts some state manager like Redux, MobX or even Flux I think you should pass props down to their child's.

React/Redux simple access to Store from Component

I'm trying to figure out how to user the reducers with and inside my React-Component.
My goal is pretty easy - at least i thought so: I want to toggle a Drawer-Menu. I know I can solve this with React-Only, but I want to learn Redux.
So, I've got a Component…
import React, { Component } from 'react';
class Example extends Component {
// ???
render() {
return (
<button className="burgerbutton" onClick={this.toggleDrawer}</button>
<div className="drawerMenu isvisible" ></div>
);
}
}
export default Example;
also a Reducer
const initialState = {
buttonstate: false
};
const example = (state = initialState, action) => {
switch (action.type) {
case 'TOGGLE_BTN':
return Object.assign({}, state, {
buttonstate: !state.buttonstate
})
default:
return state
}
}
export default example
and an Action (although I don't know where to put that since it's so simple)
export const toggleDrawer = () => {
return {
type: 'TOGGLE_DRAWER'
}
}
I read a lot of tutorials and most of them want me to seperate between "Presentational Components" and "Container Components". I can't really see how these concepts apply here.
So what do I have to do to do to make this work? Am I looking at this problem from the right angle or do I need 12 "Container Components" to solve this?
I really hope this question makes sense at all and/or is not a duplicate!
In redux you have to dispatch action to update reducer state. So normally a component is connected to the redux store and communication is done through dispatch.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { toggleDrawer } from 'action file location';
class Example extends Component {
toggleDrawerHandler() {
this.props.dispatch(toggleDrawer())
}
render() {
// access button state from this.props.buttonstate
return (
<button className="burgerbutton" onClick={this.toggleDrawerHandler.bind(this)}</button>
<div className="drawerMenu isvisible" ></div>
);
}
}
export default connect((store) => {buttonstate: store.buttonstate})(Example);
First, I'm really enjoying using redux "ducks" which is basically a redux reducer bundle. You put your reducer, action constants, and action creators in one file (called a duck). Then you may have multiple ducks for different modules or pieces of state that you'd then combine with combineReducers.
While #duwalanise has the right idea, I'd rather see the second param of connect() be used to directly map the action to dispatch (and there's a good shortcut for it) instead of having to use this.props.dispatch
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { toggleDrawer } from './duck';
class Example extends Component {
render() {
const { buttonstate, togglerDrawer } = this.props;
return (
<div>
<button className="burgerbutton" onClick={toggleDrawer}</button>
<div className="drawerMenu isvisible" ></div>
</div>
);
}
}
const mapStateToProps = (state) => ({
buttonstate: state.buttonstate,
});
export default connect(mapStateToProps, { toggleDrawer })(Example);
One side note, if you have a handler method in your component, it's better to do .bind(this) inside the constructor instead of using an arrow function or .bind(this) inside the event, ie don't do this onClick={() => /* do something */ } or this onClick={this.myHandler.bind(this)} This is an interesting (and long) read on it.
To touch on the Container vs Presentational Component piece: The idea would be to put all of your logic, handlers, redux actions etc into your containers, and pass that through props to your simple (hopefully stateless/pure function) presentational components. Technically, your component the way it's written could be turned into a stateless component:
const Example = ({ buttonstate, togglerDrawer }) => (
<div>
<button className="burgerbutton" onClick={toggleDrawer}</button>
<div className="drawerMenu isvisible" ></div>
</div>
);

Categories