Push state updates directly to conditionally rendered React component? - javascript

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.

Related

How to communicate with child components in React?

I have a parent component VideoPlayer that conditionally renders one of many different video player providers (think videoJS, JW player, etc.) - so VideoJSPlayer, JWPlayer, etc.
The parent component monitors when a video has completed and needs to let the child component know it is time to play a new video. Each different video provider has a different JS library / API that I may be referencing. The parent component also renders shared components like a Play button, a Skip button, etc.
How can I most easily have the parent component maintain similar functionality and ensure the child components only handle the library-specific actions?
For example, if the shared Skip button is clicked, how should I call that from the parent? I'm currently using postMessage() but it doesn't feel quite right.
Here's some minimal code. Realistically there is more than one thing being maintained and shared by the Parent component.
class Player extends React.Component {
constructor(props) {
super(props)
this.state = {
props.videos,
activeIndex: 0,
activeVideo: props.videos[0]
}
}
loadNextVideo() {
// how to communicate to VideoJSPlayer and JWPlayer?
}
render() {
const playerCode = this.props.playerCode
return (
<div>
{
playerCode === 'videojs' ?
<VideoJSPlayer/> :
playerCode === 'jwplayer' ?
<JWPlayer/> : null
}
<SkipButton
activeVideo={this.state.activeVideo}
onClick={() => this.loadNextVideo()}/>
)
}
}
If I understand your question correctly, using state might be the right thing here:
class Parent extends Component{
constructor(){
super();
this.state = {
isPlaying: false,
seconds_passed : 0,
}
}
handleTimePass(current_time){
this.setState({
seconds_passed: current_time
})
}
handlePause(){
this.setState({
isPlaying: false
})
}
handleResume(){
this.setState({
isPlaying: true
})
}
render(){
return <div>
<Player handleSetTime={this.handleTimePass} isPlaying={this.state.isPlaying}/>
<OptionPanel
isPlaying={this.state.isPlaying}
handleStop={this.handlePause}
handleResume={this.handleResume}
/>
</div>
}
}
Now your player and Option components can call a prop function that changes parent state and that changed state again gets passed to other child components
const Player = props => {
return <div>
<YourPlayerLibrary setTime={props.handleSetTime} playingStatus={props.isPlaying} />
<div>
}
and
const Options = props => {
return <div>
<button onClick={props.handleStop}>stop</button>
<button onClick={props.handleResume}>resume</button>
</div>
}
I would start pointing out some poor patterns:
derived state from props (props.videos);
you for most time should not duplicate props to state;
derived state from other state (activeVideo);
activeVideo is not a necessary state. it's a computed value derived from activeIndex and props.videos. It's a redundant state, you only need to keep store the reference of current video;
And some small tips;
constructor is not necessary for some years, you can declare your state directly outside;
declaring your methods as arrow functions is a practical and cleaner way to bind this;
Given that, your loadNextVideo would update to the next index. VideoJSPlayer and JWPlayer should receive as props activeVideo and loadNextVideo that should be called in these components when the video is done playing;
class Player extends React.Component {
state = {
activeIndex = 0
}
loadNextVideo = () => {
// you might add some validation if it's the last index
if (this.props.videos.length - 1 === this.state.activeIndex) return
this.setState(({ activeIndex }) => ({ activeIndex: activeIndex + 1 }))
}
render() {
const { activeIndex } = this.state
const activeVideo = this.props.videos[activeIndex]
const playerCode = this.props.playerCode
return (
<div>
{
playerCode === 'videojs' ?
<VideoJSPlayer activeVideo={ activeVideo } loadNextVideo={ this.loadNextVideo } /> :
playerCode === 'jwplayer' ?
<JWPlayer activeVideo={ activeVideo } loadNextVideo={ this.loadNextVideo } /> : null
}
<SkipButton
activeVideo={ this.state.activeVideo }
onClick={ this.loadNextVideo }/>
</div>
)
}
}

React - a way to unify, if many components have the same piece of code in lifecycle methods

I have multiple component with similar piece code in lifecycle methods and some similarity in state variables. Is there a way to unify them, by inheriting from one parent or something like that?
constructor(props) {
super(props);
this.state = {
//state properties similar in all components, getting from redux
//state properties specific for this component
}
// same code in many components
}
componentWillMount() {
// same code in many components
// code specific for this component
}
Can I use children methods and props in parent "wrapper" ? Can I change component state from parent ?
You can create Higher Order Component (HOC) for that, basically, you just write component with your same lifecycle method which is repeating, and then in render() function, call this.props.children function with any HOC internal state arguments you want, you can pass the whole state and a setState function as well, so you can change the HOC's state inside the underlying component.
For example:
class HOC extends React.Component {
constructor(props) {
super(props);
state = {
someState: 'foo',
};
}
componentWillMount() {
console.log('i mounted!')
}
render() {
return (
<div>
{this.props.children({ state: this.state, setState: this.setState })}
</div>
)
}
}
const SomeComponent = () =>
<HOC>
{({ state, setState }) => (
<div>
<span>someState value: </span>
<input
value={state.someState}
onChange={e => setState({ someState: e.target.value})}
/>
</div>
)}
</HOC>
You can also do really cool and interesting things with it, like connecting a slice of your redux state whenever you need it:
import { connect } from 'react-redux';
const ProfileState = connect(
state => ({ profile: state.profile }),
null,
)(({
profile,
children
}) => (
<div>
{children({ profile })}
</div>
));
const ProfilePage = () => (
<div>
Your name is:
<ProfileState>
{({ profile }) => (
<span>{profile.name}</span>
)}
</ProfileState>
</div>
);
Here is the full documentation on this technique.
You could create HOCs (Higher Order Components) in that case. It can look like this:
/*
A Higher Order Component is a function,
that takes a Component as Input and returns another Component.
Every Component that gets wrapped by this HOC
will receive `exampleProp`,`handleEvent`,
plus all other props that get passed in.
*/
function WithCommonLogic(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
example: ''
}
}
componentWillMount() {
...
// Same code in many components.
}
callback = () => {
/* Enhanced components can access this callback
via a prop called `handleEvent`
and thereby alter the state of their wrapper. */
this.setState({example: 'some val'})
}
render() {
return <WrappedComponent
exampleProp={this.state.example}
handleEvent={this.callback}
{...this.props}
/>
}
}
// You use it like this:
const EnhancedComponent1 = WithCommonLogic(SomeComponent);
const EnhancedComponent2 = WithCommonLogic(SomeOtherComponent);
Now all the shared logic goes into that HOC, which then wrap all your different components you want to share it with.
See the React Docs for further reading.

Initialize state with dynamic key based on props in reactJS

How to initialize state with dynamic key based on props? The props is a data fetched from external source (async). So the props will change when the data is succesfully downloaded. Consider a component like this.
edit: I want to make the state dynamic because I want to generate a dialog (pop up) based on the item that is clicked. the DialogContainer is basically that. visible prop will make that dialog visible, while onHide prop will hide that dialog. I use react-md library.
class SomeComponent extends React.Component {
constructor() {
super();
this.state = {};
// the key and value will be dynamically generated, with a loop on the props
// something like:
for (const item of this.props.data) {
this.state[`dialog-visible-${this.props.item.id}`] = false}
}
}
show(id) {
this.setState({ [`dialog-visible-${id}`]: true });
}
hide(id) {
this.setState({ [`dialog-visible-${id}`]: false });
}
render() {
return (
<div>
{this.props.data.map((item) => {
return (
<div>
<div key={item.id} onClick={this.show(item.id)}>
<h2> Show Dialog on item-{item.id}</h2>
</div>
<DialogContainer
visible={this.state[`dialog-visible-${item.id}`]}
onHide={this.hide(item.id)}
>
<div>
<h1> A Dialog that will pop up </h1>
</div>
</DialogContainer>
</div>
);
})}
</div>
)
}
}
// the data is fetched by other component.
class OtherComponent extends React.Component {
componentDidMount() {
// fetchData come from redux container (mapDispatchToProps)
this.props.fetchData('https://someUrlToFetchJSONData/')
}
}
The data then is shared via Redux.
However, based on my understanding so far, state can be updated based on props with componentWillReceiveProps or the new getDerivedStateFromProps (not on the constructor as above). But, how to do that on either method?
The example here only explains when the state is initialized on the constructor, and call setState on either cWRP or gDSFP. But, I want the key value pair to be initialized dynamically.
Any help/hint will be greatly appreciated. Please do tell if my question is not clear enough.
import React from 'react';
import {connect} from 'react-redux';
import {yourAction} from '../your/action/path';
class YourClass extends React.Component {
state = {};
constructor(props){
super(props);
}
componentDidMount(){
this.props.yourAction()
}
render() {
const {data} = this.props; //your data state from redux is supplied as props.
return (
<div>
{!data ? '' : data.map(item => (
<div>{item}</div>
))}
</div>
)
}
}
function mapStateToProps(state) {
return{
data:state.data //state.data if that is how it is referred to in the redux. Make sure you apply the correct path of state within redux
}
}
export default connect(mapStateToProps, {yourAction})(YourClass)
If you do this, <div>{item}</div> will change as you change the data state. The idea is to just map the redux state to your class props - you don't have to map the props back to the state. The render() automatically listens to changes in props supplied by redux. However, if you do want to somehow know redux state change in events, you can add the following functions.
componentWillReceiveProps(newProps){
console.log(newProps)
}
getDerivedStateFromProps(nextProps, prevState){
console.log(nextProps);
console.log(prevState);
}

React - set state using button and pass it as props

I want to use the 'compare' button to toggle the compare state to true or false.
Next I want to pass this compare state to pivot as props.
I am literally using the same code as in the react documentation when looking at the Toggle class. https://facebook.github.io/react/docs/handling-events.html
The only thing I changed is the name isToggleOn to compare.
When looking at the console client side I get following error every time the component renders:
modules.js?hash=5bd264489058b9a37cb27e36f529f99e13f95b78:3941 Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.`
My code is following:
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = { compare: true };
this.handleClick = this.handleClick.bind(this);
}
handleClick(button) {
if (button === 'compare') {
this.setState(prevState => ({
compare: !prevState.compare,
}));
}
}
render() {
return (
<Grid>
<div className="starter-template">
<h1>This is the dashboard page.</h1>
<p className="lead">
Use this document as a way to quickly start any new project.<br />{' '}
All you get is this text and a mostly barebones HTML document.
</p>
</div>
<ButtonToolbar>
<button onClick={this.handleClick('compare')}>
{this.state.compare ? 'AGGREGATE' : 'COMPARE'}
</button>
</ButtonToolbar>
<PivotTable
ready={this.props.isReady}
data={this.props.gapData}
compare={this.state.compare}
/>
</Grid>
);
}
}
export default (DashboardContainer = createContainer(() => {
// Do all your reactive data access in this method.
// Note that this subscription will get cleaned up when your component is unmounted
const handle = Meteor.subscribe('weekly-dashboard');
return {
isReady: handle.ready(),
gapData: WeeklyDashboard.find({}).fetch(),
};
}, Dashboard));
Any advice on how to fix this?
The reason is this line
<button onClick={this.handleClick('compare')}>
This will call the handleClick function while executing render function. You can fix by:
<button onClick={() => this.handleClick('compare')}>
Or
const handleBtnClick = () => this.handleClick('compare');
...
<button onClick={this.handleBtnClick}>
...
I prefer the latter

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