Check Whether Another Component Has Rendered Before calling JS - javascript

First watch this, so you can see the behavior going on.
Timing Issue (JS in one component relies on another component to exist first)
I need to be able to somehow check that another component exists before I apply this JS in this component's ComponentDidMount
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
const el = ReactDOM.findDOMNode(this);
console.log("table of contents mounted");
if(document.getElementById('interview-heading') && el) {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
}
},
it does hit my if statement and does hit the Sticky() function but I still think I have problems when I refresh the page whereas this JS isn't working on the interview-heading component for some reason.
Note the id="interview-heading" below.
const InterviewContent = Component({
componentDidMount() {
console.log("InterviewContent mounted");
},
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
const InterviewContentMain = Component({
componentDidMount() {
console.log("InterviewContentMain mounted");
},
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents company={company}/>
</div>
<div className="all-80">
<InterviewContainer company={company}/>
</div>
</div>
</div>
)
}
})
export default InterviewContent;
I realize TableOfContents is being rendered before InterviewContent because it's a child of TableOfContents and I believe in React children are rendered before their parents (inside-out)?

I think you need to rethink your component structure. I don't know your entire setup, but it looks like you should probably have a shared parent component pass the message from TableOfContents to InterviewContent:
const InterviewContentMain = Component({
getInitialState() {
return {
inkEnabled: false
}
},
componentDidMount() {
console.log("InterviewContentMain mounted");
},
enableInk() {
this.setState({ inkEnabled: true });
}
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents inkEnabled={this.state.inkEnabled} company={company}/>
</div>
<div className="all-80">
<InterviewContainer enableInk={this.enableInk} company={company}/>
</div>
</div>
</div>
)
}
})
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
console.log("table of contents mounted");
this.props.enableInk();
},
...
const InterviewContent = Component({
enableInk() {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
},
// willReceiveProps isn't called on first mount, inkEnabled could be true so
componentDidMount() {
if (this.props.inkEnabled) {
this.enableInk();
}
},
componentWillReceiveProps(nextProps) {
if (this.props.inkEnabled === false && nextProps.inkEnabled === true) {
this.enableInk();
}
}
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
Then have componentDidMount trigger this.props.enableInk().
Or better yet, why not just put the Ink.UI.Sticky call in componentDidMount of InterviewContent?

Related

React - Re render

Im building a simple spotify app using react for the first time. Currenty I am able to render the current track being played by a user with it automatically appearing on the page. If I change tracks quickly(within a few seconds) it renders the new track details on to the page. However if I wait a couple of seconds, it stops rendering. Any reason why?
below is an example of my code
return (
<div className="App">
<a href='http://localhost:8888'>
<button>Login But With Spotify</button>
</a>
{this.getNowPlaying()}
<div> Now Playing: { this.state.nowPlaying.name} </div>
<div> By: { this.state.nowPlaying.artist} </div>
<div> Id: { this.state.nowPlaying.id} </div>
<div>
<img src={ this.state.nowPlaying.image} style={{ width: 100}}/>
</div>
</div>
)
}
Any help would be great :)
It is because you invoked the API call method "getNowPlaying" inside render method.
Each render cycle of react will call the render method, so it may be called many times.
Remove the {this.getNowPlaying()} from render and create a method "componentDidMount" and place it there. (see the code below)
The "componentDidMount" method is a react's component lifecycle method called after the component successfully mounted (initialized).
Read more in react's class component lifecycle methods docs
import React, { Component } from 'react';
import './App.css';
import Spotify from 'spotify-web-api-js';
const spotifyWebApi = new Spotify();
class App extends Component {
constructor(){
super();
const params = this.getHashParams();
this.state ={
loggedIn: params.access_token ? true : false,
nowPlaying: {
name: 'Not Checked',
image: ''
}
}
if (params.access_token){
spotifyWebApi.setAccessToken(params.access_token)
}
}
componentDidMount() {
this.getNowPlaying()
}
getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ( e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
getNowPlaying(){
spotifyWebApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response.item.name,
image: response.item.album.images[1].url,
artist: response.item.artists[0].name,
id: response.item.id
}
})
}
)
}
render() {
return (
<div className="App">
<a href='http://localhost:8888'>
<button>Login But With Spotify</button>
</a>
<div> Now Playing: { this.state.nowPlaying.name} </div>
<div> By: { this.state.nowPlaying.artist} </div>
<div> Id: { this.state.nowPlaying.id} </div>
<div>
<img src={ this.state.nowPlaying.image} style={{ width: 100}}/>
</div>
</div>
)
}
}
Ok, this is some basic stuff.
Basically in your code, you get the new stuff on every rerender. Here there is the problem, the rerender happens only when a state, the parent or (not always) a prop changes. This can lead to some issues.
You see it working, because you change song before the This.setState is fired, so when setting the state, it triggers the component to rerender, which calls the function again. At this point, if the response has changed (basicly if you change the song), the state gets updated and this step is repeated, else, if the response is the same (didn't change the song in the meantime), the state doesn't change (leading the component not to rerender --> not refetching the data);
Here you can find the solution(s). One is class component, one is hooks (i've tested only the hooks' one). I personally would recommend the second one, because it's less code, more flexible and easier to understand!
I hope i could help you!
Class Component
import React, { Component } from 'react';
import './App.css';
import Spotify from 'spotify-web-api-js';
const spotifyWebApi = new Spotify();
class App extends Component {
constructor(){
super();
const params = this.getHashParams();
this.state ={
loggedIn: params.access_token ? true : false,
nowPlaying: {
name: 'Not Checked',
image: ''
}
}
if (params.access_token){
spotifyWebApi.setAccessToken(params.access_token)
}
}
getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ( e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
componentDidMount(){
this.getNowPlaying();
}
getNowPlaying(){
spotifyWebApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response.item.name,
image: response.item.album.images[1].url,
artist: response.item.artists[0].name,
id: response.item.id
}
})
this.getNowPlaying();
}
)
}
render() {
return (
<div className="App">
<a href='http://localhost:8888'>
<button>Login But With Spotify</button>
</a>
<div> Now Playing: { this.state.nowPlaying.name} </div>
<div> By: { this.state.nowPlaying.artist} </div>
<div> Id: { this.state.nowPlaying.id} </div>
<div>
<img src={ this.state.nowPlaying.image} style={{ width: 100}}/>
</div>
</div>
)
}
}
Hooks
import React, { useState, useEffect, useCallback } from "react";
import "./App.css";
import Spotify from "spotify-web-api-js";
const spotifyWebApi = new Spotify();
function App() {
//not used?
const [loggedIn, setLoggedIn] = useState();
//telling whether the component is mount or not
const [isComponentMount,setIsComponentMount]=useState(true);
//response container
const [nowPlaying, setNowPlaying] = useState({
name: "Not Checked",
image: "",
artist: "",
id: ""
});
//Fetches the newest data
const getNowPlaying = useCallback(() => {
//Needed to stop the infinite loop after the component gets closed
if(!isComponentMount)return;
spotifyWebApi.getMyCurrentPlaybackState().then(response => {
setNowPlaying({
name: response.item.name,
image: response.item.album.images[1].url,
artist: response.item.artists[0].name,
id: response.item.id
});
//Change this one as you like (perhaps a timeout?)
getNowPlaying();
});
}, []);
useEffect(() => {
//Get hash params
const params = {};
var e,
r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ((e = r.exec(q))) {
params[e[1]] = decodeURIComponent(e[2]);
}
if (params.access_token) {
spotifyWebApi.setAccessToken(params.access_token);
}
getNowPlaying();
return ()=>{
setIsComponentMount(false);
}
}, [getNowPlaying]);
return (
<div className="App">
<a href="http://localhost:8888">
<button>Login But With Spotify</button>
</a>
<div> Now Playing: {nowPlaying.name} </div>
<div> By: {nowPlaying.artist} </div>
<div> Id: {nowPlaying.id} </div>
<div>
<img src={nowPlaying.image} style={{ width: 100 }} alt="" />
</div>
</div>
);
}
export default App

React click on item to show details

Iam new to React and I'm trying to interact with the swapi API.
I want to get the list of films (movie titles list) and when I click on a title to show the opening_crawl from the json object.
I managed to get the film titles in an array. I don't know how to proceed from here.
Here is my code:
class StarWarsApp extends React.Component {
render() {
const title = "Star Wars";
const subtitle = "Movies";
return (
<div>
<Header title={title} />
<Movies />
</div>
);
}
}
class Header extends React.Component {
render() {
return (
<div>
<h1>{this.props.title}</h1>
</div>
);
}
}
class Movies extends React.Component {
constructor(props) {
super(props);
this.handleMovies = this.handleMovies.bind(this);
this.state = {
movies: []
};
this.handleMovies();
}
handleMovies() {
fetch("https://swapi.co/api/films")
.then(results => {
return results.json();
})
.then(data => {
console.log(data);
let movies = data.results.map(movie => {
return <div key={movie.episode_id}>{movie.title}</div>;
});
this.setState(() => {
return {
movies: movies
};
});
});
}
render() {
return (
<div>
<h1>Episodes</h1>
<div>{this.state.movies}</div>
</div>
);
}
}
ReactDOM.render(<StarWarsApp />, document.getElementById("app"));
To iterate over movies add this in render method:
render(){
return (
<div>
<h1>Episodes</h1>
{
this.state.movies.map((movie, i) => {
return (
<div className="movie" onClick={this.handleClick} key={i}>{movie.title}
<div className="opening">{movie.opening_crawl}</div>
</div>
);
})
}
</div>
);
}
Add this method to your Movies component to add active class on click to DIV with "movie" className:
handleClick = event => {
event.currentTarget.classList.toggle('active');
}
Include this css to your project:
.movie .opening {
display: none;
}
.active .opening {
display: block
}
After fetching the data, just keep it in your state then use the pieces in your components or JSX. Don't return some JSX from your handleMovies method, just do the setState part there. Also, I suggest using a life-cycle method (or hooks API maybe if you use a functional component) to trigger the fetching. By the way, don't use class components unless you need a state or life-cycle methods.
After that, you can render your titles in your render method by mapping the movies state. Also, you can have a place for your opening_crawls part and render it with a conditional operator. This condition changes with a click. To do that you have an extra state property and keep the movie ids there. With the click, you can set the id value to true and show the crawls.
Here is a simple working example.
const StarWarsApp = () => {
const title = "Star Wars";
const subtitle = "Movies";
return (
<div>
<Header title={title} />
<Movies />
</div>
);
}
const Header = ({ title }) => (
<div>
<h1>{title}</h1>
</div>
);
class Movies extends React.Component {
state = {
movies: [],
showCrawl: {}
};
componentDidMount() {
this.handleMovies();
}
handleMovies = () =>
fetch("https://swapi.co/api/films")
.then(results => results.json())
.then(data => this.setState({ movies: data.results }));
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
render() {
return (
<div>
<h1>Episodes</h1>
<div>
{this.state.movies.map(movie => (
<div
key={movie.episode_id}
id={movie.episode_id}
onClick={this.handleCrawl}
>
{movie.title}
{this.state.showCrawl[movie.episode_id] && (
<div style={{ border: "1px black solid" }}>
{movie.opening_crawl}
</div>
)}
</div>
))}
</div>
</div>
);
}
}
ReactDOM.render(<StarWarsApp />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I am using id on the target div to get it back from the event object. I don't like this method too much but for the sake of clarity, I used this. You can refactor it and create another component may be, then you can pass the epoisde_id there and handle the setState part. Or you can use a data attribute instead of id.

Unable to render html divs with React?

I'm trying to generate several divs based off an array - but I'm unable to. I click a button, which is supposed to return the divs via mapping but it's returning anything.
class History extends Component {
constructor(props) {
super(props);
this.state = {
info: ""
};
this.generateDivs = this.generateDivs.bind(this);
}
async getCurrentHistory(address) {
const info = await axios.get(`https://api3.tzscan.io/v2/bakings_history/${address}?number=10000`);
return info.data[2];
}
async getHistory() {
const info = await getCurrentHistory(
"tz1hAYfexyzPGG6RhZZMpDvAHifubsbb6kgn"
);
this.setState({ info });
}
generateDivs() {
const arr = this.state.info;
const listItems = arr.map((cycles) =>
<div class="box-1">
Cycle: {cycles.cycle}
Count: {cycles.count.count_all}
Rewards: {cycles.reward}
</div>
);
return (
<div class="flex-container">
{ listItems }
</div>
)
}
componentWillMount() {
this.getHistory();
}
render() {
return (
<div>
<button onClick={this.generateDivs}>make divs</button>
</div>
);
}
You are not actually rendering the the divs just by invoking the generateDivs function, the JSX it is returning is not being used anywhere.
To get it to work you could do something like -
render() {
return (
<div>
<button onClick={this.showDivs}>make divs</button>
{this.state.isDisplayed && this.generateDivs()}
</div>
);
}
where showDivs would be a function to toggle the state property isDisplayed to true
The main point is that the JSX being returned in the generateDivs function will now be rendered out in the render function. There is many ways to toggle the display, that is just one straight forward way

React - setState errir - can't update during state transition

When this component is called I get the follow error.
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.
It seems to be because { this.renderCurrentAthlete() } inside render. When I call renderCurrentAthlete I'm trying to let state know who the current Athlete is by running the this.setState({ currentAthlete: currentAthleteData.Athlete }) but it causes an error. Any advise on how to handle this properly? Also any other advise on the component would be awesome too! Learning so all info is a great help :)
class MiniGame extends Component {
constructor(props){
super(props);
this.state = {
score: 0,
currentAthlete: null
}
}
gameData = [
{Athlete: "Peyton Manning", Img: "someURL"},
{Athlete: "Tony Hawk", Img: "someURL"},
{Athlete: "Tomy Brady", Img: "someURL"},
{Athlete: "Usain Bolt", Img: "someURL"}
]
renderGameButtons() {
return(
<div>
{this.gameData.map((x) => {
return(
<div key={x.Athlete}>
<button className="btn btn-outline-primary" onClick={ () => this.answerHandler(x.Athlete)}> {x.Athlete} </button>
</div>
)
})}
</div>
)
}
renderCurrentAthlete() {
const currentAthleteData = this.gameData[Math.floor(Math.random() * 4)];
//console.log(currentAthleteData);
const imgUrl = currentAthleteData.Img;
const athleteName = currentAthleteData.Athlete;
console.log(imgUrl, athleteName);
//console.log(currentAthlete);
this.setState({ currentAthlete: currentAthleteData.Athlete });
return(
<img className="card-img-top imgCard" src={imgUrl} alt="..."></img>
)
}
answerHandler(answer){
// console.log(a)
// console.log(this.state.currentAthlete)
if(answer === this.state.currentAthlete) {
this.setState({score: this.state.score + 10})
console.log(this.state.score);
}
}
render(){
return(
<div className="miniGameContainer">
<div className="card card-outline-info mb-3">
{ this.renderCurrentAthlete() }
<div className="card-block">
<p className="card-text">Pick your Answer Below</p>
{ this.renderGameButtons() }
</div>
</div>
</div>
)
}
}
Add method componentWillMount put this code to it and remove from renderCurrentAthlete. method componentWillMount will invoke before render. See more react lifecycle
componentWillMount() {
const currentAthleteData = this.gameData[Math.floor(Math.random() * 4)];
//console.log(currentAthleteData);
const imgUrl = currentAthleteData.Img;
const athleteName = currentAthleteData.Athlete;
console.log(imgUrl, athleteName);
//console.log(currentAthlete);
this.setState({ currentAthlete: currentAthleteData.Athlete });
}

Why does my small React project respond terribly when the window is resized?

I have made a small project to learn more about react and have noticed that the view responds very poorly when the window size is changed.
I feel i must be doing something somewhere i shouldn't and that is creating this cumbersome experience.
Here's my app:
import ColourCard from "./components/colour-card";
const url = "https://raw.githubusercontent.com/mdn/data/master/css/syntaxes.json";
class App extends React.Component {
constructor() {
super();
this.state = {
error: null,
colours: []
};
}
componentDidMount() {
fetch(url)
.then( response => response.json() )
.then( data => {
let colours = data['named-color']['syntax'].split(' | ');
colours = colours.filter((colour) => {
return !colour.includes('gray') && !colour.includes('transparent');
});
this.setState({ colours });
let clipboard = new Clipboard('.js-copy');
clipboard.on('success', function(e) {
const el = e.trigger.closest('.card').parentNode.getElementsByClassName('card-flash')[0];
el.getElementsByTagName('strong')[0].innerHTML = e.text;
el.classList.add('active');
setTimeout(() => el.classList.remove('active'), 1000);
});
})
.catch( e => this.setState({ error: 'Ooops, error' }) )
}
render() {
const { error, colours } = this.state;
if ( error ) {
return <div>{error}</div>
}
if ( !colours.length ) {
return <div>Loading...</div>
}
return (
<div className="grid">
{colours.map((colour, index) => {
return <ColourCard colour={colour} key={index}></ColourCard>
})}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"));
and heres my card component:
class ColourCard extends React.Component {
render() {
const colour = tinycolor(this.props.colour);
const style = {
backgroundColor: colour.toHexString()
};
return (
<div className="grid__item size-6#m size-4#l">
<div className="card">
<div className="card__colour" style={style}></div>
<div className="card__meta">
<div className="card__meta-item js-copy" data-clipboard-text={this.props.colour}>{this.props.colour}</div>
<div className="card__meta-item js-copy" data-clipboard-text={colour.toHexString()}>{colour.toHexString()}</div>
<div className="card__meta-item js-copy" data-clipboard-text={colour.toRgbString()}>{colour.toRgbString()}</div>
<div className="card__meta-item js-copy" data-clipboard-text={colour.toHslString()}>{colour.toHslString()}</div>
<div className="card__meta-item js-copy" data-clipboard-text={colour.toHsvString()}>{colour.toHsvString()}</div>
</div>
</div>
<div className="card-flash" style={style}>
<span className="card-flash__text">
<strong className="card-flash__strong"></strong>
<br />
Copied!
</span>
</div>
</div>
);
}
}
export default ColourCard;
https://codepen.io/matt3224/project/editor/ZvLGGA#
Any help much appreciated!
This whole block of code here:
let clipboard = new Clipboard('.js-copy');
clipboard.on('success', function(e) {
const el = e.trigger.closest('.card').parentNode.getElementsByClassName('card-flash')[0];
el.getElementsByTagName('strong')[0].innerHTML = e.text;
el.classList.add('active');
setTimeout(() => el.classList.remove('active'), 1000);
});
You are never supposed to manually manipulate the DOM with react. That is really the one golden rule when using this library. This is the same reason why libraries like d3 have trouble with react, because it wants to get its hand into the DOM. React manages a virtual DOM, and any interference with that is not good. It can lead to performance issues and generally speaking, will break your app more times than not.

Categories