I am trying to render a video on a page with the following code. The name of the video is sent to the component from the previous page with match props and react router dynamic routing.
router.js
<LayoutRoute
path="/videos/:id"
render={matchprops => (
<Suspense>
<AccessControl>
<LazyVideoPage {...matchprops} />
</AccessControl>
</Suspense>
)}
/>
videoList.js snippet
history.push(`/videos/${id}`, {
name
})
Currently the video is not able to play properly as it keeps restarting on every mouse move or page scroll. Using the componentDidUpdate lifecycle method it can be seen that the page rerendering is triggered by the match property, however this property is not changing at all.
I have investigated the middleware and can't see anything that may be tracking the mouse coordinates and causing the behaviour, nor have such features been purposely implemented. This behaviour is also only observed when logged into the app, even though there are no details about user log in used in the component.
videoPage.js
const VideoDisplay = ({videoId, location:{state:{name}}}) => {
return (
<Card className="main-card mb-3">
<CardBody className="top-card-yellow">
<CardTitle><h1>{name}</h1></CardTitle>
<ReactHlsPlayer
src={`https://hello-world.com/${videoId}/${videoId}.m3u8`}
autoPlay={false}
controls={true}
width="100%"
height="auto"
/>
</CardBody>
</Card>
)
}
class VideoPage extends Component {
constructor(props) {
super(props);
this.state = {
videoId: this.props.match.params.id,
}
}
componentDidUpdate(prevProps, prevState) {
Object.entries(this.props).forEach(([key, val]) =>
prevProps[key] !== val && console.log(`Prop '${key}' changed`)
);
if (this.state) {
Object.entries(this.state).forEach(([key, val]) =>
prevState[key] !== val && console.log(`State '${key}' changed`)
);
}
}
render() {
const { videoId } = this.state;
const { location } = this.props;
return (
<Fragment>
<Row>
<Col md="12">
<VideoDisplay videoId={videoId} location={location} />
</Col>
</Row>
</Fragment>
);
}
}
export default VideoPage;
react-router-dom is not currently on the latest version, so the useParams hook is not an option but this will be updated and could be used in the next sprint.
How do I go about fixing this behaviour so that the video can be viewed without being constantly restarted? Is there something I may have overlooked?
Related
I am having trouble understanding why I cannot get images to show up in my components. I have a boolean which indicates loading, and an array that gets filled async. When I finish, I set the boolean and the component re renders. Now, I want to create a card for each item in the array and put in in a card deck (this is from react-bootstrap if that wasn't obvious). I can do this with any given boolean and array, but not with the boolean and arrays created with React.useState... Why is that and how should I go about fixing this?
I encountered this problem quite a few hours ago, and have tracked down its source to this minimal working example that still reflects what I am trying to do, but I am unsure of what to do from here.
function TestCard() {
return (
<Card>
<Card.Img src="holder.js/200x200" />
</Card>
);
}
I am trying to render the following component:
function MainComponent() {
const [boolState, setBoolState] = React.useState(false);
const [arrayState, setArrayState] = React.useState([]);
React.useEffect(() => {
setTimeout(() => {
setBoolState(true);
setArrayState([1,2,3]);
}, 2000);
});
return (
<>
{/* This works */}
{
true &&
<CardDeck>
{
[1,2,3].map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
{/* This doesn't, why? */}
{
boolState &&
<CardDeck>
{
arrayState.map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
</>
);
}
Code sandbox
I am trying to move the open state for material-ui dialog to redux to prevent it from closing when a rerender occurs, but i having trouble with the dialog when a rerender occurs. Although the state is saved in redux and the dialog does stay open whenever a rerender occurs the open state stays open but the dialog does show the open animation (fading in) which is kinda annoying.
Students.js (parent component of the modal)
const Students = ({
app: { studentsPage: { savedAddDialogOpen }},
setStudentsPageAddDialogOpen}) => {
// Create the local states
const [dialogOpen, setDialogOpen] = React.useState(savedAddDialogOpen);
const dialogOpenRef = React.useRef(savedAddDialogOpen);
// Change redux dialog open
const setReduxDialogState = () => {
setStudentsPageAddDialogOpen(dialogOpenRef.current, savedAddDialogOpen);
};
// Open add student dialog
const dialogClickOpen = () => {
setDialogOpen(true);
dialogOpenRef.current = true;
setTimeout(() => setReduxDialogState(), 300);
};
// Close add student dialog
const dialogClose = () => {
setDialogOpen(false);
dialogOpenRef.current = false;
setTimeout(() => setReduxDialogState(), 300);
};
return (
<Container>
{/* Add student modal */}
<AddStudentModal dialogOpen={dialogOpen} dialogClose={dialogClose} />
</Container>
)
}
// Set the state for this component to the global state
const mapStateToProps = (state) => ({
app: state.app,
});
AddStudentModal.js
const AddStudentModal = ({
dialogOpen, dialogClose
}) => {
return (
<Dialog
open={dialogOpen}
>
{/* Lots of stuff*/}
<DialogActions>
<Button onClick={dialogClose}>
Close dialog
</Button>
</DialogActions>
</Dialog>
)
};
I hope this should be sufficient. I tried checking if the open state is actually correct when a rerender occurs and it is correct every time but it looks like the dialog is closed at a rerender no matter what the open state is and only a few ms later actually notices that it should be opened.
Any help would be really appreciated
Edit 1: Found out it has nothing to do with the open state coming from redux, if i use open={true} it still flashes, so probably a problem with material-ui itself?
Edit 2: PrivateRoute.js
const PrivateRoute = ({
auth: { isAuthenticated, loadingAuth },
user: { loggedInUser },
component: Component,
roles,
path,
setLastPrivatePath,
...rest
}) => {
useEffect(() => {
if (path !== '/dashboard' && path !== '/profile') {
setLastPrivatePath(path);
}
// Prevent any useless errors with net line:
// eslint-disable-next-line
}, [path]);
// If we are loading the user show the preloader
if (loadingAuth) {
return <Preloader />;
}
// Return the component (depending on authentication)
return (
<Route
{...rest}
render={props =>
!isAuthenticated ? (
<Redirect to="/login" />
) : (loggedInUser && roles.some(r => loggedInUser.roles.includes(r))) ||
roles.includes('any') ? (
<Component {...props} />
) : (
<NotAuthorized />
)
}
/>
);
};
// Set the state for this component to the global state
const mapStateToProps = state => ({
auth: state.auth,
user: state.user
});
I found the problem thanks to #RyanCogswell!
For anyone having the same problem here is the cause for me and the fix:
I was passing components into the Route component like this:
<PrivateRoute
exact
path={'/dashboard/students'}
component={(props) => (
<Students {...props} selectedIndex={selectedIndexSecondary} />
)}
roles={['admin']}
/>
By doing it this way i could pass props through my privateRoute function but this would also happen if you send the component this way in a normal Route component
Solution for me is just moving selectedIndexSecondary to redux and sending the component the normal way it prevented the re-mounting.
So just doing it like this will prevent this from happening.
<PrivateRoute
exact
path={'/dashboard/students'}
component={Students}
roles={['admin']}
/>
Also this will solve the localstates in your components from resseting to the default value. So for me it fixed two problems!
I am having an issue with my application. My user component only loads UserCard when I start the application from the homepage then click users link there... if I just refresh the users URL... UserCard doesn't get loaded which means something is wrong with my this.props.users. I do see that in chrome it says: Value below was evaluated just now when I refresh but when I go through the flow it doesn't say that. Any help will be appreciated.
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
users = []
axios.get('/getall').then((res) => {
for(var d in res.data) {
users.push(new User(res.data[d]));
}
});
this.setState({ users });
}
render() {
const { users } = this.state;
return (
<Router history={history}>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path='/users' render={(props) => <Users {...props} users={users} />}/>
</Switch>
</Router>
)
}
}
PrivateRoute:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
<Component {...props} /> )} />
)
User.js
export default class Users extends Component {
render() {
console.log(this.props.users);
return (
<Row>
{this.props.users.map(u =>
<UserCard key={u.name} user={u}/>
)}
</Row>
);
}
}
export class User {
constructor(obj) {
for (var prop in obj){
this[prop] = obj[prop];
}
}
getURLName() {
return this.name.replace(/\s+/g, '-').toLowerCase();
}
}
class UserCard extends Component {
render() {
return (
<Link to={'/users/' + this.props.user.getURLName()} >
<div>
// Stuff Here
</div>
</Link>
);
}
}
As per the comments:
The issue here is how you're setting state. You should never modify state directly since this will not cause the component to rerender See the react docs
Some additional thoughts unrelated to the question:
As per the comments - use function components whenever possible, especially with hooks on the way
There is probably no need to create a User class, only to new up little user objects. Simply use plain old JS objects and calculate the link url right in the place its used:
render() {
const { user } = this.props
return <Link to={`/users/${user.name.replace(/\s+/g, '-').toLowerCase()}`} />
}
It might be a good idea to start using a linter such as eslint. I see that you're declaring users = [] without using let or const (don't use var). This is bad practice since creating variables in this way pollutes the global name space. Linters like eslint will help you catch issues like this while you're coding.
Well taking the following component:
export const OrganizationEventSelector =
inject('organizationStore', 'routingStore')(observer(class EventSelector extends Component {
constructor(props) {
super(props);
}
gotoPreviousTab = () => {
const {routingStore} = this.props;
routingStore.goBack();
};
render() {
const {classes, onEnrollEvents, match} = this.props;
console.log(match.params[0]);
const remainingPath = match.params[0] || "";
const idx = remainingPath ? 1 : 0;
return (<>
<div>
<Toolbar disableGutters={true}>
{idx > 0 &&
<div>
<IconButton onClick={this.gotoPreviousTab}>
<ChevronLeftIcon />
</IconButton>
</div>
}
<Typography >
{idx === 0 ? "Organizations" : "Details"}
</Typography>
</Toolbar>
</div>
</>);
}
}));
This component lives in a route:
<Switch>
<Route path={['/', '/home']} exact component={HomeView}/>
<Route path={['/organizations']} exact component={OrganizationEventSelector}/>
<Route path={['/organizations/*']} component={OrganizationEventSelector}/>
</Switch>
Now when going to the url SITE/organizations it correctly shows the 'Organizations' title. And if I go to the next page (button, which is in another component) the url becomes SITE/organizations/org-0 and it correctly displays the 'back icon' and "Details".
However if I then click on the IconButton the url is updated (to SITE/organizations), and adding breakpoints/console logs shows that the render function is called.
However during rendering the match.params[0] = org-0. As if the url didn't update, even worse: match.url is actually still SITE/organizations/org-0.
So why does this happen? And how do I make my components rerender after the url has updated (or let them know the new url).
EDIT:
I notice the site does work in firefox. It is "just" in chrome that it isn't working.
I have multiple layers of React components for getting an embed from a music service API, including a higher-order component that hits the API to populate the embed. My problem is that my lowest-level child component won't change state. I basically want the populated embed (lowest level component) to display an album cover, which disappears after clicking it (revealing an iframe), and whose state remains stable barring any change in props higher up (by the time this component is revealed, there should be no other state changes aside from focus higher up). Here's the code:
Parent:
return (
/*...*/
<Embed
embed={this.props.attributes.embed}
cb={updateEmbed}
/>
/*...*/
First child ( above):
render() {
const {embed, className, cb} = this.props;
const {error, errorType} = this.state;
const WithAPIEmbed = withAPI( Embed );
/*...*/
return <WithAPIEmbed
embed={embed[0]}
className={className}
cb={cb}
/>;
/*...*/
withAPI:
/*...*/
componentWillMount() {
this.setState( {fetching: true} );
}
componentDidMount() {
const {embed} = this.props;
if ( ! embed.loaded ) {
this.fetchData();
} else {
this.setState( {
fetching: false,
error: false,
} );
}
}
fetchData() {
/*... some API stuff, which calls the callback in the top level parent (cb()) setting the embed prop when the promise resolves -- this works just fine ...*/
}
render() {
const {embed, className} = this.props;
const {fetching, error, errorType} = this.state;
if ( fetching ) {
/* Return some spinner/placeholder stuff */
}
if ( error ) {
/* Return some error stuff */
}
return (
<WrappedComponent
{...this.props}
embed={embed}
/>
)
}
And finally the last child I'm interested in:
constructor() {
super( ...arguments );
this.state = {
showCover: true,
};
}
render() {
const {embed, setFocus, className} = this.props;
const {showCover} = this.state;
if ( showCover ) {
return [
<div key="cover-image" className={classnames( className )}>
<figure className='cover-art'>
<img src={embed.coverArt} alt={__( 'Embed cover image' )}/>
<i onClick={() => {
this.setState( {showCover: false,} );
}}>{icon}</i> // <-- Play icon referenced below.
</figure>
</div>,
]
}
return [
<div key="embed" className={className}>
<EmbedSandbox
html={iframeHtml}
type={embed.embedType}
onFocus={() => setFocus()}
/>
</div>,
];
}
My issue is that clicking the play icon should clear the album cover and reveal the iframe embed, but even though the click is registering, the state never changes (or does and then changes back). I believe it's because a higher-level component is mounting/unmounting and reinstantiating this component with its default state. I could move this state up the tree or use something like Flux, but I really feel I shouldn't need to do that, and that there's something fundamental I'm missing here.
The problem is that const WithAPIEmbed = withAPI( Embed ); is inside the render method. This creates a fresh WithAPIEmbed object on each render, which will be remounted, clearing any state below. Lifting it out of the class definition makes it stable and fixes the problem.