Looping dynamic server response in React render - javascript

I'm developing some SPAs in React and I came across with this issue several times; I eventually solved it in UGLY ways like the one posted in the code below.
I feel like I'm missing something obvious but I really can't figure out a more elegant (or even right) way to accomplish the same result, can you help me?
class Leaderboard extends React.Component {
constructor(props) {
super(props);
this.state={
lb:{},
leaderboard:""
};
this.leaderboardGet = this.leaderboardGet.bind(this);
this.leaderboardSet = this.leaderboardSet.bind(this);
this.lblooper = this.lblooper.bind(this);
this.lbconstruct = this.lbconstruct.bind(this);
}
leaderboardGet(callback){ //server call which returns data
axiosCall.get('leaderboard.php',{params:{user:this.props.user}})
.then((response)=>{
var arr=response.data;
callback(arr);
})
.catch((error)=>{
console.log(error);
})
}
leaderboardSet(a){ //puts in lb object the results of the server call and calls lb looper
this.setState({lb: a});
this.lblooper();
}
componentWillMount(){
this.leaderboardGet(this.leaderboardSet);
}
lblooper(){ //the ugliness itself: loops the data in lb object, and pushes it into an "html string" in lblconstruct function
Object.entries(this.state.lb).forEach(
([key, value]) => this.lbconstruct(`<div class="leaderblock ${value.active}"><div class="leaderscore">${value.pos}) </div><div class="leadername">${value.usrname}</div><div class="leaderscore dx">${value.pts} <i class='fa fa-trophy'></i></div></div>`)
);
}
lbconstruct(s){
this.setState({leaderboard:this.state.leaderboard+=s});
}
render() {
return (
<div>
<div className="leaderboard">
<div dangerouslySetInnerHTML={{__html:this.state.leaderboard}}/>
</div>
</div>
);
}
}
Basically, if I have server data which has to be put inside html format for N loops, i couldn't find another way, so I'm wondering where I'm wrong.

Output your data into react elements in your render function:
class Leaderboard extends React.Component {
constructor(props) {
super(props);
this.state={
leaderboard: {},
};
}
componentWillMount(){
axiosCall.get('leaderboard.php', { params: { user:this.props.user } })
.then(response => this.setState({ leaderboard: response.data }))
.catch(console.log)
}
render() {
const { leaderboard } = this.state
return (
<div>
<div className="leaderboard">
// .map returns a new array, which we have populated with the react elements
{ Object.keys(leaderboard).map((key) => {
const value = leaderboard[key]
return (
<div key={key} class={`leaderblock ${value.active}`}>
<div class="leaderscore">{value.pos}</div>
<div class="leadername">{value.usrname}</div>
<div class="leaderscore dx">
{value.pts}
<i class='fa fa-trophy'></i>
</div>
</div>
)
}) }
</div>
</div>
);
}
}
Doing it like this is what allows react to work, it can keep track of what elements are there, and if you add one, it can see the difference and just adds the one element to the end, rather than re-render everything.
Also note that if you are only fetching data once, it may make sense to use a "container" component which fetches your data and passes it in to your "dumb" component as a prop.

Related

Map is undefined in React

I am using fetch to get API data that I'm using to create a drop down that I will add routes too. I've done this a couple of times before but I used axios previously but I just wanted to get familiar with fetch as well. Can anyone see the problem of why map would be undefined?
import React, { Component } from 'react'
class Fetchheroes extends Component {
constructor() {
super();
this.state = {
heroes: [],
}
}
componentDidMount(){
fetch('https://api.opendota.com/api/heroStats')
.then(results => {
return results.json();
}).then(data =>{
let heroes = data.results.map((hero) =>{
return(
<div key={hero.results}>
<select>
<option>{hero.heroes.localized_name}</option>
</select>
</div>
)
})
this.setState({heroes: heroes});
console.log("state", this.state.heroes);
})
}
render(){
return(
<div>
<div>
{this.state.heroes}
</div>
</div>
)
}
}
export default Fetchheroes
You have a bad mapping about data. You need to use data instead of data.result and you have a bad key value because results are not unique key in that case. You also don't need your hero.heroes.localized_name just hero.localized_name. I made an example in codesandbox.
https://codesandbox.io/s/clever-hodgkin-7qo6p
Edit
I made another example when I put all records to one select, not for multiple selects, maybe is that what you need or someone else :).
https://codesandbox.io/s/bold-grass-gv0wc

Adding child elements dynamically in React (state array)

I am working to build a Pokedex from JSON data in React. I am refactoring this project from one I built in jQuery, so it could be that the jQuery approach is causing me to misunderstand how to approach this problem with proper React thinking. What's tripping me up so far is how to dynamically render multiple child elements based on the JSON I pass from a the parent element (this would be jQuery append).
Here is my App.js code:
class App extends Component {
render() {
return (
<div className="App background">
<div className="content">
<Header />
<TilesContainer pokedexName="national"/>
</div>
</div>
);
}
The TilesContainer essentially receives the name of a Pokedex and makes a call to an API. The individual Pokemon names are stored in an array in the TilesContainer state (this.state.pokemon), as below.
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {pokemon: []};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
}
getPokemon() {
// set this.state.pokemon to the list
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
list.forEach(pokemon => {
this.state.pokemon.push(pokemon);
})
})
this.tiles();
}
tiles() {
if (this.state.pokemon.length > 0) {
return (
this.state.pokemon.map(pokemon => {
<Tile number={pokemon.entry_number}/>
})
)
}
}
render(){
this.getPokemon();
return (
<div id="tiles-container"
className="tiles-container">
<h1>TilesContainer Test</h1>
<Tile number={1} />
</div>
)
}
}
export default TilesContainer
Again, the idea is that a Pokemon tile is render for each Pokemon in the Pokedex JSON (which for now I've stored in this.state.pokemon - not sure if this is the best approach). I found an example here on Stack Overflow that uses an additional function (this this case this.tiles() to generate what I think is an array of returns with different child elements). The <Tile number={1} /> is a hardcoded example of how the tile is called.
Currently no dynamically-rendered tiles show up when the code runs. Is this the correct approach. I'd really appreciate any suggestions.
Thanks!
It looks like you're almost there.
First off, never modify state directly. Use this.setState() instead. State in React is updated asynchronously. For your purposes, you should be able to modify getPokemon() like the following. I also removed the this.tiles() call, as it is unnecessary.
getPokemon() {
// set this.state.pokemon to the list
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
});
})
}
A minor correction for tiles(): when using an arrow function and returning something in one line, use parentheses instead of curly braces. When you use curly braces, you have to include a return statement. With parentheses, you do not.
tiles() {
if (this.state.pokemon.length > 0) {
return (
this.state.pokemon.map(pokemon => (
<Tile number={pokemon.entry_number}/>
))
)
}
}
Next, since tiles() returns your dynamic tile components, it needs to be included in what you return in render().
render(){
return (
<div id="tiles-container"
className="tiles-container"
>
<h1>TilesContainer Test</h1>
{this.tiles()}
</div>
)
}
Lastly, I think the call to this.getPokemon() would make more sense in the constructor, rather than in render().
I think your method of getting the json data and storing it in state is fine, by the way. In the future, you may want to look into Redux to manage your state, but it could be overkill for a really small application.
so you are passing the pokedexName from the parent component which is app.js, once you get the props you can call the rest api call on the componentWillMount life cycle.
so on the render since the api call has been initiated it wont have any data thats why we are using a ternary operator to check the array once the api call get finished and we get the data we are setting the data to the pokemon array.
Since the state is updated react will automatically render a re render so the data will appear.
i hope the below code will solve the issue, please let me know :)
// App.js
import React, { Component } from 'react';
import TilesContainer from './components/TileContainer/TilesContainer'
class App extends Component {
render() {
return (
<div>
<TilesContainer pokedexName="national" />
</div>
);
}
}
export default App;
// Tiles container
import React, {Component} from 'react';
import axios from 'axios';
class TilesContainer extends Component{
//state
state ={
pokemon: []
}
// life cycle methods
componentWillMount(){
let link = 'https://pokeapi.co/api/v2/pokedex/' + this.props.pokedexName + '/';
axios.get(link)
.then(res => {
this.setState({
pokemon: res.data["pokemon_entries"]
})
})
}
render(){
let style ={display:"inline"}
return(
<div>
{
this.state.pokemon.length > 0 ?
this.state.pokemon.map(pokemon => {
return(
<div key={pokemon.entry_number}>
<p style={style}>{pokemon.entry_number}</p>
<a href={pokemon.pokemon_species.url}>{pokemon.pokemon_species.name}</a>
</div>
)
})
:
null
}
</div>
)
}
}
export default TilesContainer

Get movies of all genres

I am making a movie application with ReactJS and the TMDb API, I would like to get the movies by genres, and display them in my homepage, for that I created for example a method initHorrorMovies() who:
Performs an axios TDMb API request for movies of a kind
https://developers.themoviedb.org/3/discover/movie-discover
Changes the state horrorMoviesList with new data
The problem is that there are many genres, so I'm going to create as many functions and states as there are genres.
I was thinking of creating a movieList object that would contain the results of the tmdb query for each genre as well as the title of the genre, and then update a movieList state with that object.
Do you have any suggestions for me?
I tried this
class App extends Component {
constructor(props){
super(props);
this.state = {
movieListWithGenre:[],
}
}
componentWillMount() {
this.initGenreMovie();
}
initGenreMovie(){
axios.get(LINK_GENRES).then(function(response){
this.initListMovie(response.data.genres)
}.bind(this));
}
initListMovie(GenreList){
this.setState({ moviesList: this.state.moviesList.push(newMovies)});
GenreList.map((element) => {
axios.get(`${API_END_POINT}discover/movielanguage=en
&with_genres=${element.id}
&include_adult=false&append_to_response=images
&${API_KEY}`).then(function(response){
this.setState({movieListWithGenre:this.state.movieListWithGenre.
push(response.data.results)})
}.bind(this));
})
}
}
Edit
Hello, I allow myself to go back the post because I develop a solution that works, I am able to get the list of films sorted by genres using the TMDB API request.
My solution works but I have a lot of latency when launching the application because I think the procedure is heavy, performance is impaired.
Here is my code, could I have some tips to improve this code? I thank you in advance for answers.
class App extends Component {
constructor(props){
super(props);
this.state = {
defaultgenre:28,
movieListWithGenre:[],
genreList:[],
genreId:[],
genreTitle:[]
}
}
componentDidMount() {
this.initGenreMovie();
}
initGenreMovie(){
axios.get(`${LINK_GENRES}`).then(function(response){
this.initListMoviesWithGenre(response.data.genres)
}.bind(this));
}
initListMoviesWithGenre(genres){
genres.map((genre) => {
axios.get(`${API_END_POINT}${POPULAR_MOVIES_URL}&${API_KEY}`)
.then(function(response){
let movies = response.data.results.slice(0,14);
let titleGenre = genre.name;
let idGenre = genre.id;
this.setState({movieListWithGenre:[...this.state.movieListWithGenre, movies]});
this.setState({genreTitle:[...this.state.genreTitle, titleGenre]});
this.setState({genreId:[...this.state.genreId, idGenre ]});
}.bind(this));
})
}
render(){
const renderVideoListGenre = () => {
if(this.state.movieListWithGenre) {
return this.state.movieListWithGenre.map((element,index) => {
return (
<div className="list-video">
<Caroussel
key={element.name}
idGenre {this.state.genreId[index]}
movieList={element}
titleList={this.state.genreTitle[index]}
/>
</div>
)
})
}
}
return (
<div>
{renderVideoListGenre()}
</div>
)
}
export default App
Once you discovered all the genre ids you want you can begin making axios calls to
https://api.themoviedb.org/<GENRE_ID>/genre/movie/list?api_key=<API_KEY>&language=en-US
You can make a single function for all genres or split them up, but should likely be called in the constructor. Once your axios calls return, you can put the movies data into your state like so:
this.setState({ moviesList: this.state.moviesList.push(newMovies) });
The shape of your movie and moviesList object is up to you and the data returned by the API.

Load external JSON in React

So I quite new to this. I want to load a external json file into react, and then use it.
The problem apparently is that the json hasn't been yet loaded, so I get errors. Like Cannot read property 'map' of undefined etc. The console.log give:
1
3
Uncaught TypeError: Cannot read property 'map' of undefined
So I've read this has to do with asynchronous things. But I can't find any example on how to fix it. Or how to make it work.
I would really appreciate it to see a small example like this, to make it work.
Later on I want to make it possible to filter the json with <input type=text etc> with some kind of dynamic search. But first things first. So can someone help me out?
This is my simple file:
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(){
super();
this.state = {
data: []
};
console.log('1');
};
componentDidMount() {
fetch("http://asite.com/file.json")
.then( (response) => {
return response.json() })
.then( (json) => {
this.setState({data: json});
console.log('2');
})
};
render() {
console.log("3");
return(
<div className="Employee">
{
this.state.data.employees.map(function(employee) {
return (
<div>
<h1> {employee.name} </h1>
</div>
)
})
}
</div>
)
}
}
export default App;
Since you have this.state.data.employees, I would assume you want to shape the initial state like this:
constructor(){
super();
this.state = {
data: {
employees: []
}
};
};
You can either choose to save to the state another variable which is flipped when you know you've loaded the data, or just check to ensure the data exists before trying to map it.
The latter could be done like below:
<div className="Employee">
{ this.state.data.employees &&
this.state.data.employees.map(function(employee) {
return (
<div>
<h1> {employee.name} </h1>
</div>
)
})
}
</div>
Or you could adjust the initial state to include an empty (but initialized) version of the return such as:
this.state = {
data: { employees: [] }
};
Checking to ensure the state contains the data field you're mapping is much safer though incase the return ever doesn't include the field.

Rendering react component after api response

I have a react component that I wish to populate with images using the Dropbox api. The api part works fine, but the component is rendered before the data comes through & so the array is empty. How can I delay the rendering of the component until it has the data it needs?
var fileList = [];
var images = [];
var imageSource = [];
class Foo extends React.Component {
render(){
dbx.filesListFolder({path: ''})
.then(function(response) {
fileList=response.entries;
for(var i=0; i<fileList.length; i++){
imageSource.push(fileList[0].path_lower);
}
console.log(imageSource);
})
for(var a=0; a<imageSource.length; a++){
images.push(<img key={a} className='images'/>);
}
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
Thanks for your help!
Changes:
1. Don't do the api call inside render method, use componentDidMount lifecycle method for that.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
2. Define the imageSource variable in state array with initial value [], once you get the response update that using setState, it will automatically re-render the component with updated data.
3. Use the state array to generate the ui components in render method.
4. To hold the rendering until you didn't get the data, put the condition inside render method check the length of imageSource array if length is zero then return null.
Write it like this:
class Foo extends React.Component {
constructor(){
super();
this.state = {
imageSource: []
}
}
componentDidMount(){
dbx.filesListFolder({path: ''})
.then((response) => {
let fileList = response.entries;
this.setState({
imageSource: fileList
});
})
}
render(){
if(!this.state.imageSource.length)
return null;
let images = this.state.imageSource.map((el, i) => (
<img key={i} className='images' src={el.path_lower} />
))
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
You should be using your component's state or props so that it will re-render when data is updated. The call to Dropbox should be done outside of the render method or else you'll be hitting the API every time the component re-renders. Here's an example of what you could do.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
imageSource: []
}
}
componentDidMount() {
dbx.filesListFolder({ path: '' }).then(function(response) {
const fileList = response.entries;
this.setState({
imageSource: fileList.map(file => file.path_lower);
})
});
}
render() {
return (
<div className="folioWrapper">
{this.state.imageSource.map((image, i) => <img key={i} className="images" src={image} />)}
</div>
);
}
}
If there are no images yet, it'll just render an empty div this way.
First off, you should be using the component's state and not using globally defined variables.
So to avoid showing the component with an empty array of images, you'll need to apply a conditional "loading" class on the component and remove it when the array is no longer empty.

Categories

Resources