Child component in React doesn't get updated? - reactjs

The user.entries state gets updated; however, it's not getting passed immediately to the child component, it only gets updated on the second button submit and it passes the first user.entries not the second one, though.
App.js file:
class App extends Component {
constructor(){
super();
this.state = {
input: '',
imageUrl:'',
box:{},
route:'signin',
isSignedIn: false,
user: {
id: '',
name: '',
email: '',
entries: 0,
joined: ''
}
}
}
onButtonSubmit = () => {
this.setState({imageUrl: this.state.input})
app.models.predict(Clarifai.FACE_DETECT_MODEL,this.state.input).then(response => {
if (response){
fetch('http://localhost:3000/image', {
method: 'put',
headers: {'content-type': 'application/json'},
body: JSON.stringify(
{id: this.state.user.id}
)
})
.then(resp => resp.json())
.then(count => {Object.assign(this.state.user, {entries: count})});
}
this.displayFaceBox(this.calculateFaceLocation(response))
}).catch(err => console.log(err));
}
render(){
return(
<div>
<Rank name={this.state.user.name} entries={this.state.user.entries} />
</div>
)
};
Rank.js file:
import React from 'react';
const Rank = ({name, entries}) => {
return(
<div className='mv2'>
<p className='f3 mv0 white'>
{`Welcome back ${name}, your entries count is...`}
</p>
<p className='f1 mv0 b white'>
{`#${entries}`}
</p>
</div>
)
}
export default Rank;

It is not re-rendering the component after you update since you are directly updating the object. You need to instead use this.setState
fetch(...)
.then(...)
.then(count => {
this.setState((previousState) => ({
user: {
...previousState.user,
entries: count,
}
}));
});
Along with updating the state, this.setState also triggers a re-render of the component which is required to see the updated state on the UI as soon as the state is updated.

Related

TypeError: categories is undefined

I made my program mad. I was trying to make some adjustments from a previous issue I was having with a unpermitted parameters: :recipe issue I was having. I basically encapsulated attributes within a recipe object to hopefully match what was being sent to the backend due to strong params. I pointed to where those changes were made. Whether that being resolved is contingent on the work around for this current issue now. After making some changes I keep getting this TypeError: categories is undefined after I adjusted my action type for my dispatch payload on the frontend using redux.
switch(action.type){
case 'Add_Recipe':
const recipe = {
recipe:{ <-----I encapsulated everything within a recipe value and recipe as my key
name: action.name,
ingredients: action.ingredients,
chef_name: action.chef_name,
origin: action.origin,
// categoryId: action.categoryId,
category: action.category
}
}
return{
...state,
recipes: [...state.recipes, recipe],
}
default:
return state
}
}
And to make sure I everything was consistent with this change I also changed my initial state and setState in my RecipeInput component.
Note: I left event handlers out on purpose for this code snippet
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
recipe:{ <-----I encapsulated everything within a recipe value and recipe as my key
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
}
this.handleNameChange.bind(this)
this.handleOriginChange.bind(this)
this.handleChefChange.bind(this)
this.handleIngChange.bind(this)
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGOREIS_URL =`${BASE_URL}/categories`
fetch(CATEGOREIS_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
console.log(initialCats)
this.setState({
category: initialCats,
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
recipe:{ <-----I encapsulated everything within a recipe value and recipe as my key
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
I also made necessary changes in my postRecipe action
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response =>
response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
recipe:{ <-----I encapsulated everything within a recipe value and recipe as my key
category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
// categoryId: resp.categoryId
}
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
After making those changes I started getting the TypeError: categories is undefined error in my Categories component.
import React, { Component } from 'react'
class Categories extends Component{
render(){
let categories = this.props.category
//I am getting screamed at for categories being undefined after previous changes
let optionItems = categories.map((cat,index) =>
<option key={index}>{cat.category}</option>
)
return (
<div>
<select>
{optionItems}
</select>
</div>
)
}
}
export default Categories
The type error started happening after encapsulated everything as recipe as my key and all the other attributes as the values. Can somebody help point where my broke at after making changes?
I think you are updating your state from the fetch with undefined data:
this.setState({
category: initialCats
})
You've encapsulated the category property inside the recipe object. So it should be something like:
this.setState({
recipe: {
...this.state.recipe,
category: initialCats,
}
})
In your Categories component return you could also do this for example to show categories are loading:
return (
<div>
<select>
{this.props.category.length ? optionItems : <p>Loading...</p>}
</select>
</div>
)

How to update state of parent from child component in reactJS

class A extends Component {
constructor(props) {
super(props);
this.state = {
fruitsDetailsList: [],
fruitCode: this.props.fruitCode,
};
}
showModal = () => {
this.setState({ show: true });
};
hideModal = () => {
this.setState({ show: false });
};
componentDidMount() {
const url = 'http://localhost:3000/getFruitslist';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fruitCode: this.state.fruitCode,
}),
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error(res.status);
})
.then(res => {
this.setState({
fruitsDetailsList: res.fruitDetailsList,
});
})
.catch(error => {});
}
render() {
const columns = [
{
Header: 'Sr.No',
id: "row",
maxWidth: 50,
Cell: (row) => {
return <div>{row.index + 1}</div>
}
},
{
Header: 'Actions',
id: 'FruitName',
accessor: d => (
<div>
<B/>
</div>
)
}
];
return (
<div>
<ReactTable
className="-striped -highlight"
columns={columns}
data={this.state.fruitsDetailsList}
defaultPageSize={10}
noDataText={'No Data available.'}
/>
<p></p>
</div>
);
}
Class B extends component{
constructor(props) {
super(props);
this.state = { modal: false };
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState({
modal: !this.state.modal
});
}
render() {
return (
<div>
<Button onClick={this.toggle}/>
<Modal isOpen={this.state.modal}>
<ModalHeader>Fruits list</ModalHeader>
<ModalBody>
<Formik
initialValues={{fruitName: ''}}
onSubmit={(fields, action) => {
action.setSubmitting(true);
const url = 'http://localhost:3000/getFruit';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fruitName: fields.fruitName,
}),
})
.then(res => {
action.setSubmitting(false);
console.log("Success!!!);
})
.catch(error => {});
}}
render={({ errors, touched, isSubmitting }) => (
!isSubmitting ? (
<Form>
<div className="form-group">
<label htmlFor="fruitName">FruitName</label>
<Field name="fruitName" type="text"/>
</div>
<div className="form-group">
<Button type="submit" className="bg-gradient-theme-left border-0 " size="m">Submit</Button>
</div>
</Form>
)
)}
/>
</ModalBody>
</Modal>
</div>
);
}
}
- REACT JS
As you can see there are 2 classes
1)Component A
2)Component B
In Component A I am calling B component as Button in react-table
Actually we have to render react table with all the data in database by calling post API request '/getFruitslist' which we are calling in componentDidMount of Component A so that data in react-table gets populated correctly in table
Now when we click on button of Component B one record gets inserted in
database but as API is called in ComponentDidMount of Component A which is
parent of Component B , data does not populate in react-table on clicking button of B component . How to achieve this ?
The philosophy of React is to manage the state into the top component. A child component should not keep a reference to its parent in order to update the parent state.
To achieve this behavior, the parent component should pass a callback to the child component through its properties, then the child component can invoke this callback by calling the matching property
In your example, you can pass a property onAddFruit to the B component. When the POST call succeed, you should call this function
.then(fruit => {
action.setSubmitting(false);
console.log("Success!!!);
this.props.onAddFruit(fruit);
})
In the parent component A, you define a function which will add a fruit to the state variable fruitsDetailsList. Then you pass this function to the component B through the property onAddFruit
handleAddFruit = fruit => {
this.setState(prevState => ({
fruitsDetailsList: [...prevState.fruitsDetailsList, fruit]
});
};
The declaration of the B element will be like this :
<B onAddFruit={this.handleAddFruit}/>
There are ways to achieve what you are looking for. The easiest one is to pass down through props the necessary objects, values. In complex cases I would recommend to use redux for application state management, it is way more easier to keep your state consistent in a case when you need to have the state objects accessible in several components.
Pass down the props:
Once you have 2 components, data can be passed down for example from A to B. Sharing below 1 small example how you can achieve that:
class A extends Component {
constructor(props) {
super(props);
this.state = {
fruitsDetailsList: [],
fruitCode: this.props.fruitCode,
};
}
render {
return (
<B dataFromA={this.state.fruitsDetailsList} />
)
}
}
Redux option:
So what is Redux?
A predictable state container for JavaScript apps.
Basically your application will have the state object in 1 common place what you can match to each components if it needs to be accessed with mapStateToProps function from redux. The link Connect: Extracting Data with mapStateToProps explains how to that in more details. Once you need to modify any of the elements you can use mapDispatchToProps to do so. Please find the Connect: Dispatching Actions with mapDispatchToProps link for further setup.
Read further about the setup, getting started here: Redux
Summary:
In your scenario because of API calls, possible frequent update in state objects I recommend to go with the second option. It might be complicated at the first look but it is worth to effort because you don't need to worry about the state consistency and through props the data is updated automatically once you have modification on any of the objects.
I hope this helps!

Setting initial state to null and passing file object to preview modal

I am creating a preview modal to view a specific image on click. My thought process is to have a state property set to null, on clicking the image I then set the state to the specific file and render the image path as the image source. However, Typescript does not like this and states Object is possibly null.
I tried adding selectedFile: Asset in my extended props but I am given an error in the parent component expecting it to pass the file down. I do not want it to behave this way.
I tried writing it as selectedFile: Asset<{}>() but Typescript complains that I'm using it as a type instead of a value.
import * as React from "react"
import { Company } from "data/companies"
import { Asset } from "data/companies"
import Modal from "components/Modal"
interface MediaLibraryProps {
company: Company
}
class MediaLibrary extends React.Component<MediaLibraryProps> {
state = {
mediaLibrary: [],
editModalIsOpen: false,
selectedFile: null
}
toggleEditModal = () => {
this.setState({ editModalIsOpen: !this.state.editModalIsOpen })
}
openEditModal = (file: Asset) => {
this.setState({
editModalIsOpen: true,
selectedFile: file
})
}
getMediaLibrary = async () => {
await fetch(
`${process.env.REACT_APP_SA_API_URL}/${this.props.company.id}/images`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
).then(blob => blob.json())
.then(function (data: any) {
return data.map((file: Asset) => Object.assign(file, {
assetId: file.assetId,
path: file.path
}))
}).then((data) => this.setState({ mediaLibrary: [...data] }))
}
render() {
const files = this.state.mediaLibrary.map((file: Asset) => (
<div key={file.assetId} onClick={() => this.openEditModal(file)}>
<div>
<img src={`${process.env.REACT_APP_SA_CDN_URL}${file.path}`} />
</div>
</div>
))
return (
<div>
<div>
<h2>Media Library</h2>
</div>
{files}
<hr />
<Modal isOpen={this.state.editModalIsOpen} toggleOpenness=
{this.toggleEditModal}>
<img
src={this.state.selectedFile.path}
onClick={this.toggleEditModal}
/>
</Modal>
</div>
)
}
}
export default MediaLibrary
I expect the file to be passed to the state and be given access to its properties to be used in my Modal.
Actual behaviour is that TypeScript does not like a state to be initialized as null.
I'm sure there is a cleaner way to do this but I fixed it by setting
state = {
selectedFile: {
path: ""
}
}
openEditModal = (file: Asset) => {
this.setState({
editModalIsOpen: true,
selectedFile: {
path: file.path
}
})
}

Setting State Array and Appending Value on Update

I'm still learning about state and lifecycle with ReactJS and have run into a scenario where I have a form that on submit should save the form value and then append the returned JSON object to the end of an array which would re-render the component storing the original array.
With my current setup, I have the components setup and form submit with returned JSON object, but the state contains an empty array rather than the object spread {...comment} and it doesn't look like the setState is updating component, but that could be due to the empty array mentioned before. Can anyone point me in the right direction?
Comment:
import React from 'react';
import fetch from 'node-fetch';
//record Comment - Comment Form Handle POST
class CommentForm extends React.Component {
constructor(props){
super(props);
this.state = {
value: '',
comments: []
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
postComment(comment, recordId, csrfToken) {
var body = { comment: comment };
var route = 'http://localhost:3000/record/' + recordId + '/comment';
fetch(route,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
'X-CSRF-Token': csrfToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
let commentsArr = this.state.comments;
this.setState({comments: commentsArr.concat(data)});
})
.catch(err => {
console.log(err);
});
}
onChange(e){
this.setState({
value: e.target.value
});
}
handleSubmit(e){
e.preventDefault();
this.postComment(this.state.value, this.props.recordId, this.props.csrf);
}
render(){
return (
<div className="record-comment__form">
<div className="row">
<form action={"/record/" + this.props.recordId + "/comment"} method="post" onSubmit={this.handleSubmit}>
<input type="hidden" name="_csrf" value={this.props.csrf}/>
<textarea name="comment" className="record-comment__form-text-area" onChange={e => this.setState({ value: e.target.value })} value={this.state.value}></textarea>
<button type="submit" className="record-comment__form-button" disabled={!this.state.value}>Comment</button>
</form>
</div>
</div>
)
}
}
//record Comment - Comment
const Comment = props => {
return (
<div className="row">
<div className="col-md-12">
<h5>{props.user_id}</h5>
<h4>{props.comment}</h4>
<h3>{props.synotate_user.fullNameSlug}</h3>
</div>
</div>
)
}
//record Comment - Container
export default class Comments extends React.Component {
render() {
return (
<div className="record-comment-container">
<CommentForm recordId={this.props.recordId} csrf={this.props.csrf}/>
{ this.props.record_comments.map((comment, i) =>
<Comment {...comment} key={this.props.recordCommentId}/>
)}
</div>
);
}
}
Record (Parent component)(Where Comment is being set):
//GET /api/test and set to state
class RecordFeedContainer extends React.Component{
constructor(props, context) {
super(props, context);
this.state = this.context.data || window.__INITIAL_STATE__ || {records: []};
}
fetchList() {
fetch('http://localhost:3000/api/test')
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
this.setState({ records: data.record, user: data.user, csrf: data.csrfToken });
})
.catch(err => {
console.log(err);
});
}
componentDidMount() {
this.fetchList();
}
render() {
return (
<div className="container">
<h2>Comments List</h2>
<RecordFeed {...this.state} />
</div>
)
}
};
//Loop through JSON and create Record and Comment Container Component
const RecordFeed = props => {
return (
<div>
{
props.records.map((record, index) => {
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 record-card">
<RecordCard {...record} key={record.recordIdHash} user={props.user} />
<Comments {...record} key={index} recordId={record.recordIdHash} csrf={props.csrf}/>
</div>
</div>
);
})
}
</div>
)
}
Your problem is that when rendering <Comments>, the this.props.record_comments is not the comments you've updated in the state of the <CommentForm> component. Each component has it's own internal state.
You need to pass the state along to your <Comments> component. You will need to move your state up to the top level or use a state management system like Redux which will allow you to access a shared state which could contain your comments array.
From the top level component you could manage the state there, like so:
this.state = {
comments: [],
// other shared state
};
You can pass along an update comments function, named for example updateCommentsFunc() to <CommentForm> like so:
<CommentForm updateComments={this.updateCommentsFunc} recordId={this.props.recordId} csrf={this.props.csrf}/>
Which will allow you to pass the updated comments back up to the parent component via something like:
const updateCommentsFunc = (newComments) => {
this.setState({comments: [...this.state.comments, newComments]});
}
Your postComment() function doesn't appear to be properly bound to your enveloping <CommentForm/> component's this. As a result; calling this.setState() from within the function isn't really doing anything.
Try binding it within your constructor method.
constructor(props) {
// ...
this.postComment = this.postComment.bind(this)
}
Or by declaring it using an arrow function.
postComment = (comment, recordId, csrfToken) => {
// ...
}
See this article for more info on React binding patterns.

State of React child doesn't update after updating parent component

I try to update the center prop of the child BaseMap component via the parent component. Even though the parent components state gets updated (and I could read the new updated properties in the console.log), it is not passed down to the child properties.
The last thing I tried was the componentWillReceiveProps method. It still doesn't work.
This is my code:
const google = window.google;
let geocoder = new google.maps.Geocoder();
class App extends Component {
constructor(props) {
super(props);
this.state = {
avatar: '',
username: 'someUse03',
realName: '',
location: '',
followers: '',
following: '',
repos: '',
address: '',
}
}
render() {
return (
<div>
<SearchBox fetchUser={this.fetchUser.bind(this)}/>
<Card data={this.state} />
<BaseMap />
</div>
);
}
fetchApi(url) {
fetch(url)
.then((res) => res.json())
.then((data) => {
this.setState({
avatar: data.avatar_url,
username: data.login,
realName: data.name,
location: data.location,
followers: data.followers,
following: data.following,
repos: data.public_repos,
address: geocoder.geocode({'address': data.location}, function(results, status) {
if (status == 'OK') {
var coords = [];
var results = results.map((i) => {
i.geometry.location = i.geometry.location
.toString()
.replace(/[()]/g, '')
.split(', ');
coords.push(i.geometry.location[0], i.geometry.location[1]);
results = coords.map((i) => {
return parseInt(i, 10)
});
return results;
});
} else {
alert('Geocoding was not successfull because ' + status)
}
})
})
});
}
fetchUser(username) {
let url = `https://api.github.com/users/${username}`;
this.fetchApi(url);
}
componentDidMount() {
let url = `https://api.github.com/users/${this.state.username}`;
this.fetchApi(url);
}
}
export default App;
This is the child component:
BaseMap extends React.Component {
constructor(props) {
super(props);
this.state = {
center: [41, 21],
}
}
componentWillReceiveProps(nextProps) {
this.setState({ center: nextProps.address});
}
render() {
return (
<Col md={10} mdPull={1}>
<div className="map">
<GoogleMap
bootstrapURLKeys={'AIzaSyBhzwgQ3EfoIRYT70fbjrWASQVwO63MKu4'}
center={this.state.center}
zoom={11}>
</GoogleMap>
</div>
</Col>
);
}
}
You are fetching inside the render method. this is a big NO NO.
Instead do that in the componentDidMount life cycle method
Another thing that may or may not be related to your problem, Arrays are reference types, that means if you mutate them they still points to the same ref in the memory. this could be problematic for the Reconciliation and diff algorithm of react to determine if the state indeed changed.
when you want to change or return a new array you could simply use the ES6 spread operator:
const nextArray = [...nextProps.address]
this.setState({ center: nextArray });
EDIT
Ok i forgot to mention the most important part here :)
You are not passing any props to <BaseMap /> so you won't get any helpful data in componentWillReceiveProps.

Resources