So i'm currently working on a PokeDex using the PokeApi available online.
The code of the project is as follows:
import React, { Component } from "react";
import PokemonCard from "./PokemonCard";
import "../ui/PokemonList.css";
import axios from "axios";
export const PokemonList = class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokemon: res.data["results"] });
console.log(res);
}
render() {
return <div></div>;
}
};
export const PokeList = () => {
return (
<React.Fragment>
{this.state.pokemon ? (
<section className="poke-list">
{this.state.pokemon.map(pokemon => (
<PokemonCard />
))}
</section>
) : (
<h1>Loading Pokemon</h1>
)}
</React.Fragment>
);
};
As you can see, I have declared a state in the PokemonList Component class, but then I try to call it further down within the variable PokeList. The issue is that the state is not being recognized in PokeList
(I get the error "TypeError: Cannot read property 'state' of undefined" )
How can I go about calling the state that's declared in the class above?
-------------------EDIT-------------------------------
Okay, so I realized something. I have a code for my Dashboard.js that displays my list. Code is as follows
import React, { Component } from "react";
import { PokeList } from "../pokemon/PokemonList";
export default class Dashboard extends Component {
render() {
return (
<div>
<div className="row">
<div className="col">
<PokeList />
</div>
</div>
</div>
);
}
}
When I change the code from PokeList to PokemonList. so it'd be
import React, { Component } from "react";
import { PokemonList } from "../pokemon/PokemonList";
export default class Dashboard extends Component {
render() {
return (
<div>
<div className="row">
<div className="col">
<PokemonList />
</div>
</div>
</div>
);
}
}
I think get a list of 20 pokemon from the Api from
console.log(this.state.pokemon);.
But since I'm not displaying PokeList on the dashboard, then none of the pokemon cards display.
Screenshot of console output
First of all functional components are stateless. If you need to maintain state use class components or hooks. You can't use the state of one component in another component, You have two options,
Create a parent-child relationship between those components
Use state management libraries(Redux, etc)
There's a little of confusion between your PokemonList and PokeList component. I believe that what you really are looking for is to have just one of those. If you mix the two, you can have a component that controls the view based on the state, in your case, the state is your Pokemon list.
I mixed the two here, so your render method renders "Loading Pokemon" until you get your response back from axios, then when the response is back, it gets that data, updates your state and the state update trigger a re-render.
import React, { Component } from "react";
import PokemonCard from "./PokemonCard";
import axios from "axios";
class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
componentDidMount() {
axios.get(this.state.url).then(res => {
this.setState({ pokemon: res.data["results"] });
});
}
render() {
let pokemonList = <h1>Loading Pokemon</h1>;
const pokemons = this.state.pokemon;
if (pokemons) {
pokemonList = (
<section className="poke-list">
<ul>
{pokemons.map(pokemon => (
<PokemonCard pokemon={pokemon} />
))}
</ul>
</section>
);
}
return <React.Fragment>{pokemonList}</React.Fragment>;
}
}
export default PokemonList;
I also created a simple PokemonCard component where I list the result from the API, just to show you that that approach works.
import React from "react";
const pokemonCard = props => {
return (
<li key={props.pokemon.name}>
<a href={props.pokemon.url}>{props.pokemon.name}</a>
</li>
);
};
export default pokemonCard;
You can find the final code, with PokeList and PokemonList now combined into one component called PokemonList here:
Keep in mind that if your render function depends on a certain state, it's probably certain that you should have that state being managed in that component, or passed down from a parent component.
In your example, I noticed you set url inside your state. URL is really not something that will change. It's a constant,so you can easily remove that from your state and place it in a variable and just leave your pokemon list there.
For example:
const url = "https://pokeapi.co/api/v2/pokemon/";
state = {
pokemon: null
};
componentDidMount() {
axios.get(url).then(res => {
this.setState({ pokemon: res.data["results"] });
});
}
import React , { Component } from "react";
import axios from "axios";
//make it as class based component
export default class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokemon: res.data["results"] });
console.log(res);
}
render() {
//check your data here
console.log(this.state.pokemon)
{/*pass data to child*/}
return <div> <PokeList data = { this.state } /> </div>;
}
};
//export this component
export const PokeList = (props) => {
//check your data is coming or not
console.log(props.data)
//access your data from props
return (
<React.Fragment>
{props.data.pokemon ? (
<section className="poke-list">
{props.data.pokemon.map(pokemon => (
pokemon.name
))}
</section>
) : (
<h1>Loading Pokemon</h1>
)}
</React.Fragment>
);
};
You need iterate your your pokelist passing the result from your componentDidMount function to your child component as a prop , then receive your prop in the child component here it's a working codesandbox iterating your pokemon names in the pokeList child component
Related
So I am using React's context because I have to change a state in the opposite direction.
E.g.:
App.js (has state) <--- My Component (changes the state in App.js)
I know how to do this using an onClick event. However, I fail understanding how to do this in a componentDidMount(). I created a basic example to illustrate what I'm trying to achieve:
MyComponent.js
import { MyConsumer } from '../App.js';
export default class MyComponent extends Component {
componentDidMount() {
// TRYING TO CHANGE STATE IN COMPONENTDIDMOUNT
<MyConsumer>
{({ actions }) => actions.setMyState(true)}
</MyConsumer>
}
render() {
return (
<SearchConsumer>
{({ actions }) => {
return (
<div onClick={() => actions.setMyState(true)}>
My content
</div>
)
}}
</SearchConsumer>
)
}
}
App.js
export const SearchContext = createContext();
export const SearchProvider = SearchContext.Provider;
export const SearchConsumer = SearchContext.Consumer;
class App extends Component {
constructor (props) {
super (props)
this.state = {
setMyState: 0,
}
}
render(){
return(
<SearchProvider value={
{
actions: {
setMyState: event => {
this.setState({ setMyState: 0 })
},
}
}
}>
<Switch>
<Route
exact path='/' render={(props) => <MyComponent />}
/>
</Switch>
</SearchProvider>
)
}
}
If you're using react 16.6.0 or later and are using exactly one context consumer, then the simplest approach is to use contextType (note that that's singular, not plural). This will cause react to make the value available on this.context, which you can then use in lifecycle hooks. For example:
// In some other file:
export default MyContext = React.createContext();
// and in your component file
export default class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.context;
actions.setMyState(true);
}
// ... etc
}
If you are on an older version and thus can't use contextType, or if you need to get values from multiple contexts, you'll instead need to wrap your component in another component, and pass the context in via a prop.
// In some other file:
export default MyContext = React.createContext();
// and in your component file
class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.props;
actions.setMyState(true);
}
// ... etc
}
export default props => (
<MyContext.Consumer>
{({ actions }) => (
<MyComponent actions={actions} {...props} />
)}
</MyContext.Consumer>
);
I fixed my problem by an idea given thanks to Nicholas Tower's answer. Instead of using the contextType in React, I just passed my actions as a prop in a different component. This way I could still use everything of my consumer if I just pass it on as a prop.
class MyComponent extends Component {
componentDidMount() {
this.props.actions.setMyState(true);
}
// ... etc
}
export default class MyComponentTwo extends Component {
render(){
return(
<MyConsumer>
{({ actions }) => (
<MyComponent actions={actions}/>
)}
</MyConsumer>
)
}
);
I am making a basic dropdown selector. I almost had it working when I realized I was setting the state in both the parent and the child so I refactored again to try to simplify it all and put most of the responsibility in one place.
My logic is in the MyDropDown component, then I have a Header component, then the Main which should render it all.
import React from 'react';
class MyDropdown extends React.Component {
render() {
let initialUsers = this.props.state.users;
let alphabetizeUsers = initialUsers
.sort((a, b) => {
return a.name > b.name;
})
.map(obj => {
return (
<option key={obj.id} value={obj.name}>
{obj.name}
</option>
);
});
return <select>{alphabetizeUsers}</select>;
}
}
export default MyDropdown;
Then I have my main component where I do the api call and pass the state into the dropdown component.
import React from 'react';
import MyDropdown from './MyDropdown';
class UserHeader extends React.Component {
state = {
users: []
};
componentDidMount() {
let initialUsers = [];
fetch('http://localhost:3000/users')
.then(response => {
return response.json();
})
.then(data => {
this.setState({ users: data });
});
}
render() {
return <MyDropdown state={this.state} />;
}
}
export default UserHeader;
And finally my Main Component, where I want to show the value from the selected dropdown menu
import React, { Component } from 'react';
import './Main.css';
import MyDropdown from './components/MyDropdown';
import UserHeader from './components/UserHeader';
class Main extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<span className="App-title">SELECT A USER:</span>
<UserHeader />
</header>
<p className="App-intro">
I should get the dropdown value here: {this.state.user}
</p>
</div>
);
}
}
export default Main;
What I tried doing is moving the statement
I should get the dropdown value here: {this.state.policies} .
into the UserHeader component. How do I get the value selected in the child back up to its parent?
Another thing I've tried is adding a handler to the child component
onChange = e => {
this.setState({ selectedUser: e.target.value });
};
and add it to the select... but again not sure how to get this value up to the parent.
return <select onChange={this.onChange}>{alphabetizeUsers}</select>;
The easiest way to pass the value back to the parent component is through a callback.
Try defining and passing in an onChange={this.onChange} to your Main component like so your Main component becomes:
import React, { Component } from 'react';
import './Main.css';
import MyDropdown from './components/MyDropdown';
import UserHeader from './components/UserHeader';
class Main extends Component {
this.state = {
user: null,
}
constructor(props) {
super(props);
this.onChangeUser = this.onChangeUser.bind(this);
}
onChangeUser(newUser) {
this.setState({ user: newUser });
}
render() {
return (
<div className="App">
<header className="App-header">
<span className="App-title">SELECT A USER:</span>
<UserHeader onChangeUser={this.onChangeUser} />
</header>
<p className="App-intro">
I should get the dropdown value here: {this.state.user}
</p>
</div>
);
}
}
export default Main;
Now you are passing in a callback, you can do the same thing with your UserHeader component.
import React from 'react';
import MyDropdown from './MyDropdown';
class UserHeader extends React.Component {
state = {
users: []
};
componentDidMount() {
let initialUsers = [];
fetch('http://localhost:3000/users')
.then(response => {
return response.json();
})
.then(data => {
this.setState({ users: data });
});
}
render() {
return <MyDropdown state={this.state} onChange={this.props.onChangeUser} />;
}
}
export default UserHeader;
And finally, you can now attach this callback to your <select> element.
import React from 'react';
class MyDropdown extends React.Component {
render() {
let initialUsers = this.props.state.users;
let alphabetizeUsers = initialUsers
.sort((a, b) => {
return a.name > b.name;
})
.map(obj => {
return (
<option key={obj.id} value={obj.name}>
{obj.name}
</option>
);
});
return <select onChange={(ev) => this.props.onChange(ev.target.value)}>{alphabetizeUsers}</select>;
}
}
export default MyDropdown;
By defining the onChange on your select element like this, onChange={(ev) => this.props.onChange(ev.target.value)}, you can return the value to the main component and use it in your state.
I am building a simple movie catalogue using themoviedb API however I am facing an issue that I am unable to solve.
The issue is that the result after fetching is always undefined.
I tried with the method componentWillMount to fetching data and the setting the state inside this method but it does not work.
I tried to fetch inside constructor, no result.
This is my code so far
import React, { Component } from 'react';
import Header from './components/Header';
import MovieList from './components/MovieList';
import Footer from './components/Footer';
const MOVIE_API = "http://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&sort_by=release_date.desc&include_adult=true&include_video=false&page=2&primary_release_year=2018";
//class
class App extends Component {
constructor(props){
super(props);
this.state = {
movies: [],
movieName: ''
}
}
componentWillMount(){
this.fetchMovie();
}
//fetching movie
fetchMovie = () =>{
const req = new Request(MOVIE_API, {
method: 'GET',
cache: 'default'
});
fetch(req).then(response =>{
return response.json();
}).then(data =>{
console.log(data); //REF 1;
this.setState({
movies: data
});
}).catch(err => {
console.log("ERROR: " + err);
})
}
render() {
return (
<div className="root">
<Header />
<MovieList moviesRes={this.state.movies}/>
<Footer />
</div>
);
}
}
export default App;
As you can see I called the method componentWillMount to fetch the data but it does not work.
It is also noticeable that if I log the data (REF 1) I can see the result (json).
===========================
EDIT
This is the code for MovieList
/*import React, { Component } from 'react';
export default class MovieList extends Component{
constructor(props){
super(props);
this.state = {
movies: this.props.movieRes
}
}
render(){
//if result is undefined
if(this.state.movieRes === undefined){
return(
<h1>Loading...</h1>
);
}else{
return(
<ul>
{this.state.movieRes.map((movie, index)=>{
return (
<li key={index}>{movie.title}</li>
);
})}
</ul>
);
}
}
}*/
=================
update child code
import React, { Component } from 'react';
export default class MovieList extends Component{
render(){
const { movieRes = [] } = this.props; // we are assigning a default prop here of an empty array.
return(
<ul>
{
//return movie from array
movieRes.map((movie, index)=>{
return (
<li key={index}>
{movie.id}
</li>
);
})
}
</ul>
);
}
}
In this I way I suppress the error, but still it is not working.
From what I learnt, React should render as soon as it detect changes but for some reason it not the case.
IMAGE
As you can see from the image when I am passing the array from parent component to the child component the array length is 20 but in the child component the array length seems to be 0
===================
Solution
I changed the component from class to a const and pass to it the array and everything went smooth. Here is the final code:
import React from 'react';
const MovieList = ({movies}) =>{
if(!movies){
return <h1>Loading...</h1>
}
return (
<ul>
{
movies.map((movie, index) => {
return (
<li key={index}>
<p>{movie.title}</p>
</li>
)
})
}
</ul>
);
}
export default MovieList;
Originally I misunderstood your issue but after re-reading it I noticed that you defined movies as an array in your constructor.
Without an actual error message, I'm going to assume that MovieList is expecting an array for it's prop movieRes and you're probably then trying to do something like .map or a loop to render the movies.
However, the API you're using doesn't return an array. It returns an object with an array key'd under results. So, I changed it to access data.results when doing setState.
//fetching movie
fetchMovie = () =>{
const req = new Request(MOVIE_API, {
method: 'GET',
cache: 'default'
});
fetch(req).then(response =>{
return response.json();
}).then(data =>{
console.log(data);
this.setState({
movies: data.results // <-- change made here.
});
}).catch(err => {
console.log("ERROR: " + err);
})
}
Here's a working JSFiddle:
https://jsfiddle.net/patrickgordon/69z2wepo/99513/
EDIT:
In the child component, instead of assigning props to state, just use props and default props.
import React, { Component } from 'react';
export default class MovieList extends Component{
render(){
const { movieRes = [] } = this.props; // we are assigning a default prop here of an empty array.
return(
<ul>
{movieRes.map((movie, index)=>{
return (
<li key={index}>{movie.title}</li>
);
})}
</ul>
);
}
}
I am practicing React native. When I compile the following program, I am getting Cannot read property 'props' of undefined error for Details.js. Kindly let me know as to what went wrong here.
Layout.js
import React, {Component} from 'react';
import Header from './Header';
import Details from './Details';
export default class Layout extends React.Component {
constructor(props){
super(props);
this.state = {
heading: "Welcome no-name guy!",
header: "I am your header",
footer: "I am your footer"
};
}
render() {
return (
<div>
<Header headerprop={this.state.header} />
<Details detailprop={this.state.heading} />
</div>
);
}
}
Details.js
import React from 'react';
const Details = (detailprop) => {
return (
<div className="heading-style">{this.props.detailprop}</div>
);
};
Details.bind(this);
export default Details;
Header.js
import React, {Component} from 'react';
export default class Header extends React.Component {
render(){
return(
<div>{this.props.headerprop}</div>
);
}
}
In functional components, the props are passed as the first parameter. So, you only need to do this:
const Details = (props) => {
return (
<div className="heading-style">{props.detailprop}</div>
);
};
If you know the prop that you want to handle you can destructure that prop:
const Details = ({ detailProp }) => {
return (
<div className="heading-style">{detailprop}</div>
);
};
Your component argument should be props:
const Details = (props) => {
return (
<div className="heading-style">{props.detailprop}</div>
);
};
It could be detailprop as you have (or anything for that matter) but you would then need to access the prop by the confusing call:
detailprop.detailprop
props is the idiomatic approach for React.
Details.js is a stateless functional react component. https://facebook.github.io/react/docs/components-and-props.html
It receives props as its argument. You don't need this here.
import React from 'react';
const Details = (props) => {
return (
<div className="heading-style">{props.detailprop}</div>
);
};
Details.bind(this); // you don't need this
export default Details;
Also, div elements will not work for react-native . Please refer react native docs https://facebook.github.io/react-native/
I m learning redux and react and I'm having a little problem concerning good practices of props initialization.
In fact, I m having a route that looks like following :
/pokemons/:name
And here's the concerned component :
import React from 'react';
import {connect} from 'react-redux';
import {showDetail} from './../../redux/modules/pokemons';
export class PokeDetail extends React.Component{
render(){
return(
<div>
{this.currentPokemon.name}
</div>
)
}
}
const mapStateToProps = state => ({
currentPokemon:state.pokemons.currentPokemon
});
export default connect((mapStateToProps),{
showDetail
})(PokeDetail);
The fact is that I don't know at all when / where to send my action to change my app state. In fact, when should I send my "showDetail('myPokemonName')" so that the currentPokemon state would change and my app work ?
I m needing some good practices if possible
Thanks for your help
EDIT :
My PokeView :
import React from 'react';
import {connect} from 'react-redux';
import {loadRemoteAction} from './../../redux/modules/pokemons';
import {Link} from 'react-router';
export class PokeView extends React.Component {
render(){
let i = 0;
return (
<div>
<h4>Super page</h4>
<button onClick={this.props.loadRemoteAction}>Start</button>
<ul>
{
this.props.pokemons.map(item => {
i++;
return <li key={i}><Link to={`/pokemons/${item.name}`}>{item.name}</Link></li>
})
}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
pokemons : state.pokemons.pokemons
});
export default connect((mapStateToProps), {
loadRemoteAction
})(PokeView);
My action / reducer :
import immutable from 'immutable';
/**
* Action part
*/
export const LOAD_REMOTE = 'LOAD_REMOTE';
export const SHOW_DETAIL = 'SHOW_DETAIL';
export function loadRemoteAction() {
return dispatch => {
fetch('http://pokeapi.co/api/v2/pokemon/')
.then(res => res.json())
.then(res => dispatch({
type:LOAD_REMOTE,
payload:res.results
}));
};
}
export function showDetail(name){
return {
type:SHOW_DETAIL,
payload:name
}
}
/**
* Reducer part
*/
const ACTION_HANDLER = {
[LOAD_REMOTE] : (state, action) => {
if(action.payload) return Object.assign({},state,{pokemons:state.pokemons.concat(action.payload)});
return state;
},
[SHOW_DETAIL] : (state, action) =>{
let currentPokemon;
for(const pkm of state.pokemons){
if(pkm.name === action.payload) currentPokemon = pkm;
}
if(action.payload) return Object.assign({},state,{currentPokemon:currentPokemon});
return state
}
}
const initialState = {pokemons:immutable.fromJS([]), currentPokemon:immutable.fromJS({name:''})};
export default function pokemonReducer (state = initialState, action) {
const handler = ACTION_HANDLER[action.type]
return handler ? handler(state, action) : state
}
Generally speaking, an action is when something happens in the world - this would usually either be something a user does, or e.g. an asynchronous answer from the backend to an ajax call. These are the situations in which you would want to send an action (containing the information about what was changed) so that your state tree can be updated accordingly.
In your case, if you show a list of Pokemons somewhere else on the screen, and the user clicks on one of them, then that click would save the clicked-on Pokemon to the state tree, and your PokeDetail component would then pick up this information and display the details for the selected Pokemon.
In your case, the PokeView render function might look like this:
export class PokeView extends React.Component {
render(){
return (
<div>
<h4>Super page</h4>
<button onClick={this.props.loadRemoteAction}>Start</button>
<ul>
{
this.props.pokemons.map((item, i) => <li key={i}><button onClick={this.props.dispatch(showDetail(item.name))}>{item.name}</button></li> )
}
</ul>
</div>
);
}
}
and the PokeDetail class might look like this:
export class PokeDetail extends React.Component{
render(){
return <div>{this.props.currentPokemon.name}</div>;
}
}
The other question is, how does the information go into my app initially? If the data is static, you can add it to your initial state tree (one usually passes that to the reducer function as default parameter), or you could query the data from the backend via an Ajax call. The latter can be done in the componentDidMount lifecycle method of your component. (For this, you need redux-thunk in order to have an action that works together with the callback and the asynchronous answer from the backend).
You should call showDetail action in componentWillMount function.
import React from 'react';
import {connect} from 'react-redux';
import {showDetail} from './../../redux/modules/pokemons';
export class PokeDetail extends React.Component{
componentWillMount() {
// If you are using react-router, route params will be in `params` property object.
const pokemonId = this.props.params.id;
this.props.showDetail(pokemonId);
}
componentWillReceiveProps(nextProps) {
// Also you may want to update your component when url was changed.
if (nextProps.params.id !== this.props.params.id) {
this.props.showDetail(nextProps.params.id);
}
}
render(){
return(
<div>
{this.currentPokemon.name}
</div>
)
}
}
const mapStateToProps = state => ({
currentPokemon:state.pokemons.currentPokemon
});
export default connect((mapStateToProps),{
showDetail
})(PokeDetail);
When your component is mounting(or updating), you are calling your action with pokemon id. Next your store will be updated and you will receive needed props in PokeDetail component.
Also for async actions you may need redux-thunk package.
You can use redux-async-connect package. You can find sample usage of this package here.