How to set initial state dynamically? - reactjs

My initial state varies depending on how many items I get from the API call.
So basically sometimes it looks like this:
class MyComponent extends Component {
state = {
activeItem0: '',
activeItem1: '',
activeItem2: '',
activeItem3: '',
}
...
and some times it might look like this depending on the data returned by the database.
class MyComponent extends Component {
state = {
activeItem0: '',
activeItem1: '',
activeItem2: '',
activeItem3: '',
activeItem4: '',
activeItem5: '',
}
...
Is there a way to set the initial state keys dynamically?
here's what I have so far:
class MyComponent extends Component {
state = {
activeItem0: '',
activeItem1: '',
activeItem2: '',
activeItem3: ''
}
// Set the value for each product on the store
handleItemClick = (e, { name, children }) => this.setState({[e.target.name]: children })
let buttonGroup = _.times(products.length, i => (
<Button.Group>
<Button
name={`activeItem${i}`}
active={this.state[`activeItem${i}`] === 'Val1'}
onClick={this.handleItemClick}
>
Val1
</Button>
<Button
name={`activeItem${i}`}
active={this.state[`activeItem${i}`] === 'Val2'}
onClick={this.handleItemClick}>
Val2
</Button>
<Button
name={`activeItem${i}`}
active={this.state[`activeItem${i}`] === 'Val3'}
onClick={this.handleItemClick}>
Val3
</Button>
</Button.Group>
)
render() {
return(
<div>
<Grid container>{selectSizeInSideBar}</Grid>
</div>
)
}
}
So the issue is that if products.length returned by the DatabBase is 4
I should have in my initial state like this
state = {
activeItem0: '',
activeItem1: '',
activeItem2: '',
activeItem3: ''
}
but if my products.length returned by the DatabBase is 6 then
my initial state should look like this:
state = {
activeItem0: '',
activeItem1: '',
activeItem2: '',
activeItem3: ''
activeItem4: ''
activeItem5: ''
}

You can make a method that initialize your state after your component is ready ( let's say the data of your API has been received ), so its preferrable to make this in the componentDidMount.
const initializeStateForKeys = ( products ) => {
const fakeState = {};
_.each( products, product => {
fakeState[ product.id ] = { id: product.id ,isActive: product.isActive }
} );
return fakeState
}
and you should use it like this:
componentDidMount(){
API.fetchMyProducts()
.then( response => {
this.setState( { products: response.data.products }
, () => {
this.initializeStateForKeys( this.state.products);
});
});
}

Related

How do I get a component to "listen" to changes in global state in a sibling component?

I'm building a wordle clone. I've structured it so that the keypad and the letter display grid are two separate components, Keypad.js and Row,js respectively. Project structure is as follows:
src
-components
|-Grid.js
|-Keypad.js
|-Row.js
-App.js
-AppContex.js
-index.js
When a user enters a letter on the keypad, initially I want that letter to appear in the first index of the first row, as per the game of wordle. How do I get Row.js to "listen" to changes in Keypad.js, so that when a user enters a letter, it shows up in the corresponding index in the grid row?
My approach so far has been to create global state using the Context API, where I've made an empty grid to share to the entire app:
AppContext.js
import { createContext } from "react";
const guessRows = [
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', '']
]
export const AppContext = createContext()
const AppContextProvider = (props) => {
return(
<AppContext.Provider value = {guessRows}>
{props.children}
</AppContext.Provider>
)
}
export default AppContextProvider
In Keypad.js, the letter the user enters is used to update the context (or at least that's what I think it's doing):
import { useContext,useState } from "react"
import { AppContext } from "../AppContext"
const Keypad = () => {
const guessRows = useContext(AppContext);
let currentRow = 0;
let currentTile = 0;
const letters = [
"Q",
"W",
"E",
"R",
// etc
];
const handleClick = (letter) => {
guessRows[currentRow][currentTile] = letter;
currentTile++;
console.log("guess rows", guessRows);
};
return (
<div className="keyboard-container">
{letters.map((letter, index) => {
return (
<div className="key" key={index} onClick={() => handleClick(letter)}>
{letter}
</div>
);
})}
</div>
);
};
export default Keypad;
...then in Row.js, I'm looping through the context and rendering the rows:
Row.js
import { useContext } from "react";
import { AppContext } from "../AppContext";
const Row = () => {
const rowData = useContext(AppContext)
const currentRow = rowData[0]
return (
<div className="row">
{currentRow.map((letter,index) => {
return(
<div className="tile" id = {index}>{letter}</div>
)
})}
</div>
)
}
export default Row;
Unsurprisingly this isn't working, so any suggestions would be appreciated.
Your guessRows should be put into the context state, so that it can have a state setter passed down - then instead of doing guessRows[currentRow][currentTile] = letter;, call the state setter. Similarly, currentTile++; should be replaced with a state update, since this is React - the view should flow from the state.
const AppContextProvider = (props) => {
const [guessRows, setGuessRows] = useState([
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', '']
]);
return(
<AppContext.Provider value = {{ guessRows, setGuessRows }}>
{props.children}
</AppContext.Provider>
);
};
const { guessRows, setGuessRows } = useContext(AppContext);
const [currentRow, setCurrentRow] = useState(0);
const [currentTile, setCurrentTile] = useState(0);
const handleClick = (letter) => {
setGuessRows(
guessRows.map((row, i) => i !== currentRow ? row : (
row.map((item, j) => j === currentTile ? letter : item)
))
);
setCurrentTile(currentTile + 1);
};
And then when the state setter is called, the components will re-render, including Row, which will show the changes made.

Cannot read property 'files' of undefined for sending multiple images

Code
class Add_Give_Item_Form extends Component {
constructor(props) {
super(props);
this.state = {
// #インプット情報用
info: {
name: '',
owner: '',
keyword1: '',
keyword2: '',
keyword3: '',
bland: '',
state: '未使用、新品',
category: '',
images: [],
detail: '',
},
// Validation用
//  urlは必須項目ではないのでValidationには含めない
message: {
name: '',
keyword1: '',
keyword2: '',
keyword3: '',
state: '',
category: '',
detail: '',
},
allCategory: null,
allBland: null,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleImageSelect = this.handleImageSelect(this);
}
////
...
////
handleChange = (e) => {
const name = e.target.name;
const value = e.target.value;
const { info, message } = this.state;
this.setState({
info: { ...info, [name]: value },
});
this.setState({
message: { ...message, [name]: this.validator(name, value) },
});
};
handleImageSelect = (e) => {
this.setState({
info: { ...this.state.info, images: [...this.state.info.images, e.target.files] },
});
};
render() {
const { info, message, allCategory, allBland } = this.state;
// setStateが完了するまではnullにする。
if (this.state.allCategory === null || this.state.allBland === null) {
return <CircularProgress />;
} else {
return (
<div>
///////
.....
///////
<label>Images</label>
<input type="file" multiple onChange={this.handleImageSelect} />
What I want to do
I would like to catch each file sent by a user and put into state as this.state.info.images which is an array.
I saw some questions on stackoverflow and then I found some solutions. When I wrote the same code as what I saw, I got an error like below.
cannot read property files of undefined
I should write the same code but I got the error for some reasons.
I may take another way to realize what I want to do, but I want to write readable codes and figure out why it is happening.
I would like you to teach me why this happens and solutions.
Thank you very much.
I just notice I didn't put bind with this.handleImageSelect = this.handleImageSelect(this).
Now it works well.
Thank you very much.

Pass state to page on form submit?

I have a state on one page, and when i submit a form I'd like to redirect to another page with the state of the source page, and populate the state on the new page with the old state on the old page haha if that makes sense. Hopefully my code attempt can explain it better.
class NewFormDetails extends Component {
constructor(props) {
super(props);
this.state = {
language: this.props.language,
siteName: '',
counties: '',
siteAddress: '',
siteEmail: '',
siteNumber: '',
siteCat: '',
openTimes: '',
fees: '',
access: '',
gps: '',
w3w: '',
txtHeader: '',
txtContent: '',
isLoading: false
};
}
validateForm() {
if (this.state.siteName != '' &&
this.state.siteAddress != '' &&
this.state.siteEmail != '' &&
this.state.siteNumber != '' &&
this.state.openTimes != '' &&
this.state.fees != '' &&
this.state.access != '' &&
this.state.gps != '' &&
this.state.w3w != '' &&
this.state.txtHeader != '' &&
this.state.txtContent != '') {
return true;
} else {
return false;
}
}
handleChange = e => {
this.setState({ ...this.state, [e.target.name]: e.target.value });
console.log(this.state);
}
handleSubmit = event => {
event.preventDefault();
/* try {
await this.createSiteDetails({
siteName: this.state.siteName,
siteAddress: this.state.siteAddress,
siteCounty: this.state.counties,
siteNumber: this.state.siteNumber,
siteEmail: this.state.siteEmail,
siteCategory: this.state.siteCat,
siteOpeningTimes: this.state.openTimes,
siteFees: this.state.fees,
siteAccess: this.state.access,
siteGPS: this.state.gps,
siteW3W: this.state.w3w,
siteHeaderText: this.state.txtHeader,
siteContentText: this.state.txtContent
});
this.props.history.push("/");
} catch (e) {
alert(e);
this.setState({ isLoading: false });
} */
console.log(this.state);
this.props.history.push({
pathname:"/newSite/tours",
state:{
value: this.state
}
});
}
And here is the page i redirect to and attempt to get the state from the other page
class NewFormTours extends Component {
constructor(props) {
super(props);
this.state = {
language: this.props.language,
tourName: '',
waypoints: '',
duration: '',
toursTxtHeader: '',
toursTxtContent: '',
siteName: this.siteName,
counties: this.props.counties,
siteAddress: this.props.siteAddress,
siteEmail: this.props.siteEmail,
siteNumber: this.props.siteNumber,
siteCat: this.props.siteCat,
openTimes: this.props.openTimes,
fees: this.props.fees,
access: this.props.access,
gps: this.props.gps,
w3w: this.props.w3w,
detailsTxtHeader: this.props.detailsTxtHeader,
detailsTxtContent: this.props.detailsTxtContent,
};
You can use react router to pass data to new component.
Below discussion should be helpful.
How do i pass state through React_router?

ReactJS - setting an inline style equal to a property on state is not working. What's going on?

I'm trying to get a FormWarning to display when users input incorrect information, but it seems to have disappeared on me. I'm trying to control whether or not it displays with this.state.formWarning.display - when the validateInputs function runs, if it determines an input is invalid, it should change the value of display from 'none' to 'block'. I'm trying to set the style for the Component to having a display that matches this.state.formWarning.display, but I am getting an error. Is my belief that you can set the styles for a component inline via an object not correct? Getting bugs regardless. ie
export default class FormOne extends React.Component {
constructor(props) {
super(props)
this.state = {
formOne: {
shippingAddress: {
firstName: '',
lastName: '',
address1: '',
city: '',
state: '',
zip: '',
country: 'US'
},
phone: '',
email: ''
},
formWarning: {
text: '',
invalidInputID: '',
display: 'block'
},
isSubmitted: false,
submitting: false
}
this.styles = this.props.styles || {}
}
componentWillReceiveProps(nextProps) {
if(nextProps.state.stepOne &&
nextProps.state.stepOne.formOneResponse) {
let formOneResponse = nextProps.state.stepOne.formOneResponse
formOneResponse.status === "delayed" || formOneResponse.status === "success"
? this.setState({isSubmitted: true})
: alert(formOneResponse.errorMessage)
this.setState(state => ({submitting: false}))
}
}
validateInputs = (inputs) => {
let { email, phone, shippingAddress } = inputs,
shippingKeys = Object.keys(shippingAddress)
console.log('validate inputs is firing')
for(let i = 0; i < Object.keys(shippingAddress).length; i++) {
let key = shippingKeys[i], input = shippingAddress[key]
if(!input) {
return this.showFormWarning(key)
}
}
if(!phone) return this.showFormWarning('phone')
if(/\S+#\S+\.\S+/.test(email)) return
this.showFormWarning('email')
return true
}
showFormWarning = key => {
clearTimeout(this.warningTimeout)
console.log('showformwarnign is firing')
this.setState(state => ({
formWarning: {
...state.formWarning,
text: 'Please fill out this field',
invalidInputID: key,
display: 'block'
}
}))
this.warningTimeout = setTimeout(() => {
this.setState(state => ({
formWarning: {
...state.formWarning,
display: 'none'
}
}))
}, 5000)
return false
}
saveInputVal = (event) => {
let { formOne: tempFormOne } = this.state,
input = event.currentTarget
console.log('saveinputvals is firing')
if(input.name === 'phone' || input.name === 'email') {
this.setState(state => ({
formOne: {
...state.formOne,
[input.name]: input.value
}
}))
} else {
this.setState(state => ({
formOne: {
...state.formOne,
shippingAddress: {
...state.formOne.shippingAddress,
[input.name]: input.value
}
}
}))
}
}
submit = (event) => {
event.preventDefault()
if(!this.validateInputs(this.state.formOne)) return
this.setState(state => ({submitting: true}))
this.props.saveShippingData(this.state.formOne)
this.props.stepOneSubmit(this.state.formOne)
}
render() {
if (this.state.isSubmitted) return <Redirect to="/order" />
let CustomTag = this.props.labels ? 'label' : 'span',
{ inputs, saveInputVal, styles, state } = this,
{ formWarning, submitting } = state,
{ invalidInputID, text, display } = formWarning
return (
<div style={this.styles.formWrapper}>
{
typeof this.props.headerText === 'string'
? ( <h2 style={this.styles.formHeader}>
{this.props.headerText}</h2> )
: this.props.headerText.map((text) => {
return <h2 key={text} style={this.styles.formHeader}
className={'header'+this.props.headerText.indexOf(text)}>{text}</h2>
})
}
<form onSubmit={this.submit} style={this.styles.form}>
<FormOneInputs inputs={inputs} saveInputVal={saveInputVal}
CustomTag={CustomTag} styles={styles} />
<button style={this.styles.button}>{this.props.buttonText}
</button>
</form>
<Throbber throbberText='Reserving your order...' showThrobber=
{submitting} />
<FormWarning style={display: {this.state.formWarning.display}} invalidInputID={invalidInputID} text={text}/>
</div>
)
}
}
You don't need to set any CSS class. The approach is as follows:
(1) Given a component you want to render or not render depending on a variable
(2) Make a helper method that checks for the condition and returns the actual component if you want it rendered. Otherwise, do nothing (basically returns undefined)
(3) Call that method from wherever you want the component to possibly appear.
Concrete example:
class FormOne extends React.Component {
// (...) all other things omitted to focus on the problem at hand
renderFormWarning() {
if (formIsInvalid) {
return <FormWarning ... />;
}
// else won't do anything (won't show)
}
render() {
return (
{/* ... */}
{this.renderFormWarning()}
);
}
}
In the above example, replace formIsInvalid with some statement that will tell you if the form is invalid. Then, if that condition is true, it will return the FormWarning component. Otherwise, no form warning will be shown. From the render() method, all you need do is call that helper method.

Page load errors due to missing this.setState

I'm getting an error when I populate my form with stored data. My form contains an array so I'm using {this.state.careerHistoryPositions.map((careerHistoryPosition) to create a loop. The error comes from {careerHistoryPosition.errors['company']. This part of the form, is related to errors when the form is submited and I don't store this in the database so when the form is populated, errors isn't set. I assume it's to do with this.setState looking for something that doesn't exist.
Snippet: Constructor
constructor(props) {
super(props);
let uniqueId = moment().valueOf();
const profileCandidateCollection = props.profileCandidate;
const profileCandidateCollectionId = profileCandidateCollection._id;
const careerHistoryPositions = profileCandidateCollection && profileCandidateCollection.careerHistoryPositions;
this.state = {
careerHistoryPositions: careerHistoryPositions || [
{
company: '',
uniqueId: uniqueId,
title: '',
description: '',
startDateMonth: '',
startDateYear: '',
startDateMonth: '',
endDateYear: '',
isCurrent: false,
isDisabled: false,
errors: {}
}
],
profileCandidateCollectionId: profileCandidateCollectionId || null
};
}
Snippet: render
{this.state.careerHistoryPositions.map((careerHistoryPosition) => (
<div key={careerHistoryPosition.uniqueId} className="individual-position">
<SingleInput xs={9} inputType={'text'} controlFunc={this.handleCompanyNameChange(careerHistoryPosition.uniqueId)} content={careerHistoryPosition.company} placeholder={'Company'} bsSize={null}/>
{careerHistoryPosition.errors['company']
? <Col sm={12} className="has-error">
<span className="help-block custom-error">{careerHistoryPosition.errors['company']}</span>
</Col>
: ''}
</div>
))}

Resources