Error when converting a class component to a functional component - javascript

i am trying to convert my class component to a functionnal component but my filters are not working anymore.
the main error is when I want to convert listProducts because I don't know how to define the prevState with useState for that case
how can update the state for case?
this is my code
class component
class ListShopping extends Component{
constructor(props) {
super(props);
this.state = {
size: "",
sort: "",
cartItems: [],
products: [],
filteredProducts: []
};
}
componentDidMount() {
fetch("http://localhost:8000/products")
.then(res => res.json())
.catch(err =>
fetch("db.json")
.then(res => res.json())
.then(data => data.products)
)
.then(data => {
this.setState({ products: data });
this.listProducts();
});
}
listProducts = () => {
this.setState(state => {
if (state.sort !== "") {
state.products.sort((a, b) =>
state.sort === "lowestprice"
? a.price < b.price
? 1
: -1
: a.price > b.price
? 1
: -1
);
} else {
state.products.sort((a, b) => (a.id > b.id ? 1 : -1));
}
if(state.size !== ""){
return {
filteredProducts: state.products.filter(
a => a.availableSizes.indexOf(state.size.toUpperCase()) >= 0
)
}
}
return { filteredProducts: state.products };
});
};
handleSortChange = e => {
this.setState({ sort: e.target.value });
this.listProducts();
};
handleSizeChange = e => {
this.setState({ size: e.target.value });
this.listProducts();
};
render() {
return (
<div className="container">
<h1>E-commerce Shopping Cart Application</h1>
<hr />
<div className="row">
<div className="col-md-9">
<Filter
count={this.state.filteredProducts.length}
handleSortChange={this.handleSortChange}
handleSizeChange={this.handleSizeChange}
/>
<hr />
<Products
products={this.state.filteredProducts}
/>
</div>
</div>
</div>
);
}
}
functionnal component
const ListShopping = () => {
const [data, setData] = useState({
products : [],
filteredProducts : [],
sort : '',
size : ''
})
const {products, filteredProducts, sort, size} = data;
const fetchApi = () => {
axios.get(`http://localhost:8000/products`)
.then(response => response.data)
.then(data => {
setData({...data, products: data});
})
}
const listProducts = () => {
};
const handleSortChange = e => {
setData({...e, sort: e.target.value})
listProducts();
};
const handleSizeChange = e => {
setData({...e, size: e.target.value})
listProducts();
};
useEffect(()=> {
fetchApi()
}, [])
return(
<div className="container">
<h1>E-commerce Shopping Cart Application</h1>
<hr />
<div className="row">
<div className="col-md-9">
<Filter
count={filteredProducts.length}
handleSortChange={handleSortChange}
handleSizeChange={handleSizeChange}
/>
<hr />
<Products
products={filteredProducts}
/>
</div>
</div>
</div>
)
}

Try this
const listProducts = () => {
setData(data => {
if (data.sort !== '') {
data.products.sort((a, b) =>
data.sort === 'lowestprice'
? a.price < b.price
? 1
: -1
: a.price > b.price
? 1
: -1,
);
} else {
data.products.sort((a, b) => (a.id > b.id ? 1 : -1));
}
if (data.size !== '') {
return {
...data,
filteredProducts: data.products.filter(
a => a.availableSizes.indexOf(data.size.toUpperCase()) >= 0,
),
};
}
return { ...data, filteredProducts: data.products };
});
};

Suppose you have a state like this
const [todos, setTodos] = useState([1,2,3,4,5,6,7]);
Method 1:
Now if you want to get prevState from this hook try this way
setTodos(oldTodo => {
oldTodo.push(1000);
setTodos(oldTodo)
})
this oldTodo work the same way the prevState works.
Method 2:
const listProducts = () => {
let oldState = todos
//Now do what you want to do with it. modify the oldTodo
setTodos(oldTodo);
}
I prefer the second method because it gives me more flexibility to change modify the state and if you cannot manage the state properly in the first method or if it finds any bug then it will return undefined so I prefer to work taking the whole state in a temporary variable and after work is done set it

Related

React fetching data based on previously fetched data

What I want is to fetch all users and then based on the username I fetch movies they have watched. I'm still struggling to understand when a state gets changed, more often than not at the end movies is not in the right order, so that when the MovieInfo-Component gets the data the users get the wrong movies assigned.
My code:
export default class Admin extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
movies: [],
};
}
componentDidMount() {
fetch('https://a-url/users/')
.then((res) => res.json())
.then((data) => {
this.setState(
{
users: data.users,
},
() => {}
);
data.users.map((user) => this.fetchMovies(user.name));
});
}
fetchMovies = (user) => {
fetch('https://a-url/' + user + '/movies/')
.then((res) => res.json())
.then((data) => {
this.setState(
{
movies: [...this.state.movies, ...[data.movies]],
},
() => {}
);
});
};
render() {
const { users, movies } = this.state;
return (
<div className='wum__admin section__padding'>
{users.length > 0 &&
movies.length > 0 &&
users.length === movies.length ? (
<>
{movies &&
users &&
users.map((user, i) => (
<MovieInfo
key={i}
movies={movies[user.id - 1]}
id={user.id}
user={user.name}
/>
))}
</>
) : (
<></>
)}
</div>
);
}
}
If you need to filter movies by user you could change the movies state to be an object where each property (user.name) will have its list of movies:
export default class Admin extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
movies: {},
};
}
...
fetchMovies = (user) => {
fetch('https://a-url/' + user + '/movies/')
.then((res) => res.json())
.then((data) => {
const newMovies = { ...this.state.movies };
newMovies[user] = movies;
this.setState(
{
movies: newMovies,
},
() => {}
);
});
};
render() {
const { users, movies } = this.state;
return (
<div className='wum__admin section__padding'>
{users.length > 0 &&
users.map((user, i) => (
<MovieInfo
key={i}
movies={movies[user.name]}
id={user.id}
user={user.name}
/>
))}
</div>
);
}
}

Why my methods for React function components don't work

I am developing a project with Moviedb api. I created the movie list under the name Movie component. I make my api requests in this component. From the Movie component to the MovieInfo component, I send the release date of the movie and the genres of the movie via props.
But I cannot apply substring and map methods to these properties that come to me in the MovieInfo component with props.
class Movie extends Component {
state = {
movie: [],
loading: false,
actors: [],
directors: [],
visible : 6 // This state is for how many actors rendered.
}
componentDidMount() {
this.setState({
loading: true
})
let moviesEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}?api_key=${API_KEY}&language=tr`
let creditsEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}/credits?api_key=${API_KEY}`;
this.getMovieWithId(moviesEndPoint);
this.getDirectorsAndActors(creditsEndPoint);
}
getMovieWithId = moviesEndPoint => {
fetch(moviesEndPoint)
.then(response => response.json())
.then((movie) => {
// console.log(movie);
if (movie.overview !== "" && !movie.status_code) {
this.setState({
movie,
loading: false
})
}
else { // if have not turkish overview fetch this
let engEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}?api_key=${API_KEY}`
fetch(engEndPoint)
.then(response => response.json())
.then((movie) => {
this.setState({
movie
})
})
}
})
}
getDirectorsAndActors = creditsEndPoint => {
fetch(creditsEndPoint)
.then(response => response.json())
.then((credits) => {
// console.log(credits)
const filterDirector = credits.crew.filter(person => person.job === "Director"); // filter directors from all employees
// console.log(filterDirector)
this.setState({
actors: credits.cast,
directors: filterDirector[0].name,
loading: false
})
})
}
render() {
const { movie, loading, actors, directors, visible } = this.state
const { location } = this.props
return (
<>
{
loading ? <Spinner /> : null
}
{this.state.movie ?
<MovieInfo
movieInfo={movie}
actors={actors}
directors={directors}
searchWord={location.searchWord}
visible = {visible}
loadMore = {this.loadMore}
loading = {loading}
/> : null
}
{
!actors && !loading ? <h1>Film Bulunamadı! </h1> : null
}
</>
)
}
}
This is the non-working code inside my MovieInfo component and my errors like this :
TypeError: Cannot read property 'substring' of undefined
TypeError: Cannot read property 'map' of undefined
const MovieInfo = ({ movieInfo, searchWord, directors, actors, visible, loadMore, loading }) => {
const editReleaseDate = (date) => { //? Idk why doesn't work !
// return date.substring(5).split("-").concat(date.substring(0,4)).join("/")
return date
// console.log(date)
// return date;
}
return (
<Col sm={5} className="movieInfo p-4 animated fadeInRightBig">
<p className = "movie-title" > {movieInfo.title} </p>
<h5 className = "mb-4 text-warning">Yayınlanma Tarihi: <span className = "text-light">{editReleaseDate(movieInfo.release_date)}</span></h5>
<h5 className = "text-warning">Açıklama</h5>
<p>{movieInfo.overview} </p>
<ProgressBar label={`IMDB: ${movieInfo.vote_average}`} animated now = {`${movieInfo.vote_average}`} min={0} max={10} />
<h5 className = "text-warning mt-3">Türü:
{ //? Idk why doesn't work !
// movieInfo.genres.map((genre, i) => {
// return <span key = {i} >{genre.name}</span>
// })
}
</h5>
<h5 className ="mt-2 text-warning">Yönetmen: <span className = "text-light">{directors} </span> </h5>
<div> <i className="fas fa-film fa-5x"></i> </div>
</Col>
)
You have 1 loading state for 2 api calls so once one is finishing it is telling the component that it is done loading even if the second has finished. I split it up into 2 different loading states, loadingMovie & loadingActors.
class Movie extends Component {
state = {
movie: [],
loadingMovie: false,
loadingActors: false,
actors: [],
directors: [],
visible : 6 // This state is for how many actors rendered.
};
componentDidMount() {
this.setState({
...this.state,
loadingMovie: true,
loadingActors: true,
});
let moviesEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}?api_key=${API_KEY}&language=tr`;
let creditsEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}/credits?api_key=${API_KEY}`;
this.getMovieWithId(moviesEndPoint);
this.getDirectorsAndActors(creditsEndPoint);
}
getMovieWithId = moviesEndPoint => {
fetch(moviesEndPoint)
.then(response => response.json())
.then((movie) => {
// console.log(movie);
if (movie.overview !== "" && !movie.status_code) {
this.setState({
movie,
loadingMovie: false
})
}
else { // if have not turkish overview fetch this
let engEndPoint = `${BASE_URL}/movie/${this.props.match.params.movieId}?api_key=${API_KEY}`
fetch(engEndPoint)
.then(response => response.json())
.then((movie) => {
this.setState({
movie,
loadingMovie: false
});
})
}
})
}
getDirectorsAndActors = creditsEndPoint => {
fetch(creditsEndPoint)
.then(response => response.json())
.then((credits) => {
// console.log(credits)
if (credits && credits.crew) {
const filterDirector = credits.crew.filter(person => person.job === "Director"); // filter directors from all employees
// console.log(filterDirector)
this.setState({
actors: credits.cast,
directors: filterDirector[0].name,
loadingActors: false
})
} else {
console.log('bad credits');
}
})
}
render() {
const { movie, loadingActors, loadingMovie, actors, directors, visible } = this.state;
const { location } = this.props;
return (
<>
{loadingActors || loadingMovie ? <Spinner /> :
(movie.length && actors.length) ?
<MovieInfo
movieInfo={movie}
actors={actors}
directors={directors}
searchWord={location.searchWord}
visible = {visible}
loadMore = {this.loadMore}
loading = {(loadingActors || loadingMovie)}
/> : null
}
{
actors.length && !loadingActors ? <h1>Film Bulunamadı! </h1> : null
}
</>
);
}
}

how to add multiple objects in reactjs?

I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues

How to pass the dynamic created props to the component?

I am new to the react js. Here , I have a table which has checkbox functionality.In this I am creating a dynamic state and I want to pass that to the child component where the actual table is
My code is like ,
JobList.js
class JobList extends React.Component {
constructor(props) {
this.state = {
isCheckd: false
}
handleCheckBox = () => {
this.setState({
isCheckd: !this.state.isCheckd
}, () => {
this.props.jobs.forEach((item) => this.setState({ [item.jdName]: this.state.isCheckd }))
});
}
handleTableCheckboxChange = (e) => {
this.setState({
[e.target.name]: e.target.checked
}, () => {
const uncheckedItems = this.props.jobs.filter((item) => !this.state[item.jdName])
this.setState({
isCheckd: uncheckedItems.length === 0 ? true : false
});
});
}
return() (
<table className="table" id="actions-table">
<tbody>
<tr>
<td className="text-right mr-1"><input type="checkbox" checked={this.state.isCheckd} onChange={this.handleCheckBox} />
</td> </tr></tbody></table>
}
<UserJobsTabel
jobList={filteredList}
sortAscending={this.sortData}
sortCountAndScoreAscending={this.sortNumbersAscending}
addNewRow={this.addNewRow}
isRowAddingEditorVisible={this.props.isRowAddingEditorVisible}
removeRow={this.removeRow}
checked={this.state.isCheckd}
handleTableCheckboxChange={this.handleTableCheckboxChange} />
}
const mapStateToProps = (state) => {
return {
jobs: state.UserJobs.jobList,
}
}
Now, In this code I am trying to check and uncheck the checkboxes.
this.state[item.jdName] so this state is getting generated for the each element in the array.
Now, I want to pass it to the UserJobsTable .
const UserJobsTable = (props) => {
return (
<tbody className="text-center">
{props.isRowAddingEditorVisible && <RowAddingEditor removeRow={props.removeRow} />}
{props.jobList && props.jobList && props.jobList.length > 0 && props.jobList.map((item, key) => {
return (
<tr key={key}>
<td align="center"> <input type="checkbox" name={props.key} checked={props[item.jdName]} onChange={props.handleTableCheckboxChange} /></td></tr></tbody> ) }
handleTableCheckboxChange = (e) => {
this.props.jobs.filter((item) => this.setState(prevState => ({
dynamicProp: {
...prevState.dynamicProp,
[item.jdName]: e.target.name === [item.jdName] ? true : false
}
})
))
}
I want to set it to the checkbox . So, I am not getting a way through which I can pass this state. is there any way I can do this ?
You could wrap the dynamic property under a key and pass it.
this.state = {
isCheckd: false,
dynamicProp: {}
}
handleCheckBox = () => {
this.setState({
isCheckd: !this.state.isCheckd
}, () => {
this.props.jobs.forEach((item) =>
this.setState(prevState => {
dynamicProp: {
...prevState.dynamicProp,
[item.jdName]: prevState.isCheckd
}
})
)
});
}
In all your setState, you do
this.setState(prevState => {
dynamicProp: {
...prevState.dynamicProp,
[item.jdName]: prevState.isCheckd
}
})
and to pass it to the child accordingly.

React Changing props of component and not render each time

I am learning React and I am trying to use a component to show details of a selected item in a table, my problem is that when I click Next in the table (it is paginated), the state got updated and re-render the component and if I do click many times, it enqueue and the details component is changing between all the items I did click on.
I don't know if there is a good practice for doing this kind of things, i tried to search for something like this but nothing yet.
The component I am using (using public API):
import React from 'react';
class Pokemon extends React.Component {
constructor() {
super();
this.state = {
name: "",
abilities: [],
stats: [],
weight: 0,
height: 0,
base_experience: 0,
};
}
fetch_pokemon = () => {
fetch(this.props.pokemon_url)
.then(res => res.json())
.then(res => {
this.setState({
name: res.name,
abilities: res.abilities,
stats: res.stats,
weight: res.weight,
height: res.height,
base_experience: res.base_experience,
});
});
}
render() {
if(this.props.pokemon_url !== ''){
this.fetch_pokemon();
return (
<div>
<h1>{this.state.name}</h1>
Weight: {this.state.weight}<br />
Height: {this.state.height}<br />
Abilities:
<ul>
{this.state.abilities.map(({ability}, index) => {
return (<li key={index}>{ability.name}</li>);
})}
</ul>
</div>
);
}
else{
return (<h3>Choose one pokémon from the list...</h3>);
}
}
}
export default Pokemon;
My main component:
import React, { Component } from 'react';
//import logo from './logo.svg';
import './App.css';
import Pokemon from './Pokemon';
class App extends Component {
constructor() {
const i_pokemons = 25;
super();
this.state = {
pokemons: [],
pokemons_list: [],
pokemon_show_list: [],
p_init: 0,
p_final: i_pokemons,
pagination: i_pokemons,
pages: 0,
status: '',
enable_buttons: false,
current_page: 0,
current_pokemon_url: '',
URL: `https://pokeapi.co/api/v2/pokemon/?limit=99999`
};
}
componentDidMount(){
this.fetch_pokemons();
}
prev(){
this.setState({
current_page: this.state.current_page - 1,
p_init: this.state.p_init - this.state.pagination,
p_final: this.state.p_final - this.state.pagination
}, () => {
this.fetch_new_page();
});
}
next(){
this.setState({
current_page: this.state.current_page + 1,
p_init: this.state.p_init + this.state.pagination,
p_final: this.state.p_final + this.state.pagination
}, () => {
this.fetch_new_page();
});
}
fetch_new_page = () => {
const current_id = (this.state.current_page - 1) * this.state.pagination;
this.setState({
pokemon_show_list: this.state.pokemons_list.slice(current_id, current_id + 25)
});
this.fetch_pokemons();
}
fetch_pokemons = callback => {
this.setState({
status: 'Waiting for the server and retrieving data, please wait...',
enable_buttons: false
});
return new Promise((resolve, reject) => {
fetch(this.state.URL)
.then(res => res.json())
.then(res => {
if(!res.detail){
this.setState({
pokemons: res,
pokemons_list: res.results,
enable_buttons: true,
status: 'Done',
pokemon_show_list: res.results.slice(this.state.p_init, this.state.p_final)
});
if(this.state.pages === 0){
this.setState({
pages: Math.round(this.state.pokemons_list.length / this.state.pagination),
current_page: 1
});
}
resolve(true);
}else{
reject("Error");
this.setState({status: `Error`});
}
})
.catch(error => {
this.setState({status: `Error: ${error}`});
reject(error);
});
});
}
showPokemon({url}){
this.setState({
current_pokemon_url: url
});
}
render() {
console.log("Render");
return(
<div className="general">
<div className="pokemons-info">
{this.state.status !== '' && this.state.status}
<br />
<table className="pokemon-list">
<thead>
<tr>
<th>Name</th>
<th>More info.</th>
</tr>
</thead>
<tbody>
{this.state.pokemon_show_list.map((pokemon, index) => {
return (
<tr className="l" key={index}>
<td>{pokemon.name}</td>
<td><a className="btn btn-secondary" onClick={this.showPokemon.bind(this, pokemon)} href={`#${pokemon.name}`}>More info.</a></td>
</tr>
);
})}
</tbody>
</table>
<button className="btn btn-primary" disabled={this.state.current_page <= 1} onClick={this.prev.bind(this)}>Prev</button>
Page: {this.state.current_page} of {this.state.pages}
<button className="btn btn-primary" disabled={this.state.current_page === this.state.pages} onClick={this.next.bind(this)}>Next</button>
</div>
<Pokemon pokemon_url={this.state.current_pokemon_url}/>
</div>
);
}
}
export default App;
Feel free for giving any advice
I refactored and clean your code a little bit, but I guess the code below aims what you are looking for. (Read more about functional component 'logicless components').
const API = 'https://pokeapi.co/api/v2/pokemon/';
const PAGE_SIZE = 25;
function Status(props) {
return (
<div>{props.value}</div>
)
}
function Pagination(props) {
return (
<div>
<button
onClick={props.onPrevious}
disabled={props.disabled}>
Prev
</button>
<button
onClick={props.onNext}
disabled={props.disabled}>
Next
</button>
</div>
)
}
function Pokemon(props) {
return (
<div>
<h1>{props.pokemon.name}</h1>
Weight: {props.pokemon.weight}<br />
Height: {props.pokemon.height}<br />
Abilities:
<ul>
{props.pokemon.abilities.map(({ability}, index) => {
return (<li key={index}>{ability.name}</li>);
})}
</ul>
</div>
)
}
function PokemonTable (props) {
return (
<table className="pokemon-list">
<thead>
<tr>
<th>Name</th>
<th>More info.</th>
</tr>
</thead>
<tbody>
{props.children}
</tbody>
</table>
);
}
function PokemonRow (props) {
return (
<tr>
<td>{props.pokemon.name}</td>
<td>
<a href="#" onClick={() => props.onInfo(props.pokemon)}>
More info.
</a>
</td>
</tr>
);
}
class App extends React.Component {
state = {
pokemons: [],
detailedPokemons : {},
loading: false,
status : null,
previous : null,
next : null
}
componentDidMount () {
this.getPokemons(`${API}?limit=${PAGE_SIZE}`)
}
request(url) {
return fetch(url)
.then(blob => blob.json());
}
getPokemons (url) {
this.setState(state => ({
...state,
loading : true,
status : 'Fetching pokemons...'
}));
this.request(url)
.then(response => {
console.log(response)
this.setState(state => ({
...state,
previous : response.previous,
next : response.next,
pokemons : response.results,
loading : false,
status : null
}))
})
.catch(err => {
this.setState(state => ({
...state,
loading : false,
status : 'Unable to retrieved pockemons'
}));
});
}
getPokemonDetail (pokemon) {
const { detailedPokemons } = this.state;
const cachePokemon = detailedPokemons[pokemon.name];
if (cachePokemon !== undefined) { return; }
this.setState(state => ({
...state,
loading : true,
status : `Fetching ${pokemon.name} info`
}));
this.request(pokemon.url)
.then(response => {
this.setState(state => ({
...state,
loading: false,
status : null,
detailedPokemons : {
...state.detailedPokemons,
[response.name]: {
name: response.name,
abilities: response.abilities,
stats: response.stats,
weight: response.weight,
height: response.height,
base_experience: response.base_experience
}
}
}))
})
.catch(err => {
console.log(err)
this.setState(state => ({
...state,
loading : false,
status : 'Unable to retrieved pockemons'
}));
});
}
renderPokemons () {
const { pokemons } = this.state;
return pokemons.map(pokemon => (
<PokemonRow
pokemon={pokemon}
onInfo={this.handleView}
/>
));
}
renderDetailPokemons () {
const { detailedPokemons } = this.state;
return (
<ul>
{Object.keys(detailedPokemons).map(pokemonName => (
<li key={pokemonName}>
<Pokemon pokemon={detailedPokemons[pokemonName]}/>
</li>
))}
</ul>
)
}
handleView = (pokemon) => {
this.getPokemonDetail(pokemon);
}
handlePrevious = () => {
const { previous } = this.state;
this.getPokemons(previous);
}
handleNext = () => {
const { next } = this.state;
this.getPokemons(next);
}
render () {
const { loading, detailedPokemons, status, next, previous } = this.state;
return (
<div className='general'>
<div className="pokemons-info">
{ status && <Status value={status} /> }
<PokemonTable>
{this.renderPokemons()}
</PokemonTable>
<Pagination
disabled={loading}
onPrevious={this.handlePrevious}
onNext={this.handleNext}
/>
{
Object.keys(detailedPokemons).length > 0 &&
this.renderDetailPokemons()
}
</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector('#app')
);

Categories

Resources