Adding child elements dynamically in React (state array) - javascript

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

Related

React - How to re-render a component using another component?

I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories). These links are stored inside a child component of the NavBar, called DrawerMenu.
The NavBar is a child of the main App.js component.
In my Category component, I have a "delete" function that deletes a category. Once I delete a category I want to remove the link to it in the NavBar. How would I go about doing this?
For further context, my components are given below:
DrawerMenu component
class DrawerMenu extends Component {
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
getData = (query) => {
// Query backend for category data and set it to this.state.menuItems
}
componentDidMount() {
this.getData(menuItemsQuery)
}
render() {
const { classes, handleDrawerClose, open } = this.props
const { menuItems } = this.state
const drawer = (classes, handleDrawerClose) => (
<div>
...
{
menuItems.map((menuItem, index) => (
<Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
<ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
<ListItemText primary={menuItem.name} />
</ListItem>
</Link>
))
}
...
</div>
)
...
return (
<div>
<Drawer
variant="temporary"
anchor='left'
open={open}
onClose={handleDrawerClose}
classes={{
paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer(classes, handleDrawerClose)}
</Drawer>
</div>
)
}
}
NavBar component
function PrimarySearchAppBar(props) {
return (
<div className={classes.grow}>
...
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
/>
...
</div>
)
}
Category component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.history.push(`/`)
}
...
}
There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.
Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props.
I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.
class DrawerMenu extends Component {
// We're gonna manage the state here, so the deletion
// will actually be handled by this component
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
handleDelete = (id) => {
let updatedMenuItem = [...this.state.menuItems]; //Create a copy
updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the
deleted item
this.setState({
menuItems: updatedMenuItem
})
}
...
// Then wherever you render the category component
<Category handleDelete = {handleDelete}/> //Pass a reference to the delete method
}
Category Component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.handleDelete(categoryId) //Pass the id of the category
this.props.history.push(`/`)
}
...
}
I would suggest reading about state management, it is a core concept in React and you will use it everywhere. Redux and Context API for example.
Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.
The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server. In the example below, this ancestor that is shared is MyCategoriesProvider
Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.
Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.
// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories,
// and supply the updating functions. For brevity, I'll just give
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc
import { createContext } from 'react';
// export this, we'll be using it later
export const CategoriesContext = createContext();
// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {
// here we can add a status flag in case we wanted to show a spinner
// somewhere down in your app
const [isRequestingCategories,setIsRequestingCategories] = useState(false);
// this is your list of categories that you got from the server
// we'll start with an empty array
const [categories,setCategories] = useState([]);
const fetch = async () => {
setIsRequestingCategories(true);
setCategories(await apiCallToFetchCategories());
setIsRequestingCategories(false);
}
const handleDelete = async category => {
await apiCallToDeleteCategory(category);
// we deleted a category, so we should re-request the list from the server
fetch();
}
useEffect(() => {
// when this component mounts, fetch the categories immediately
fetch();
// feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
},[]);
return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>
}
// And you use it like this:
const App = () => {
return (
<MyCategoriesProvider>
<SomeOtherComponent>
<SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
<SomeOtherComponent>
</MyCategoriesProvider>
)
}
// in PrimarySearchBar you'd do this:
function PrimarySearchBar(props) => {
const {categories} = useContext(CategoriesContext); // you exported this above, remember?
// pass it as a prop to navbar, you could easily put the useContext hook inside of any component
return <NavBar categories={categories}/>
}
// in your category component you could do this:
class Category extends Component {
render() {
// Don't forget, categoriesContext is the thing you exported way up at the top
<CategoriesContext.Consumer>
{({handleDelete}) => {
return <button onClick={() => handleDelete(this.props.category)}>
}}
</CategoriesContext.Consumer>
}
}
EDIT:
I see you're mixing class and functional components, which is fine. You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.
I would just refresh the list of categories that come from the server, after the delete request is done.
I'd do it as follows:
I would make the drawer component not so smart, making it receive the list of menuItems.
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
items={/* ... */}
/>
This is an important step, because now, to refresh the list of items rendered, you just pass another list. The server-side logic remains disconnected from this component in this way.
I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level. I see 2 solutions:
I'd do the request for the menuItems from the same place where I do the request for the categories:
const App = props => {
const [categories, setCategories] = React.useState([])
const [menuItems, setMenuItems] = React.useState([])
const fetchCategories = useCallback(()=> {
yourApi.getCategories().then(categories => setCategories(categories))
})
const fetchMenuItems = useCallback(() => {
yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
})
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchMenuItems()
}, [categories])
const handleDeleteCategory = useCallback(idToDelete => {
yourApi.deleteCategory(idToDelete).then(fetchCategories)
})
return (
<div>
<PrimarySearchAppBar menuItems={menuItems}/>
<Categories categories={categories} onDeleteClick={handleDeleteCategory} />
</div>
)
}
you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here. It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.
PS.
There is also a nice hook to make fetches easier:
https://github.com/doasync/use-promise
I currently use a custom version of a usePromise hook I found because I added some interesting features. I can share it if you want but I don't want to add noise to the answer.

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

Passing data up through nested Components in React

Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering

React component to return something other than jsx

I am trying to refactor some code written by someone else and was wondering if the following is possible. can I create a react component that returns non html or jsx? and returns a function or something?
basically the component so far contains 4 functions and then a return block that returns some jsx that gets rendered
I want to extract 2 of these functions out into their own component as they are similar and can be reused
however, these functions just make ajax calls and call other functions rather than returning any jsx. they also require props inside them and need the props either passed in or connected to the redux store
some of the functions it calls are dispatch functions and therefore MUST be connected to the redux store. so I cannot build a standalone function
does this make sense or am I missing something. pseudo code below to demonstrate:
const component = ({...props}) => {
const func1(){
prop1()
try{
} catch(){
callingAnotherProp()
}
}
const func2(){
}
const func3(){
}
const func4(){
}
}
imagine func1 and func2 were identical. and as you can see they don't return any jsx just make further calls.
I don't think I can isolate into a sep function. I'd rather not pass the props in as args and just connect the function to the redux store and use the props that way. is this possible?
What about HOC? You can create a component with the func1()method and the render function of this component will just return {this.props.children}.
The two other options are a standalone function (you don't like it) and inheritance (I don't like it).
Not sure i fully understand your goal here but from what i understand, you want a component that will fetch data and will expose it to other components (or child components?).
There is a nice pattern called children as a function (or render props).
Basically you treat the children passed to the component as a function and you can pass the function anything (fetched data in you case).
return this.children(this.state.data)
Here is a small running example:
class Fetcher extends React.Component {
state = { result: { data: [] } };
componentDidMount() {
const { url } = this.props;
fetch(url)
.then(response => response.json())
.then(data => this.setState({ result: { data } }))
.catch(err => this.setState({ result: { error: true, message: err } }));
}
render() {
const { children } = this.props;
return children(this.state.result);
}
}
class App extends React.Component {
renderUsers = users => users.map(user => <div>{user.name}</div>);
render() {
return (
<div>
<Fetcher url={"https://jsonplaceholder.typicode.com/users"}>
{response => (
<div>
{response.data.length
? this.renderUsers(response.data)
: "Loading..."}
</div>
)}
</Fetcher>
</div>
);
}
}
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
<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="root"/>

Looping dynamic server response in React render

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.

Categories

Resources