How to store all array values into a single React state? - javascript

I have a config file which contains an array that gets data from redux
"datapath": [
"common.registration.rgsnInfoRs[0].value", // street name
"common.registration.rgsnInfoRs[1].value", // city
"common.registration.rgsnInfoRs[2].value", // state
"common.registration.rgsnInfoRs[3].value" // zip code
]
In my react component, when I tried to render the data from this array it only returns back the data for the zip code. How can I make my component render all of index data in my render method?
constructor(props) {
super(props);
this.state = {
// data: '',
data: []
}
}
componentDidMount(){
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState({data})
})
}
}
// static getDerivedStateFromProps(props, state) {
// if(_.get(props, 'datapath')){
// _.map(props.datapath, (item, i) => {
// let data=_.get(props.state,item);
// state.data = data;
// })
// }
// }
static getDerivedStateFromProps(props, state) {
if(_.get(props, 'datapath')){
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
render() {
const {type, label} = this.props;
return (
type === "grid" ? (
<Grid.Column style={style}>
<strong>{label ? `${label}:` : null}</strong> {this.state.data}
</Grid.Column>
) : (
null
)
)
}

Your initial data type for this.state.data is an array. But in the following line, it gets the value of the item (in this case, it's either street name, city, state or zipcode). These values are string type and override the this.state.data datatype from array to string.
let data=_.get(props.state, `i`); // This is incorrect
You should append the value in the state data instead of assign in componentDidMount
componentDidMount() {
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
Now Let's look at getDerivedStateFromProps function. This is a static function and you can not use this inside a static function. According to the react docs, this function returns the updated state.
static getDerivedStateFromProps(props, state) {
const newData = [];
if(_.get(props, 'datapath')) {
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
newData.push(data);
});
return { data: newData };
}
}

I think that you are not asking the right questions. It is generally a bad idea to store data from props in state because it can lead to stale and outdated data. If you can already access it from props, then you don't need to also have it in state.
Your component receive a prop state which is an object and a prop datapath which is an array of paths used to access properties on that state. This is already weird and you should look into better patterns for accessing redux data through selector functions.
But if we can't change that, we can at least change this component. You can convert an array of paths to an array of values with one line of code:
const data = this.props.datapath.map( path => _.get(this.props.state, path) );
All of the state and lifecycle in your component is unnecessary. It could be reduced to this:
class MyComponent extends React.Component {
render() {
const { type, label = "", state, datapath = [] } = this.props;
const data = datapath.map((path) => _.get(state, path, ''));
return type === "grid" ? (
<Grid.Column style={style}>
<strong>{label}</strong> {data.join(", ")}
</Grid.Column>
) : null;
}
}

I think, when you are looping through the data and setting the state you are overriding the old data present in the state already.
may be you will have to include the previous data as well as set new data in it something like this.
this.setState({data : [...this.state.data, data]})

Related

Catch error films.map is not a function in React

I get the catch error or type error on
{films.map((film, i) => { ...
can not read proper 'map' or undefined from the Rest Api swapi in React.
build id url to show information
Create the component
State to add api data
Binding the function fetchData to this state
Call to api function characters
Call to api function movies
Life circle to call the function
Rendering the component
Bring the data of the character to the data to show them
mapping the list
const api_url_films = 'https://swapi.dev/api/films/'
class Card extends Component {
constructor(props) {
super(props)
this.state = {
films: []
}
this.fetchDataFilms = this.fetchDataFilms.bind(this)
}
fetchDataFilms(){
fetch(api_url_films)
.then(data => (data.json()))
.then(results => {
const api_data = results
this.setState({
films: api_data
})
})
}
componentDidMount() {
this.fetchDataFilms()
}
render() {
const {films} = this.state
return(
<div>
{films.map((film, i) => {
return (
<li key={i}>{film}</li>
)
}
)}
</div>
)
}
}
I try to add in a unordered list also but doesn't work
thanks
I think html is rendered before data is fetched although the films variable is an empty array in initial state.
When films variable is set from fetching function, html will be re-rendered.
try this code. This component will show an empty box until film data is fetched.
hope this works!
render() {
let films = this.state.films
if(films == null || films == []) return <></>
return(
<div>
{films.map((film, i) => {
return (
<li key={i}>{film}</li>
)
}
)}
</div>
)
}
The response has this shape:
{
count: number,
next: any,
previous: any,
results: Object[], // <-- this is what you want
}
Update the handler to access into the response object and save the results array.
fetchDataFilms(){
fetch(api_url_films)
.then(data => (data.json()))
.then(results => {
const api_data = results
this.setState({
films: api_data.results, // <-- access the results array
})
})
}
Additionally, the film objects you are mapping are still object, so they are not valid React children. You will need to access specific properties.
{films.map((film) => {
return (
<li key={film.title}>{film.title}</li> // or director, or producer, etc...
)
}

Why does my Redux props object return undefined?

I am trying to access a value within an object I'm getting from my Redux store.
Here's my Redux state:
I'm setting this on app load like this:
useEffect(() => {
// Update the global state with the app content
getContent().then(res => {
props.updateContent(res);
});
}, []);
with an action:
export const updateContent = content => {
return {
type: "UPDATE",
payload: content
};
};
and a reducer:
const contentReducer = (state = {}, action) => {
switch (action.type) {
case "UPDATE":
return action.payload;
default:
return state;
}
};
export default contentReducer;
However, when I try to log or access the header_section, I get Cannot read property 'header_section' of undefined. Which is strange, because I can log out just the content props object and see it's there.
Here's where I'm trying to access it:
const Header = props => {
console.log("CONTENT-----", props.content.header_section);
// simplified component
return (
<button>{props.content.header_section.cta_buy_ticket}</button>
);
};
const mapStateToProps = state => {
return {
content: state.contentReducer.global_options
};
};
export default connect(mapStateToProps)(Header);
Codesandbox here - https://codesandbox.io/s/thirsty-sid-ov65i?file=/src/App.js
Your mapStateToProps picks contentReducer.global_options to be the content prop.
Your question indicates that you're trying to access contentReducer.header_section by doing content.header_section. In this case, you meant to return state.contentReducer as the content prop from mapStateToProps:
const mapStateToProps = state => {
return {
content: state.contentReducer
};
};
Now you can access props.content.header_options, which refers to the same object as state.contentReducer.header_options in mapStateToProps.
If you still get the error, then contentReducer's initial value is probably undefined. Either set an initial value with the same object shape as the data, or check that the keys you access exist before using them (e.g. if (!props.content || !props.content.header_options) return null)

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>

React component not re-rendering when updating Redux store

i am trying to create a toggle for favouriting a Card Component so that it renders in my Favorites.js component. I am using Redux to store the state but when i dispatch an action to add or remove them from the store the components are not rendering. I think i am mutating the state of the array.
Here's the reducer:
export function rootReducer(state = [], action) {
switch(action.type) {
case 'ADD_FAVORITE':
return state.concat(action.data);
case 'SUB_FAVORITE':
return state.filter(state => state.name !== action.data.name);
default:
return state;
}
}
I tried using Object.assign to a create a new Array but since the data passed into my state is in a array itself, i can't use store.getState() to map them into my component. The array becomes nested within itself.
This is the function that i am running onClick to dispatch the actions:
toggleFavorites(e) {
if
(store.getState().includes(this.props.data))
{
console.log(this.props.data.name + ' removed from favorites');
store.dispatch({type: 'SUB_FAVORITE', data: this.props.data});
}
else{
console.log(this.props.data.name + ' added to favorites');
store.dispatch({type: 'ADD_FAVORITE', data: this.props.data});
}
This.props.data is passed from referencing an array in a JSON object and mapping it into my Card Component
Here's the Card Component that i am rendering:
render() {
let {name, description, url , screenshot} = this.props.data;
return (
<div className="cardComponent">
<div className="CTA-container">
<div className="CTA-wrapper">
<div onClick={() => this.toggleFavorites(this.props.data)}className="CTA-icon"><IconComponent icon="favorite"/></div>
<IconComponent icon="link"/>
</div>
</div>
<img src={screenshot} alt="" className="cardComponent_img"/>
{name}
<p className="cardComponent_description">{description}</p>
</div>
I am rendering these Card Components into the Favorites.js Component like this:
class Favorites extends Component {
constructor(props) {
super(props);
}
render() {
let cardComps = store.getState().map(data => {
return (
<CardComponent data = {data} />
)
})
return (
<div>
<div className="array-component">{cardComps}</div>
export default Favorites;
I am fairly new to React and Redux so i am not sure if i did something wrong when setting up the components. I just need the component to re-render when the user adds or remove it from their Favourites.
Redux do shallow comparison of reference for updated state and based on that decide whether to update component or not.
Both Array#concat and Array#filter return new array with same referenced elements.So,state comparison return false and no rendering happening.
Assuming action.data is a one dimensional array.
This should work.
switch(action.type) {
case 'ADD_FAVORITE':
return [...state,action.data];
case 'SUB_FAVORITE':
return [...state.filter(state => state.name !== action.data.name)]
default:
return state;
}
You also need to use the connect method fro react-redux library to listen to the store updates. For reference I have included the code.
In Reducer
switch(action.type) {
case 'ADD_FAVORITE':
return [...state,action.data];
case 'SUB_FAVORITE':
return [...state.filter(state => state.name !== action.data.name)]
default:
return state;
}
In Favorites.js Components
import { connect } from 'react-redux';
class Favorites extends Component {
constructor(props) {
super(props);
this.state = {
storeData: []
}
}
componentWillReceiveProps(nextProps){
if(nextProps.storeData !== this.state.storeData){
this.setState({
storeData: nextProps.storeData
})
}
}
render() {
const { storeData } = this.state;
let cardComps = storeData.map(data => {
return <CardComponent data = {data} />;
})
return (
<div className="array-component">{cardComps}</div>;
);
}
}
const mapStateToProps = state => {
return {
storeData: state
};
};
const connectedFavorites = connect(mapStateToProps)(Favorites);
export default connectedFavorites;

Getting undefined for the property passed to the child component

I have a parent class component where I am setting the local set like this:
constructor(props) {
super(props);
this.state = {
toogleForms: props.perioder.map((periode, index) => ({ index, open: !periode.bekreftet })),
};
this.removePeriodCallback = this.removePeriodCallback.bind(this);
}
Since, on intial rendering I don't get perioder from the props I am using componentWillReceiveProps to update local state:
componentWillReceiveProps(props) {
const toogleFormsLength = this.state.toogleForms.length;
if (toogleFormsLength < props.perioder.length) {
const addedPeriod = props.perioder
.filter((periode, index) => index >= toogleFormsLength)
.map((periode, index) => ({ index: toogleFormsLength + index, open: !periode.bekreftet }));
this.setState({ toogleForms: this.state.toogleForms.concat(addedPeriod) });
}
if (toogleFormsLength > props.perioder.length) {
const toogleForms = this.state.toogleForms.filter((periode, index) => index < toogleFormsLength - 1);
this.setState({ toogleForms });
}
}
Then, I am sending the toogleForms from the local state to redux-form fieldArray component, like this:
<FieldArray
name="perioder"
component={UttakPeriode}
removePeriodCallback={this.removePeriodCallback}
inntektsmelding={inntektsmelding}
toogleForms={this.state.toogleForms}
toggleFormCallback={this.toggleFormCallback}
/>
But, in the UttakPeriode component where I am receiving this props, I am getting undefined when I am trying to use it:
export const UttakPeriode = ({
fields, inntektsmelding, removePeriodCallback, toggleFormCallback, toogleForms,
}) => (
<div>
{fields.map((fieldId, index) => {
const tilDato = fields.get(index).tom;
const fraDato = fields.get(index).fom;
const { uttakPeriodeType, bekreftet, utsettelseÅrsak } = fields.get(index);
const arbeidsgiverNavn = inntektsmelding[0].arbeidsgiver;
const showForm = toogleForms.filter(el => el.index === index)[0].open;
This is the error:
TypeError: Cannot read property 'open' of undefined in UttakPeriode
(created by ConnectedFieldArray)
I am not sure, but I guess the child component gets rendered before it receives the props, so that's why it is undefined. But, how can I fix this?
Your asking for a lot of states to be in place and available at the same time. I'd just break down that last line from the UttakPeriode function into 2 parts and check to see if there is data available before trying to use the open property.
Replace:
const showForm = toogleForms.filter(el => el.index === index)[0].open;
With:
const form = toogleForms.filter(el => el.index === index)[0];
const showForm = (form) ? form.open : null;
// ...error handle or return if showForm == null
toogleForms is not undefined because you can filter it, so you just get empty array after filtering toogleForms.
Try to console.log(toogleForms.filter(el => el.index === index)) at first to see if it have any elements.

Categories

Resources