I am working on the library project. Users can add books to the library.
So, I have created the form to add a book. The form contains from the name, author, publisher, pages, ISBN and info fields. I have created the dropdown component for authors and publishers, so the user can choose from this component:
import AuthorsService from './AuthorsService'
const authorsService = new AuthorsService();
class AuthorsDropDown extends Component {
constructor(props) {
super(props);
this.state = {
authors: [],
};
}
componentDidMount() {
var self = this;
authorsService.getAuthors().then(function (result) {
self.setState({ authors: result});
});
}
render() {
return (
<div className="form-group col-sm-4">
<label>Author:</label>
<select className="form-control" onChange={(ev) => this.props.onChange(ev.target.value)}>
{this.state.authors.map( a =>
<option key={a.id} value={a.id}>{a.first_name + ' '+a.last_name }
</option>)
}
</select>
</div>
);
}
}
export default AuthorsDropDown;
I have assigned initial value for author.id and publisher.id fields in parent component as null, but, these fields only got their values after dropdown changes (i.e after onChange is fired). I have no idea how to set the value to them on rendering (i.e. initialization state). Here is the parent component:
import React, { Component } from "react";
import BookService from "./BooksService";
import AuthorsDropDown from "./AuthorsDropDown";
import PublishersDropDown from "./PublishersDropDown";
const bookService = new BookService();
class BookCreateUpdate extends Component {
constructor(props) {
super(props);
this.state = {
author:{id:null},
publisher:{id:null}
}
this.handleSubmit = this.handleSubmit.bind(this);
this.onChangeAuthor = this.onChangeAuthor.bind(this);
this.onChangePublisher = this.onChangePublisher.bind(this);
}
onChangeAuthor(new_author_id){
this.setState({author:{id:new_author_id}});
}
onChangePublisher(new_publisher_id){
this.setState({publisher:{id:new_publisher_id}});
}
handleCreate() {
alert(this.state.author.id);
bookService
.createBook({
name: this.refs.name.value,
author: this.state.author,
publisher: this.state.publisher,
page: this.refs.pages.value,
inventor_number: this.refs.inventor_number.value,
description: this.refs.description.value
})
.then(result => {
alert("The book is added!");
})
.catch(() => {
alert("Error!!");
});
}
handleUpdate(pk) {
bookService
.updateBook({
pk: pk,
name: this.refs.name.value,
author: this.refs.author,
publisher: this.refs.publisher,
pages: this.refs.pages.value,
description: this.refs.description.value
})
.then(result => {
console.log(result);
alert("Success");
})
.catch(() => {
alert("Error.");
});
}
handleSubmit(event) {
const {
match: { params }
} = this.props;
if (params && params.pk) {
this.handleUpdate(params.pk);
} else {
this.handleCreate();
}
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className="row">
<div className="form-group col-sm-8">
<label>Name:</label>
<input
className="form-control"
type="text"
ref="name"/>
</div>
</div>
<div className="row">
<AuthorsDropDown onChange={this.onChangeAuthor}/>
<PublishersDropDown onChange={this.onChangePublisher}/>
</div>
<div className="row">
<div className="form-group col-sm-4">
<label>Pages:</label>
<input
className="form-control"
type="number"
ref="pages"/>
</div>
<div className="form-group col-sm-4">
<label>ISBN:</label>
<input
className="form-control"
type="text"
ref="inventor_number"/>
</div>
</div>
<div className="row">
<div className="form-group col-sm-4">
<label>Info:</label>
<textarea
className="form-control"
ref="description"/>
</div>
</div>
<input className="btn btn-primary" type="submit" value="ok"/>
</form>
);
}
}
export default BookCreateUpdate;
I think you should consider a different way of tackling this problem. If I understand your requirement this component both creates and updates books. In this case, the <BookCreateUpdate/> component should receive a property which is the target book. For creation, it should be an empty object. For an update, it should be the object to update.
I think the mentioned concern relates to when editing. I suppose that books are persisted somewhere. If a book is passed in edit mode then the initial value should be passed down to the child components (input, AuthorsDropDown, PublishersDropDown) from the parent (<BookCreateUpdate/>).
class BookCreateUpdate extends Component {
constructor(props) {
super(props);
this.state(this.props.book)
}
onInputChange = propName => (e) => {
this.setState({[propName]: e.target.value })
}
...
handleCreate() {
const bookDraft = this.state;
bookService
.createBook(bookDraft)
.then(result => {
alert("The book is added!");
})
.catch(() => {
alert("Error!!");
});
}
...
render(){
const bookDraft = this.state;
return (
...
<div className="row">
<div className="form-group col-sm-8">
<label>Name:</label>
<input
className="form-control"
type="text"
value = {bookDraft.name}
onChange={this.onInputChange('name')}
/>
</div>
</div>
<AuthorsDropDown onChange={this.onChangeAuthor} authorId = {bookDraft.authorId}/>
<PublishersDropDown onChange={this.onChangePublisher} publisherId = {bookDraft.publisherId}/>
....
)
}
}
BookCreateUpdate.propsTypes = {
book: PropTypes.object
}
BookCreateUpdate.defaultProp = {
book: {authorId: null, publisherId: null}
}
It is also best not to use refs in this case. It is cleaner to pass a value to input and pass a callback for onChange event.
Related
I am new to react js. I am learning it by creating a simple app. I tried to create a simple weather app using react class component. All working fine but the result stored in a state variable is not printing in the template. I can see the API response in the console and then store the result on the 'currWeatherRes' state variable which is not showing in the template (Location is always blank)
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(){
super();
this.state = {
cityName: "",
currWeatherRes: {}
}
}
handleSubmit = (event) => {
event.preventDefault();
alert(`The name you entered was:`+ this.state.cityName);
fetch(`https://api.openweathermap.org/data/2.5/weather?q=`+this.state.cityName+`&appid=f3ee66722740d00cc6f197cbcab3d534`, {
method: 'GET'
}).then((response) => {
console.log(response)
this.setState({
currWeatherRes: response
})
//return response.json();
});
}
handleChange = (event) => {
this.setState({cityName:event.target.value});
}
render() {
return (
<div className="weather-app">
<form onSubmit={this.handleSubmit}>
<input type="text" value={this.state.cityName} onChange={this.handleChange} placeholder="Enter City"/>
<button type="submit" value="Submit">Submit</button>
</form>
{(typeof this.state.currWeatherRes.main != "undefined") ? (
<div className="weather-details">
<div className="weather-location">
<div className="location">Loctaion: {this.state.currWeatherRes.name}</div>
</div>
</div>
):('')}
</div>
);
}
}
export default App;
The problem was not related to react but the way you handled API call.
Fix:
fetch(`https://api.openweathermap.org/data/2.5/weather?q=`+this.state.cityName+`&appid=f3ee66722740d00cc6f197cbcab3d534`, {
method: 'GET'
}).then((response) => {
return response.json();
}).then((res) => {
this.setState({
currWeatherRes: res
})
});
Working code:
import React, { Component } from 'react';
class App extends Component {
constructor(){
super();
this.state = {
cityName: "",
currWeatherRes: {}
}
}
handleSubmit = (event) => {
event.preventDefault();
alert(`The name you entered was:`+ this.state.cityName);
fetch(`https://api.openweathermap.org/data/2.5/weather?q=`+this.state.cityName+`&appid=f3ee66722740d00cc6f197cbcab3d534`, {
method: 'GET'
}).then((response) => {
return response.json();
}).then((res) => {
this.setState({
currWeatherRes: res
})
});
}
handleChange = (event) => {
this.setState({cityName:event.target.value});
}
render() {
console.log(this.state.currWeatherRes)
return (
<div className="weather-app">
<form onSubmit={this.handleSubmit}>
<input type="text" value={this.state.cityName} onChange={this.handleChange} placeholder="Enter City"/>
<button type="submit" value="Submit">Submit</button>
</form>
{(typeof this.state.currWeatherRes.main != "undefined") ? (
<div className="weather-details">
<div className="weather-location">
<div className="location">Loctaion: {this.state.currWeatherRes.name}</div>
</div>
</div>
):('')}
</div>
);
}
}
export default App;
I am writing todo app. There are main files in my directory now:
App (rendering main page with header and buttons)
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { triggerText: 'Create a task' };
}
propTypes = {
triggerText: PropTypes.string.isRequired,
handleSubmit: PropTypes.object.isRequired,
};
render() {
const { triggerText } = this.state;
const { handleSubmit } = this.props;
return (
<div className="App">
<header className="App-header">
<h1>To Do List</h1>
<div id="tasksList">
<span className="tasks active">Tasks</span>
</div>
<div id="categoriesList">
<span className="categories">Categories</span>
</div>
<div>
<Container triggerText={triggerText} onSubmit={handleSubmit} /> // creates modal dialog and uses TodoForm
</div>
</header>
<div id="container" className="container">
<TodoBox tasks={[]}/>
</div>
</div>
);
}
}
TodoForm (create a form)
export default class TodoForm extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', tasks: [] };
}
propTypes = {
handleSubmit: PropTypes.object.isRequired,
}
handleRemove = (currentTaskId) => (e) => {
e.preventDefault();
const { tasks } = this.state;
this.setState({ tasks: tasks.filter(({ id }) => id !== currentTaskId) });
};
handleChange = (e) => {
e.preventDefault();
this.setState({ value: e.target.value });
}
handleSubmit = (e) => {
e.preventDefault();
const { value, tasks } = this.state;
const newTask = { id: uniqueId(), text: value };
this.setState({ value: '', tasks: [newTask, ...tasks] });
}
render() {
const { value } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="text"><strong>Create a task</strong></label>
<input
type="text"
onChange={this.handleChange}
value={value}
required
className="form-control"
id="text"
placeholder="I am going..."
/>
</div>
<div className="form-group">
<button type="submit" className="form-control btn btn-primary">Add</button>
</div>
</form>
);
}
}
TodoBox (generating list of tasks)
class Item extends React.Component {
propTypes = {
onRemove: PropTypes.object.isRequired,
task: PropTypes.string.isRequired,
};
render() {
const { task, onRemove } = this.props;
return (
<div className="row">
<div>
<button type="button" className="btn btn-primary" onClick={onRemove}>-</button>
</div>
<div className="col-10">{task.text}</div>
</div>
);
}
}
export default class TodoBox extends React.Component {
constructor(props) {
super(props);
}
propTypes = {
tasks: PropTypes.string.isRequired,
}
render() {
const { tasks } = this.props;
return (
<div className="item">
{tasks.map((task) => (
<div key={task.id}>
<Item task={task} onRemove={this.handleRemove} />
<hr />
</div>
))}
</div>
);
}
}
And the question is: how I can pass the state from TodoForm to TodoBox in App (it is initialize as an empty array now). I want to output tasks at the bottom of the same page in container after header element.
You can create a function (addTodo) in App component and pass it down to the TodoForm component. In TodoForm component you can invoke the addTodo function from props and send the todoValue as arguments props.addTodo(todoValue). In addTodo function in App component you can update the todoValue to state. Once you update the state it will re-render the App component, then the TodoBox component will call with the updated todoValue value.
Note: But it is not best practice. The best practice is to use React Context
For fun, I decided to write a booking app.
However ran into a problem when I get free rooms and send room data to TabComponent.js for the first time, everything works. But if I try to send the room data to TabComponent.js again, component does not update, but in redux devtools the state has changed. How to solve this problem? For more understanding, see the pictures below.
Component.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from "prop-types"
import { reservationCheck } from '../../../actions/axiosApi'
import { sendReservationProp } from '../../../actions/propActions'
class Form extends Component {
static propTypes = {
free_rooms: PropTypes.array.isRequired,
}
constructor(props) {
super(props)
this.state = {
arrival_date: '',
leaving_date: ''
}
}
handleChange = event => this.setState({ [event.target.name]: event.target.value });
handleSubmit = event => {
event.preventDefault()
this.props.reservationCheck(this.state)
}
sendFreeRoomPropToBookingTab = free_room => {
const reservation_prop = Object.assign(this.state, free_room)
this.props.sendReservationProp(reservation_prop) <--- sending room prop to tabComponent.js
}
render() {
const { arrival_date, leaving_date } = this.state
return (
<div className="booking-form-container">
<form onSubmit={this.handleSubmit}>
<input type="date" name="arrival_date" required onChange={this.handleChange} value={arrival_date} />
<input type="date" name="leaving_date" required onChange={this.handleChange} value={leaving_date} />
<button value="" type="submit">Find</button>
</form>
<div className="reservation-data-result">
{this.props.free_rooms.map(free_room => (
<div className="reservation-data-result__block" key={free_room.id}>
<img src={free_room.main_image} alt="" />
<div className="thumbnail-images">
{free_room.room_image.map(thumbnailImage => {
const [id, src] = thumbnailImage.split(': ')
return (
<div className="thumbnail-image">
<img src={src} key={parseInt(id, 10)}></img>
</div>
)
})}
</div>
<h3>{free_room.name}</h3>
<h5>price: {free_room.price}$</h5>
<a className="booking-btn">
<div className="btn-circle">
<div className="btn-arrow"></div>
</div>
<span className="btn-text" onClick={() => this.sendFreeRoomPropToBookingTab(free_room)}>Book room</span>
</a>
</div>
))}
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
free_rooms: state.roomReducer.free_rooms
})
export default connect(mapStateToProps, { reservationCheck, sendReservationProp })(Form)
action.js
import { RESERVATION_PROP } from './types'
export const sendReservationProp = reservation_prop => dispatch => {
dispatch({
type: RESERVATION_PROP,
payload: reservation_prop
})
}
reducer.js
import { GET_ROOM_DATA, RESERVATION_CHECK, RESERVATION_PROP } from '../actions/types'
const initialState = {
roomData: [],
free_rooms: [],
reservation_prop: {},
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_ROOM_DATA:
return {
...state,
roomData: action.payload
}
case RESERVATION_CHECK:
return {
...state,
free_rooms: action.payload
}
case RESERVATION_PROP:
return {
...state,
reservation_prop: action.payload
}
default: {
return state
}
}
}
TabComponent.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
class BookingTab extends Component {
constructor(props) {
super(props)
this.state = {
room: this.props.reservation_prop.room,
price: this.props.reservation_prop.price,
arrival_date: this.props.reservation_prop.arrival_date,
leaving_date: this.props.reservation_prop.leaving_date,
name: '',
phone_number: ''
}
}
handleSubmit = () => {
console.log("SUBMIT!")
}
render() {
return (
<div className="booking-tab">
<div className="booking-tab-container">
<h2>Booking</h2>
<form onSubmit={this.handleSubmit}>
<h3>Room: {this.props.reservation_prop.name}</h3>
<h5>Price: {this.props.reservation_prop.price}$</h5>
<span>Arrival date: {this.props.reservation_prop.arrival_date}</span>
<span>Leaving date: {this.props.reservation_prop.leaving_date}</span>
<div className="name-input">
<input type="text" name="name" placeholder="Name" />
</div>
<div className="phone-number-input">
<input type="text" name="phone_number" placeholder="X (XXX) XXX-XX-XX" />
</div>
<button>Submit</button>
</form>
</div>
</div>
)
}
}
const mapStateToProps = state => ({
reservation_prop: state.roomReducer.reservation_prop
})
export default connect(mapStateToProps)(BookingTab)
This line const reservation_prop = Object.assign(this.state, free_room) actually modifies this.state and then sets reservation_prop to this.state.
So as long as this.state is not replaced by a completely new state, you will dispatch the same object multiple times and just modify it - which is something redux cannot detect, as stateBefore === stateAfter will hold true.
You probably want to do
const reservation_prop = Object.assign({}, this.state, free_room)
which creates a new object every time.
In my code, I use a click event on the InterestBox in order to update the appearance of the InterestBox and to change the state of the parent of the parent of InterestBox.
However, when I inspect the element with the React Developer Tools or that I try to send a request to my API, the state.interests is always empty. But, when I log the state in the console, it shows an array with a length of 0, but containing all the proper values.
I tried to find what is wrong with my code, but I think I need an external look to find what is wrong.
import axios from 'axios';
import * as Cookies from 'js-cookie';
import React, { Component } from 'react';
import Button from '../../components/Button';
import Dashboard from '../Dashboard';
import ErrorContainer from '../../components/ErrorContainer';
import InterestList from '../../components/register/InterestList';
export class EditUser extends Component {
constructor(props) {
super(props);
this.state = {loading: true, interests: []}
this.addInterest = this.addInterest.bind(this);
this.logState = this.logState.bind(this);
}
addInterest(id, name) {
console.log('hoppity hippity you parenty')
var mid = 'm' + id;
console.log(this.state.interests[mid] == undefined)
if(this.state.interests[mid] == undefined) {
console.log(this.state);
this.setState((state) => {
state.interests[mid] = name;
return {interests: state.interests}
}, () => {
console.log(JSON.stringify(this.state.interests))
})
} else {
console.log('deleted')
var newInterest = this.state.interests;
delete newInterest[mid]
this.setState({interests: newInterest})
}
console.log(this.state.interests)
}
componentDidMount() {
var token = Cookies.get('token');
axios.get('http://localhost:8000/api/details', {headers: {"Accept": 'application/json', "Authorization": `Bearer ${token}`}}).then(
(success) => {
this.setState({
loading: false,
firstname : success.data.data.user.firstname,
lastname: success.data.data.user.lastname,
email: success.data.data.user.email,
dob: success.data.data.user.dob,
address: success.data.data.user.address,
uinterests: success.data.data.interests
})
}, (error) => {
this.props.history.push('/deconnexion');
}
)
var places = require('places.js');
var placesAutocomplete = places({
appId: "plZJLSHIW8M5",
apiKey: "0eddd2fc93b5429f5012ee49bcf8807a",
container: document.querySelector('#address-input')
});
}
logState() {
console.log(this.state);
}
render() {
return (
<Dashboard loading={this.state.loading}>
<h1 className="title">Modifier mon profil</h1>
<form className="user-form offer-update-form" onSubmit={this.handleSubmit}>
<label>Prénom :</label>
<input type="text" name="firstname" value={this.state.firstname} onChange={this.handleChange}></input> <br />
<label>Nom de famille :</label>
<input type="text" name="lastname" value={this.state.lastname} onChange={this.handleChange}></input> <br />
<label>Email :</label>
<input type="email" name="email" value={this.state.email} onChange={this.handleChange} /><br />
<label>Adresse :</label>
<input type="address" id="address-input" name="address" value={this.state.address} onChange={this.handleChange} /><br />
<label>Date de naissance :</label>
<input type="date" name="dob" value={this.state.dob} onChange={this.handleChange} /><br />
<InterestList alreadyChecked={this.state.uinterests} onClick={this.addInterest} />
<ErrorContainer errors={this.state.errors} />
<Button type="primary">Soumettre les changements</Button>
</form>
<Button type="danger" onClick={this.logState} />
</Dashboard>
)
}
}
export default EditUser
```
```
import React, { Component } from 'react'
import InterestBox from './InterestBox'
import Axios from 'axios'
export class InterestList extends Component {
constructor(props) {
super(props);
this.state = {pinterests: []}
this.pinterestRefs = React.createRef()
this.pinterestRefs.current = [];
}
componentDidMount() {
Axios.get('http://localhost:8000/api/interests')
.then((success) => {
this.setState({pinterests: success.data.data.interests});
})
}
componentDidUpdate(prevProps) {
console.log(JSON.stringify(prevProps));
console.log(JSON.stringify(this.props))
if(this.props.alreadyChecked != prevProps.alreadyChecked) {
this.props.alreadyChecked.forEach((item) => {
this.pinterestRefs.current.forEach((pinterest) => {
if(item == pinterest.props.id) {
console.log(pinterest)
pinterest.handleClick();
}
})
console.log(item)
})
}
console.log(this.pin)
}
render() {
return (
<React.Fragment>
{Object.keys(this.state.pinterests).map((interest, i) => {
var pinterest = this.state.pinterests[interest];
var callbackRef = node => this.pinterestRefs.current[i] = node;
return <InterestBox id={pinterest.id} onClick={this.props.onClick} icon={pinterest.picture_src} title={pinterest.name} ref={callbackRef} />
})}
</React.Fragment>
)
}
}
export default InterestList
```
```
import React, { Component } from 'react'
export class InterestBox extends Component {
constructor(props) {
super(props);
this.images = require('../../img/interests/*.svg');
this.state = {activated: false};
this.interest_box_content = React.createRef();
this.interest_text = React.createRef();
this.handleClick = this.handleClick.bind(this);
this.updateDimensions = this.updateDimensions.bind(this);
}
handleClick() {
console.log('hoppity hippity you clicky')
this.props.onClick(this.props.id, this.props.title);
this.setState(prevState => ({
activated: !prevState.activated
}))
}
updateDimensions() {
console.log((window.getComputedStyle(this.refs.interest_box_content).width))
this.refs.interest_text = (window.getComputedStyle(this.refs.interest_box_content).width)
}
render() {
return (
<div className="column is-one-fifth-desktop is-half-touch">
<div className="interest-box">
<div className="interest-box-adjuster">
<div ref={"interest_box_content"} className={"interest-box-content " + (this.state.activated == true ? 'interest-box-activated' : '')} onClick={this.handleClick}>
<img className="interest-icon" src={this.images[this.props.icon]} style={{'height': '50%'}}></img>
<i className="activated-icon fas fa-check"></i>
<span ref={"interest_text"} className="interest-text">{this.props.title}</span>
</div>
</div>
</div>
</div>
)
}
}
export default InterestBox
```
you have initialized interest as an array using []
but then, you are updating it as a map state.interests[mid] = name;
Note: JS is not type-safe. after the above statement, interests no longer remained an array
That's the reason why on console you are able to see it being populated but with an array length of 0. that's because you are outputting an object now and not an array.
I am trying to create dynamic form, so I pass some jsx elements to a child component as a property. Even though the state is being updated, the updated state is not passed to the element. Below is the code:
This is the child component which maps over the passed controls and outputs them.
class Form extends Component {
render() {
return (
<div>
{this.props.controls.map((c, i) => {
return <React.Fragment key={i}>{c}</React.Fragment>;
})}
</div>
);
}
}
This is the App that calls the child component:
class App extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
this.controls = [
<input
type="text"
onChange={this.onChangeUsername}
value={this.state.username}
/>
];
}
componentDidUpdate() {
console.log(this.state);
}
render() {
return (
<div className="App">
<div className="app__group">
<h1>This is not working</h1>
<Form controls={this.controls} />
</div>
<div className="app__group">
<h1>This is working</h1>
<input
type="text"
onChange={this.onChangePassword}
value={this.state.password}
/>
</div>
</div>
);
}
onChangeUsername = e => {
console.log('onChangeUsername', e.target.value);
this.setState({ username: e.target.value });
};
onChangePassword = e => {
console.log('onChangePassword');
this.setState({ password: e.target.value });
};
}
As an example of the unexpected behaviour, when an input passed as a property to the child component, I cannot type in the input. The state gets updated but it's is not passed to the child, thus the text does not show.
On the other hand, a standard input element works, I can type and see the output.
What am I doing wrong?
the problem is that you are trying to make something "fixed" as something dynamic. lets go with a more functional approach and it will refresh each one of the inputs like if they are dynamic.
class App extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
componentDidUpdate() {
console.log(this.state);
}
render() {
return (
<div className="App">
<div className="app__group">
<h1>This is not working</h1>
<Form controls={this.controls()} />
</div>
</div>
);
}
controls = () => {
return [<input
type="text"
onChange={this.onChangeUsername}
value={this.state.username}
/>]
}
onChangeUsername = e => {
console.log('onChangeUsername', e.target.value);
this.setState({ username: e.target.value });
};
onChangePassword = e => {
console.log('onChangePassword');
this.setState({ password: e.target.value });
};
}