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>
)
}
}
Related
I have a very simple component with a text field and a button:
It takes a list as input and allows the user to cycle through the list.
The component has the following code:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
This component works great, except I have not handled the case when the state changes. When the
state changes, I would like to reset the currentNameIndex to zero.
What is the best way to do this?
Options I have conciderred:
Using componentDidUpdate
This solution is ackward, because componentDidUpdate runs after render, so I need to add a clause
in the render method to "do nothing" while the component is in an invalid state, if I am not careful,
I can cause a null-pointer-exception.
I have included an implementation of this below.
Using getDerivedStateFromProps
The getDerivedStateFromProps method is static and the signature only gives you access to the
current state and next props. This is a problem because you cannot tell if the props have changed. As
a result, this forces you to copy the props into the state so that you can check if they are the same.
Making the component "fully controlled"
I don't want to do this. This component should privately own what the currently selected index is.
Making the component "fully uncontrolled with a key"
I am considering this approach, but don't like how it causes the parent to need to understand the
implementation details of the child.
Link
Misc
I have spent a great deal of time reading You Probably Don't Need Derived State
but am largely unhappy with the solutions proposed there.
I know that variations of this question have been asked multiple times, but I don't feel like any of the answers weigh the possible solutions. Some examples of duplicates:
How to reset state in a component on prop change
Update component state when props change
Updating state on props change in React Form
Appendix
Solution using componetDidUpdate (see description above)
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
Solution using getDerivedStateFromProps:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
As i said in the comments, i'm not a fan of these solutions.
Components should not care what the parent is doing or what is the current state of the parent, they should simply take in props and output some JSX, this way they are truly reusable, composable and isolated which also makes testing a lot easier.
We can make the NamesCarousel component hold the names of the carousel together with the functionality of the carousel and the current visible name and make a Name component which does only one thing, display the name that comes in through props
To reset the selectedIndex when the items are changing add a useEffect with items as a dependency, although if you just add items to the end of the array you can ignore this part
const Name = ({ name }) => <span>{name.toUpperCase()}</span>;
const NamesCarousel = ({ names }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [names])// when names changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
<Name name={names[selectedIndex]} />
<button onClick={next} disabled={selectedIndex === names.length - 1}>
Next
</button>
</div>
);
};
Now this is fine but is the NamesCarousel reusable? no, the Name component is but the Carousel is coupled with the Name component.
So what can we do to make it truly reusable and see the benefits of designing component in isolation?
We can take advantage of the render props pattern.
Lets make a generic Carousel component which will take a generic list of items and invoke the children function passing in the selected item
const Carousel = ({ items, children }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [items])// when items changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
{children(items[selectedIndex])}
<button onClick={next} disabled={selectedIndex === items.length - 1}>
Next
</button>
</div>
);
};
Now what this pattern actually gives us?
It gives us the ability to render the Carousel component like this
// items can be an array of any shape you like
// and the children of the component will be a function
// that will return the select item
<Carousel items={["Hi", "There", "Buddy"]}>
{name => <Name name={name} />} // You can render any component here
</Carousel>
Now they are both isolated and truly reusable, you can pass items as an array of images, videos, or even users.
You can take it further and give the carousel the number of items you want to display as props and invoke the child function with an array of items
return (
<div>
{children(items.slice(selectedIndex, selectedIndex + props.numOfItems))}
</div>
)
// And now you will get an array of 2 names when you render the component
<Carousel items={["Hi", "There", "Buddy"]} numOfItems={2}>
{names => names.map(name => <Name key={name} name={name} />)}
</Carousel>
Can you use a functional component? Might simplify things a bit.
import React, { useState, useEffect } from "react";
import { Button } from "#material-ui/core";
interface Props {
names: string[];
}
export const NameCarousel: React.FC<Props> = ({ names }) => {
const [currentNameIndex, setCurrentNameIndex] = useState(0);
const name = names[currentNameIndex].toUpperCase();
useEffect(() => {
setCurrentNameIndex(0);
}, names);
const handleButtonClick = () => {
setCurrentIndex((currentNameIndex + 1) % names.length);
}
return (
<div>
{name}
<Button onClick={handleButtonClick}>Next</Button>
</div>
)
};
useEffect is similar to componentDidUpdate where it will take an array of dependencies (state and prop variables) as the second argument. When those variables change, the function in the first argument is executed. Simple as that. You can do additional logic checks inside of the function body to set variables (e.g., setCurrentNameIndex).
Just be careful if you have a dependency in the second argument that gets changed inside the function, then you will have infinite rerenders.
Check out the useEffect docs, but you'll probably never want to use a class component again after getting used to hooks.
You ask what is the best option, the best option is to make it a Controlled component.
The component is too low in the hierarchy to know how to handle it's properties changing - what if the list changed but only slightly (perhaps adding a new name) - the calling component might want to keep the original position.
In all cases I can think about we are better off if the parent component can decide how the component should behave when provided a new list.
It's also likely that such a component is part of a bigger whole and needs to pass the current selection to it's parent - perhaps as part of a form.
If you are really adamant on not making it a controlled component, there are other options:
Instead of an index you can keep the entire name (or an id component) in the state - and if that name no longer exists in the names list, return the first in the list. This is a slightly different behavior than your original requirements and might be a performance issue for a really really really long list, but it's very clean.
If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.
The "canonical" way to do it with classes seems to be getDerivedStateFromProps - and yes that means keeping a reference to the name list in the state and comparing it. It can look a bit better if you write it something like this:
static getDerivedStateFromProps(props: Props, state: State = {}): Partial<State> {
if( state.names !== props.names){
return {
currentNameIndex: 0,
names: props.names
}
}
return null; // you can return null to signify no change.
}
(you should probably use state.names in the render method as well if you choose this route)
But really - controlled component is the way to go, you'll probably do it sooner or later anyway when demands change and the parent needs to know the selected item.
I've got a may confusing question because it does not fit standard-behaviour how react and the virtual dom works but i would like to know the answer anyway.
Imagine i've got a simple react-component which is called "Container".
The Container-component has a "div" inside of the render-method which contains another component called "ChildContainer". The "div" which surrounds the "ChildContainer" has the id "wrappingDiv".
Example:
render() {
<Container>
<div id="wrappingDiv">
<ChildContainer/>
</div>
</Container
}
How can i destroy the "ChildContainer"-component-instance and create a completly new one. Which mean the "ComponentWillUnmount" of the old instance is called and the "ComponentDidMount" of the new component is called.
I don't want the old component to update by changing the state or props.
I need this behaviour, because an external library from our partner-company got a libary which change the dom-items and in React i'll get a "Node not found" exception when i Update the component.
If you give the component a key, and change that key when re-rendering, the old component instance will unmount and the new one will mount:
render() {
++this.childKey;
return <Container>
<div id="wrappingDiv">
<ChildContainer key={this.childKey}/>
</div>
</Container>;
}
The child will have a new key each time, so React will assume it's part of a list and throw away the old one, creating the new one. Any state change in your component that causes it to re-render will force that unmount-and-recreated behavior on the child.
Live Example:
class Container extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
class ChildContainer extends React.Component {
render() {
return <div>The child container</div>;
}
componentDidMount() {
console.log("componentDidMount");
}
componentWillUnmount() {
console.log("componentWillUnmount");
}
}
class Example extends React.Component {
constructor(...args) {
super(...args);
this.childKey = 0;
this.state = {
something: true
};
}
componentDidMount() {
let timer = setInterval(() => {
this.setState(({something}) => ({something: !something}));
}, 1000);
setTimeout(() => {
clearInterval(timer);
timer = 0;
}, 10000);
}
render() {
++this.childKey;
return <Container>
{this.state.something}
<div id="wrappingDiv">
<ChildContainer key={this.childKey}/>
</div>
</Container>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
Having said that, there may well be a better answer to your underlying issue with the plugin. But the above addresses the question actually asked... :-)
Using hooks, first create a state variable to hold the key:
const [childKey, setChildKey] = useState(1);
Then use the useEffect hook to update the key on render:
useEffect(() => {
setChildKey(prev => prev + 1);
});
Note: you probably want something in the array parameter in useEffect to only update the key if a certain state changes
I've recently seen this type of react pattern where the state is being set in a render by using this.state:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
};
}
render() {
if (this.props.show) {
this.state.showButton = true; //setting state in render!!
}
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
This seems like an anti-pattern. Can this cause bugs? It seems to work properly though.
I would just use a component lifecycle to set the state:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
};
}
componentWillReceiveProps(nextProps) {
if(nextProps.show) {
this.setState({
showButton: true,
})
}
}
render() {
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
What is the recommended way?
render should always be pure without any side effects, so it's certainly a bad practice.
from the React docs :
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead. Keeping render() pure makes components easier to think about.
Take a look also here and here.
It is an anti-pattern. If showButton state is not always equal to show props (which is the case in the example), I would use this:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: this.props.show,
};
}
componentDidUpdate(prevProps, prevState) {
prevProps.show !== this.props.show && this.setState({showButton: this.props.show})
}
render() {
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
Edit: As of React 16.3 one should use getDerivedStateFromProps in this case.
Note that componentWillReceiveProps will be deprecated.
From the docs: getDerivedStateFromProps is invoked after a component is instantiated as well as when it receives new props. It should return an object to update state, or null to indicate that the new props do not require any state updates.
https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
It is incorrect setting state in render method. You can set state in lifecyles method. But other thing is that your component can receive same props many times, so your component will be set state many times, and renderd. To solve this problem you need to compare your new with your current props for example compare json objects:
componentWillReceiveProps(nextProps) {
if(JSON.stringify(this.props) !== JSON.stringify(nextProps) && nextProps.show) {
this.setState({
showButton: true,
})
}
}
or use PureComponent. And that garentee you that your component will not rerendered constantly.
And it will be better if you do not rerender component if state.showButton currently seted to true.
componentWillReceiveProps(nextProps) {
if(JSON.stringify(this.props) !== JSON.stringify(nextProps) && nextProps.show) {
if(!this.state.showButton) {
this.setState({
showButton: true,
})
}
}
}
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 says we should not use refs where possible and I noticed that you can't use shallow rendering testing with refs so I have tried to remove refs where possible. I have a child component like this:
class Child extends React.Component {
play = () => {
//play the media
},
pause = () => {
//pause the media
},
setMedia = (newMedia) => {
//set the new media
}
}
I then have a parent component that needs to call these methods. For the setMedia I can just use props with the componentWillReceiveProps and call setMedia when the new props come in to the child.
With the play and pause functions I cannot do this.
Ben Alpert replied to this post and said:
In general, data should be passed down the tree via props. There are a few exceptions to this (such as calling .focus() or triggering a one-time animation that doesn't really "change" the state) but any time you're exposing a method called "set", props are usually a better choice. Try to make it so that the inner input component worries about its size and appearance so that none of its ancestors do.
Which is the best way to call a child function?
play() and pause() methods can be called from refs as they do not change the state just like focus() and use props for the other functions that have arguments.
Call the child functions by passing the method name in although this just seems hacky and a lot more complex:
class Child extends React.Component {
play = () => {
//play the media
},
pause = () => {
//pause the media
},
setMedia = (newMedia) => {
//set the new media
},
_callFunctions = (functions) => {
if (!functions.length) {
return;
}
//call each new function
functions.forEach((func) => this[func]());
//Empty the functions as they have been called
this.props.updateFunctions({functions: []});
}
componentWillReceiveProps(nextProps) {
this._callFunctions(nextProps.functions);
}
}
class Parent extends React.Component {
updateFunctions = (newFunctions) => this.setState({functions: newFunctions});
differentPlayMethod = () => {
//...Do other stuff
this.updateFunctions("play");
}
render() {
return (
<Child updateFunctions={this.updateFunctions}/>
);
}
}
Do this in the child component: this.props.updateFunctions({play: this.play});
The problem with this is that we are exposing(copying) a method to another component that shouldn't really know about it...
Which is the best way to do this?
I am using method number 2 at the moment and I don't really like it.
To override child functions I have also done something similar to above. Should I just use refs instead?
Rather than call child functions, try to pass data and functions down from the parent. Alongside your component, you can export a wrapper or higher order function that provides the necessary state / functions.
let withMedia = Wrapped => {
return class extends React.Component {
state = { playing: false }
play() { ... }
render() {
return (
<Wrapped
{...this.state}
{...this.props}
play={this.play}
/>
)
}
}
}
Then in your parent component:
import { Media, withMedia } from 'your-library'
let Parent = props =>
<div>
<button onClick={props.play}>Play</button>
<Media playing={props.playing} />
</div>
export default withMedia(Parent)
Keep the state as localized as you can, but don't spread it over multiple components. If you need the information whether it is currently playing in both the parent and the child, keep the state in the parent.
This leaves you with a much cleaner state tree and props:
class Child extends React.Component {
render() {
return (
<div>
<button onClick={this.props.togglePlay}>Child: Play/Pause</button>
<p>Playing: {this.props.playing ? 'Yes' : 'No'}</p>
</div>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.togglePlay = this.togglePlay.bind(this);
this.state = {
playing: false
};
}
togglePlay() {
this.setState({
playing: !this.state.playing
});
}
render() {
return (
<div>
<button onClick={this.togglePlay}>Parent: Play/Pause</button>
<Child togglePlay={this.togglePlay} playing={this.state.playing} />
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'></div>