React conditional subset of state element - javascript

I have a set of results from an api, stored in the state as the array 'results'.
I want a second array, 'visible_results' in the state. This should be a subset of 'results'. I'm trying to do this like this:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: null,
visible_results: null,
low_price: null,
high_price: null,
min_price: null,
max_price: null
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100
});
this.setState({
low_price: this.state.min_price,
high_price: this.state.max_price
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
this.setState({
visible_results: this.state.results.reduce((items, x) => {
if (this.state.low_price <= x.price && x.price <= this.state.high_price)
items.push(x);
return items;
})
});
};
The handleChange is tied to two sliders, one setting low_price, and one setting high_price. The function should then generate a subset of results, based on the new value of low_price or high_price, and save it to the state as visible_results.
It doesn't work. There are no errors, but visible_results always remain 'null'. The sliders definitely work. I've tried replacing the if statement with if (1==1) to make sure that it wasn't just an if statement typo. It did the same thing.

A few things:
Setting the values to null makes your code more complicated, I would either add a useful default value, e.g. min_price: 0, max_price: Infinity or just don't initialize it.
this.setState is asynchronous! If you call setState twice, the first call will be deferred, so this.state isn't yet update in the second call, so e.g. this.state.low_price inside the second call in handleChange wasn't yet updated.
If you don't pass an initial value to reduce, it will take the arrays first element, which in your case is an object, calling push on this won't work. You probably want .reduce(fn, []), but in your case .filter is actually more appropriate.
visible_results doesn't have to be part of the state as it is determined by other state props, so just determine it on render
Code:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
low_price: 0,
high_price: Infinity,
min_price: 0,
max_price: Infinity
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100,
min_price: 1,
high_price: 100,
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
}
render() {
const { results, low_price, high_price } = this.state;
const visibleResults = results.filter(item =>
low_price <= item.price && item.price <= high_price);
//...
}
}

Related

Render() state element is initially empty until api call

I have a class component which should display some list values after an API call, in my render function I call a function to populate some other state list (with specific object properties from the fetched list), the problem is that in the render call, the state values are initially empty and as such the component I return is also just empty.
I've tried using componentDidUpdate() but I dont have much of an idea on how to go about using it, it usually gives me an infinite loop.
Here is my relevant code:
class AdminSales extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
data: [],
};
}
componentDidMount() {
this.fetchData();
}
fetchData() {
fetch("/api/items")
.then((res) => res.json())
.then((items) => this.setState({ items: items }));
}
componentDidUpdate(prevState) {
if (JSON.stringify(prevState.items) == JSON.stringify(this.state.items)) {
// Do nothing
} else {
// This gives infinite loop ...
// this.fetchData();
}
}
populateData() {
this.state.items.forEach(function (item) {
this.state.data.push({
name: item.name,
value: item.quantity,
});
}, this);
}
render() {
// Output shown line: 65
console.log(this.state);
this.populateData();
const { data } = this.state;
return ( ... );
}
}
export default AdminSales;
Any help will be much appreciated.
First of all there are multiple issues in your code
Updating/Mutating state in render
Instead of updating the state using setState, updating the state inplace in populateData method
Also, as #Drew mentioned we don't have to duplicate items into data instead we can store only the required info in the state once after getting the response from the API.
In the meantime while waiting for the response if you want to show a loading info you can do that as well.
Below is the example covering all those points mentioned above.
const mockAPI = () => {
return new Promise((resolve) => setTimeout(() => {
resolve([{id:1, name: "ABC", quantity: 1}, {id: 2, name: "DEF", quantity: 5}, {id: 3, name: "XYZ", quantity: 9}])
}, 500));
}
class AdminSales extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
loading: true
};
}
componentDidMount() {
this.fetchData();
}
fetchData = () => {
mockAPI()
.then((res) => {
this.populateData(res);
});
}
populateData = (data) => {
this.setState({
items: data.map(({name, quantity}) => ({
name,
value: quantity
})),
loading: false
})
}
render() {
//console.log(this.state);
const { items, loading } = this.state;
return loading ? <p>Loading...</p> :
items.map(item => (
<div>{item.name}: {item.value}</div>
));
}
}
ReactDOM.render(<AdminSales />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Note: For simplicity I've mocked the backend API with simple setTimeout.

React state variable updates automatically without calling setState

I am facing the following issue and not able to figure it out.
I have two variables inside the state called userDetails & userDetailsCopy. In componentDidMount I am making an API call and saving the data in both userDetails & userDetailsCopy.
I am maintaining another copy called userDetailsCopy for comparison purposes.
I am updating only userDetails inside setState but even userDetailsCopy is also getting updated instead of have old API data.
Below is the code :
constructor(){
super()
this.state={
userDetails:{},
userDetailsCopy: {}
}
}
componentDidMount(){
// API will return the following data
apiUserDetails : [
{
'name':'Tom',
'age' : '28'
},
{
'name':'Jerry',
'age' : '20'
}
]
resp.data is nothing but apiUserDetails
/////
apiCall()
.then((reps) => {
this.setState({
userDetails: resp.data,
userDetailsCopy: resp.data
})
})
}
updateValue = (text,i) => {
let userDetail = this.state.userDetails
userDetail[i].name = text
this.setState({
userDetails: userDetail
})
}
submit = () => {
console.log(this.state.userDetials) // returns updated values
console.log(this.state.userDetailsCopy) // also return updated values instead of returning old API data
}
Need a quick solution on this.
The problem with this is that you think you are making a copy of the object in state by doing this
let userDetail = this.state.userDetails
userDetail.name = text
But, in Javascript, objects are not copied like this, they are passed by referrence. So userDetail at that point contains the referrence to the userDetails in your state, and when you mutate the userDetail it goes and mutates the one in the state.
ref: https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c
To properly clone the object from the state to your local variable, you need to instead do this:
let userDetail = {...this.state.userDetails}
OR
let userDetail = Object.assign({}, this.state.userDetails)
Always remember, Objects are passed by referrence not value.
EDIT: I didn't read the question properly, but the above answer is still valid. The reason userDetailCopy is being updated too is because resp.data is passed by referrence to both of them, and editing any one of them will edit the other.
React state and its data should be treated as immutable.
From the React documentation:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Here are five ways how to treat state as immutable:
Approach #1: Object.assign and Array.concat
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = Object.assign({}, userDetails[index]);
userDetail.name = text;
const newUserDetails = []
.concat(userDetails.slice(0, index))
.concat(userDetail)
.concat(userDetails.slice(index + 1));
this.setState({
userDetails: newUserDetails
});
}
Approach #2: Object and Array Spread
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = { ...userDetails[index], name: text };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
}
Approach #3: Immutability Helper
import update from 'immutability-helper';
updateValue = (text, index) => {
const userDetails = update(this.state.userDetails, {
[index]: {
$merge: {
name: text
}
}
});
this.setState({ userDetails });
};
Approach #4: Immutable.js
import { Map, List } from 'immutable';
updateValue = (text, index) => {
const userDetails = this.state.userDetails.setIn([index, 'name'], text);
this.setState({ userDetails });
};
Approach #5: Immer
import produce from "immer";
updateValue = (text, index) => {
this.setState(
produce(draft => {
draft.userDetails[index].name = text;
})
);
};
Note:
Option #1 and #2 only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object.
To maintain the userDetailsCopy unchanged you need to maintain the immutability of state (and state.userDetails of course).
function getUserDerails() {
return new Promise(resolve => setTimeout(
() => resolve([
{ id: 1, name: 'Tom', age : 40 },
{ id: 2, name: 'Jerry', age : 35 }
]),
300
));
}
class App extends React.Component {
state = {
userDetails: [],
userDetailsCopy: []
};
componentDidMount() {
getUserDerails().then(users => this.setState({
userDetails: users,
userDetailsCopy: users
}));
}
createChangeHandler = userDetailId => ({ target: { value } }) => {
const { userDetails } = this.state;
const index = userDetails.findIndex(({ id }) => id === userDetailId);
const userDetail = { ...userDetails[index], name: value };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
};
render() {
const { userDetails, userDetailsCopy } = this.state;
return (
<React.Fragment>
{userDetails.map(userDetail => (
<input
key={userDetail.id}
onChange={this.createChangeHandler(userDetail.id)}
value={userDetail.name}
/>
))}
<pre>userDetails: {JSON.stringify(userDetails)}</pre>
<pre>userDetailsCopy: {JSON.stringify(userDetailsCopy)}</pre>
</React.Fragment>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Cannot call the first element of a JSON object

I'm trying to access the first object from data[]. Then, grab the keys using Object.keys() but it gives me this error:
"TypeError: Cannot convert undefined or null to object".
I need the output to be an array of the keys.
import React, { Component } from 'react';
class CodecChart extends Component {
constructor(props) {
super(props);
this.state = {
post: [],
isLoaded: false,
}
}
componentDidMount() {
const url = 'https://jsonplaceholder.typicode.com/users';
fetch(url)
.then(result => result.json())
.then(post => {this.setState({ post: post })
})
}
render() {
const data = this.state.post;
// cannot reach the first object of data[]
var keys = Object.keys(data[0]);
return (
<div>
//output should be an array of the keys
<h5>{keys}</h5>
</div>
)
}
}
export default CodecChart;
The first time you try to access data[0], it's still empty:
this.state = {
post: [],
isLoaded: false,
}
and const data = this.state.post; means that data[0] is undefined.
it's only after the component is mounted, and the state is set correctly that data[0] is defined (or not, depending on what the API returns).
I found a way for it to work by adding another "then" so it can set the "keys" state right after the "posts" state was set. But I wonder if there is another way to make it more elegant. Thank you for trying to help.
constructor(props) {
super(props);
this.state = {
posts: [],
isLoaded: false,
keys: []
}
}
componentDidMount() {
const url = 'https://jsonplaceholder.typicode.com/users';
fetch(url)
.then(result => result.json())
.then(posts => {
this.setState({ posts: posts })
})
.then(_ => { this.setState({ keys: Object.keys(this.state.posts[0]) }) })
}
render() {
const keys = this.state.keys;
return (
<div>
<h5>{keys}</h5>
</div>
)
}

React setState() after fetch not rerendering

I am fetching data in componentDidMount() (I am getting them in the form I want) and I want to save them in the component state with this.setState.
The state is not changing.
I console log that I am getting to the point where setState is called - there are conditions
I tried const that = this
The component is not re-rendering and state is not changing and I would like to know why.
My code:
export class Offers extends Component {
constructor(props) {
super(props);
this.renderOffer = this.renderOffer.bind(this);
this.state = {
...
};
}
componentWillMount() {
this.setState(() => ({
offer: {},
isLoading: true,
isMyOffer: false,
...
}));
}
componentDidMount() {
console.log('MOUNTED');
const { profile } = this.props;
if (profile) {
this.setState(() => ({
isLoading: false
}));
}
if (profile && profile._id) {
this.setState(() => ({
isMyOffer: true,
...
}));
fetch(`/api/offers-by/${profile._id}`,{
method: 'GET'
})
.then(response => response.json())
.then(offers => {
if(!offers || !offers.length) {
this.setState(() => ({
isLoading: false
})
);
} else {
console.log('ELSE', offers[0]._id); // getting proper data
console.log('THIS', this) // getting this object
const offerData = offers[0]
this.setState(() => ({
offer: offerData,
isLoading: false
})) // then
}}) // fetch
console.log('STATE', this.state)
}
console.log('STATE', this.state)
}
setState has a callback method as the second argument.You should use that after the initial setState.This works because setState itself is an asynchronous operation.The setState() method does not immediately update the state of the component but rather if there are multiple setStates, they will be batched together into one setState call.
this.setState(() => ({
isLoading: false
}),() =>{
/// You can call setState again here and again use callback and call fetch and invoke setState again..
});
Ideally you could refactor some of your setStates into a single setState call.Start with an empty object and add properties to your object based on conditons.
const updatedState ={}
if(loading){
updatedState.loading = false
}
if(profile &&..){
updatedState.someProperty = value.
}
this.setState(updatedObject,()=> {//code for fetch..
}) // Using the object form since you don't seem to be in need of previous State.

Best way to update / change state object in react native?

What's the best way to update a nested property deep inside the State object?
// constructor --
this.state.someprop = [{quadrangle: {rectangle: {width: * }}, ...}]
...
I want to update width of the rectangle object.
this.state.quadrangle.rectangle.width = newvalue // isn't working
I could make it work like:
const {quadrangle} = this.state
quadrangle.rectangle.width = newvalue
this.setState = {
quadrangle: quadrangle
}
But this method doesn't sound the best way for performance/memory
// ES6 WAYS TO UPDATE STATE
// NOTE: you MUST use this.setState() function for updates to your state
class Example extends Component {
constructor(props) {
super(props);
this.state = {
name: 'John',
details: {
age: 28,
height: 1.79,
}
}
componentDidMount() {
this.handleChangeName('Snow');
this.handleAgeChange(30);
}
componentDidUpdate() {
console.log(this.state);
/*
returns
{
name: 'Snow',
details: {
age: 30,
height: 1.79,
}
}
*/
}
// this way you keep your previous state immutable (best practice) with
// param "prevState"
handleChangeName = (_name) => {
this.setState(
(prevState) => ({
name: _name
})
)
}
//this is how you update just one property from an internal object
handleAgeChange = (_age) => {
this.setState(
(prevState) => ({
details: Object.assign({}, prevState.details, {
age: _age
})
})
)
}
// this is the simplest way to set state
handleSimpleAgeChange = (_age) => {
this.setState({
details: Object.assign({}, this.state.details, { age: _age })
})
}
render() {
return (
<h1>My name is {this.state.name} and I'm {this.state.details.age} years old</h1>
)
}
}
If you want to keep the best practice without making it harder, you can do:
updateState = (obj) => {
if (obj instance of Object) {
this.setState(
(prevState) => (Object.assign({}, prevState, obj))
);
}
}
usage:
//code ... code ... code ...
handleAgeChange = (_age) => {
this.updateState({
details: Object.assign({}, this.state.details, { age: _age }
})
}
The best way, and the way facebook has proposed, is to use this.setState({someProp: "your new prop"}).
Using it is the only way which is going to guarantee that the component will be rendered correctly.
This function is incremental, so you dont need to set the whole state, just the prop you need.
I strongly recomend you to read the docs here.
If your object is nested make the inner object it's own state,
this.state = {
quadrangle: {this.state.rectangle, ...}
rectangle: {width: * }}
};
then use your clone and replace technique:
const {rectangleNew} = this.state.rectangle;
rectangleNew.width = newvalue;
this.setState({rectangle: rectangleNew});
The state should propagate upwards. Should improve performance if only certain quadrangles need to be updated based on said rectangle. Props down, state up.
with hooks use this way
- setBorder((pre) => { return ({ ...pre, border_3: 2 }) })
example :
// state for image selected [ borderd ]
const [bordered, setBorder] = useState({ border_1: 0, border_2: 0, border_3: 0, border_4: 0, border_5: 0, border_6: 0, border_7: 0, border_8: 0 });
// pre is previous state value
const handle_chose = (y) => {
//generate Dynamic Key
var key = "border_" + y;
setBorder((pre) => { return ({ ...pre, [key]: 2 }) })
}

Categories

Resources