React dnd to push object of dropped item to component's state. - reactjs

In my component, there are local state of paid and droppedItems.
constructor () {
super();
this.state = {
droppedItems: [],
};
And I specified drop specification like below,
drop(props, monitor) {
const orderItem = monitor.getItem();
},
What I want to do is that when an orderItem is dropped to the target, it pushes orderItem to state of droppedItems So to do that, I made handleDrop function
handleDrop (orderItem) {
if(!orderItem){
this.setState(update(this.state, {
droppedItems: orderItem ? {
$push: [orderItem]
} : {}
}));
}
else return false;
}
And call it like this
return connectDropTarget(
<div className="payment_block" onDrop={this.handleDrop}>
In my render function, there are const { canDrop, connectDropTarget, getItem} = this.props;
Below is result from console.log(this.props)
Object { id: 1, order: Object, channel: undefined, dispatch: routerMiddleware/</</<(), connectDropTarget: wrapHookToRecognizeElement/<(), canDrop: false, itemType: "order_item", getItem: Object
Although it recognizes that there is a dropped item, it does not push to the state.
How can I push above Object of getItem to the component's state? What am I doing wrong here?
Thanks in advance

The signature of drop also receives your dnd decorated component as its third argument, so you should be able to call your handleAdd method.
drop(props, monitor, component) {
component.handleDrop(monitor.getItem());
}
Also the if statement in your handleDrop method is checking that no orderItem is passed in, which means the code will never run when you want it to, I'm not familiar with the what your update function does (presumably immutably pushing to the array), but perhaps something like this would work...
handleDrop: function(orderItem) {
if (!!orderItem) {
this.setState(
update(this.state, { droppedItems: { $push: [orderItem] } })
)
}
}

Related

react state not updated even inside callback

I'm having problem setting state. I've to pass extra classes to CardComponent on click, on click is working fine, setState is called, and the callback executes, but state is not mutated (as logs clearly deny that state is updated - see onSelect below). How could I fix that?
Flow is that if an item is selected, parent component resets the array with selected property set on each item (the selected result object gets selected set to true), that works fine and list is reset, selected is highlighted. Then if user clicks on another item (SearchResult component), it should update itself and apply extra classes. This time it's guarenteed that parent would not reset the list.
import { Component, ReactNode } from "react";
import { Subject, Subscription } from "rxjs";
import CardComponent from "../../utils/card.component";
export interface SearchResultComponentClickEvent {
(_id: string): void;
}
export interface SearchResultComponentProps {
result: any;
full: boolean;
onClick: SearchResultComponentClickEvent;
onSelect: Subject<string>;
}
interface SearchResultComponentState {
selected: string;
extraClasses: string;
}
export default class SearchResult extends Component<
SearchResultComponentProps,
SearchResultComponentState
> {
onSelectSubscription: Subscription = null;
constructor(props: SearchResultComponentProps) {
super(props);
let extraClasses = "mb-4";
if (this.props.result.selected) extraClasses += " border border-primary";
this.state = {
selected: this.props.result.selected,
extraClasses,
};
this.onSelect = this.onSelect.bind(this);
}
componentDidMount(): void {
this.onSelectSubscription = this.props.onSelect.subscribe((_id: string) => {
this.setState({
selected: '',
extraClasses: 'mb-4'
});
});
}
componentWillUnmount(): void {
this.onSelectSubscription.unsubscribe();
}
onSelect() {
this.setState({
selected: this.props.result._id,
extraClasses: "mb-4 border border-primary",
}, () => {
console.log(this.state);
// logs: { selected: '', extraClasses: "mb4 " }
});
this.props.onClick(this.props.result._id);
}
view(): ReactNode {
return <div>Simple view</div>
}
fullView(): ReactNode {
return <div>Extended view</div>;
}
render(): ReactNode {
return (
<CardComponent
padding={0}
onClick={this.onSelect}
extraClasses={this.state.extraClasses}
key={this.state.extraClasses}
>
{this.props.full ? this.fullView() : this.view()}
</CardComponent>
);
}
}
You have changed the state but setState is asynchronous in nature, changing the state will not immediately give its updated value https://facebook.github.io/react/docs/react-component.html#setstate, if you can show snippet of what exactly are you doing CardComponent, can give a more reliable solution.
If you still want to go with this approach, you can change your onSelect to something like this.
onSelect() {
this.setState({
selected: this.props.result._id,
extraClasses: "mb-4 border border-primary",
}, function() {
console.log(this.state);
this.props.onClick(this.props.result._id);
});
}
this will wrap or attached a callback to the state, which guarantees it, but it has its own side effects.
I would also recommend componentDidUpdate over this solution, more precise.
The problem is probably in the rxjs Subscription object.
Insert console.log and check...
componentDidMount(): void {
this.onSelectSubscription = this.props.onSelect.subscribe((_id: string) => {
// Log
console.log('componentDidMount-onSelectSubscription', this.state);
this.setState({
selected: '',
extraClasses: 'mb-4'
});
});
}

Render an array as list with onClick buttons

I'm new at ReactJs development, and I'm trying to render a list below the buttons I created with mapping my BE of graphQl query. I don't know what I'm doing wrong (the code has a lot of testing on it that I tried to solve the issue, but no success.)
The buttons rendered at getCategories() need to do the render below them using their ID as filter, which I use another function to filter buildFilteredCategoryProducts(categoryParam).
I tried to look on some others questions to solve this but no success. Code below, if need some more info, please let me know!
FYK: I need to do using Class component.
import React, { Fragment } from "react";
import { getProductsId } from "../services/product";
import { getCategoriesList } from "../services/categories";
//import styled from "styled-components";
class ProductListing extends React.Component {
constructor(props) {
super(props);
this.state = {
category: { data: { categories: [] } },
product: { data: { categories: [] } },
filteredProduct: { data: { categories: [] } },
};
this.handleEvent = this.handleEvent.bind(this);
}
async handleEvent(event) {
var prodArr = [];
const testName = event.target.id;
const testTwo = this.buildFilteredCategoryProducts(testName);
await this.setState({ filteredProduct: { data: testTwo } });
this.state.filteredProduct.data.map((item) => {
prodArr.push(item.key);
});
console.log(prodArr);
return prodArr;
}
async componentDidMount() {
const categoriesResponse = await getCategoriesList();
const productsResponse = await getProductsId();
this.setState({ category: { data: categoriesResponse } });
this.setState({ product: { data: productsResponse } });
}
getCategories() {
return this.state.category.data.categories.map((element) => {
const elName = element.name;
return (
<button id={elName} key={elName} onClick={this.handleEvent}>
{elName.toUpperCase()}
</button>
);
});
}
buildFilteredCategoryProducts(categoryParam) {
const filteredCategories = this.state.product.data.categories.filter(
(fil) => fil.name === categoryParam
);
let categoryProducts = [];
filteredCategories.forEach((category) => {
category.products.forEach((product) => {
const categoryProduct = (
<div key={product.id}>{`${category.name} ${product.id}`}</div>
);
categoryProducts.push(categoryProduct);
});
});
return categoryProducts;
}
buildCategoryProducts() {
const filteredCategories = this.state.product.data.categories;
let categoryProducts = [];
filteredCategories.forEach((category) => {
category.products.forEach((product) => {
const categoryProduct = (
<div key={product.id}>{`${category.name} ${product.id}`}</div>
);
categoryProducts.push(categoryProduct);
});
});
return categoryProducts;
}
buildProductArr() {
for (let i = 0; i <= this.state.filteredProduct.data.length; i++) {
return this.state.filteredProduct.data[i];
}
}
render() {
return (
<Fragment>
<div>{this.getCategories()}</div>
<div>{this.buildProductArr()}</div>
</Fragment>
);
}
}
export default ProductListing;
Ok, so this won't necessarily directly solve your problem,
but I will give you some pointers that would definitely improve some of your code and hopefully will strengthen your knowledge regarding how state works in React.
So first of all, I see that you tried to use await before a certain setState.
I understand the confusion, as setting the state in React works like an async function, but it operates differently and using await won't really do anything here.
So basically, what we want to do in-order to act upon a change of a certain piece of state, is to use the componentDidUpdate function, which automatically runs every time the component re-renders (i.e. - whenever there is a change in the value of the state or props of the component).
Note: this is different for function components, but that's a different topic.
It should look like this:
componentDidUpdate() {
// Whatever we want to happen when the component re-renders.
}
Secondly, and this is implied from the previous point.
Since setState acts like an async function, doing setState and console.log(this.state) right after it, will likely print the value of the previous state snapshot, as the state actually hasn't finished setting by the time the console.log runs.
Next up, and this is an important one.
Whenever you set the state, you should spread the current state value into it.
Becuase what you're doing right now, is overwriting the value of the state everytime you set it.
Example:
this.setState({
...this.state, // adds the entire current value of the state.
filteredProduct: { // changes only filteredProduct.
...filteredProduct, // adds the current value of filteredProduct.
data: testTwo
},
});
Now obviously if filteredProduct doesn't contain any more keys besides data then you don't really have to spread it, as the result would be the same.
But IMO it's a good practice to spread it anyway, in-case you add more keys to that object structure at some point, because then you would have to refactor your entire code and fix it accordingly.
Final tip, and this one is purely aesthetic becuase React implements a technique called "batching", in-which it tries to combine multiple setState calls into one.
But still, instead of this:
this.setState({ category: { data: categoriesResponse } });
this.setState({ product: { data: productsResponse } });
You can do this:
this.setState({
...this.state,
category: {
...this.state.category,
data: categoriesResponse,
}
product: {
...this.state.product,
data: productsResponse,
},
})
Edit:
Forgot to mention two important things.
The first is that componentDidUpdate actually has built-in params, which could be useful in many cases.
The params are prevProps (props before re-render) and prevState (state before re-render).
Can be used like so:
componentDidUpdate(prevProps, prevState) {
if (prevState.text !== this.state.text) {
// Write logic here.
}
}
Secondly, you don't actually have to use componentDidUpdate in cases like these, because setState actually accepts a second param that is a callback that runs specifically after the state finished updating.
Example:
this.setState({
...this.state,
filteredProduct: {
...this.state.filteredProduct,
data: testTwo
}
}, () => {
// Whatever we want to do after this setState has finished.
});

Re-render array of child components in React after editing the array

I have data in parent's component state like this:
data=[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
In my react component I am calling child components as list in the return part of the parent component as shown below.
class MyApp extends React.Component{
constructor(props) {
super(props);
this.state = {
record={
name="",
timestamp="",
data =[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
}
this.deleteFromStateArray=this.deleteFromStateArray.bind(this);
}
deleteFromStateArray(key){
let oldData = [...this.state.record.data];
let newData= [];
oldData.map(function (record, i) {
if (record.key != key) {
newData.push(record);
}
})
newData.map(function (record, i) {
newData[i].key = i + 1
})
this.setState({ record: Object.assign({}, this.state.record, { data: newData }), });
}
render() {
return(
{
this.state.data.map((record,index) =>
<Child key={record.key}
id={record.key}
name={record.name}
value={record.value}
onDelete={this.deleteFromStateArray} />
}
)
}
I am calling onDelete() in the child component like this
<button onClick={this.props.onDelete(this.state.id)} />
\\ I am initializing id as key in state inside constructor in child
My problem is when I am calling onDelete in the child class I am able to remove the obj with key=1 properly in the function but rerendering is not happening properly.
What I mean is state is getting set properly with only 2 items in data 1 with key=0 and other with key=2. But what i see in GUI is 2 child components 1 with key 0 and second one with key=1 which is cxurrently not present in the state data.
Can anybody help me with re-rendering the data properly?
I also want to change the key ordering after deleting from array in setState
React uses key to work out if elements in the collection need to be re-rendered. Keys should be unique and constant. In your method you are changing key property of your records and this probably leads to described error.
Also, you can replace all your code with simple filter call like this:
this.setState(oldState => ({
record: {
...oldState.record,
{
data: oldState.record.data.filter(item => item.key !== key)
}
}
});
It is also worth mentioning that you should keep your state as flat as possible to simplify required logic. In your case removing record and and leaving it as below would be a good idea:
this.state = {
name: "",
timestamp: "",
data: [
{key:0,name: "abc",value: "123"},
{key:1,name: "def",value: "456"},
{key:2,name: "ghi",value: "789"}
]
}
I'm not certain if this actually worked for you but the function declaration for deleteFromStateArray is inside the render function.
I think your component should look like this:
class MyApp extends React.Component{
constructor(props) {
super(props);
this.state = {
record={
name="",
timestamp="",
data =[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
}
this.deleteFromStateArray=this.deleteFromStateArray.bind(this);
}
deleteFromStateArray(key){
let oldData = [...this.state.record.data];
let newData= [];
oldData.map(function (record, i) {
if (record.key != key) {
newData.push(record);
}
})
newData.map(function (record, i) {
newData[i].key = i + 1
})
this.setState({ record: Object.assign({}, this.state.record, { data: newData }), });
}
render() {
return(
{
this.state.record.data.map((record,index) =>
<Child key={record.key}
id={record.key}
onDelete={this.deleteFromStateArray} />
}
)
}
}
You can use a built-in set function provided by react with state. See below for an example:
import { useState } from 'react';
const [data, setData ] = useState([]);
fetch('https://localhost:5001/getdata')
.then((response) => response.json())
.then((data) => setData(data)) // Example setData usage
.catch((error) => alert)

Hold the component's rendering until the store has finished hydration

I'm fetching my initial data like so:
export function populate (dispatch) {
let posts = []
dispatch(requestNews())
fetch(someEndPoint)
.then(payload => payload.json())
.then(items => {
//some fetching logic that populates posts list above
})
.then(() => { dispatch(receiveNews(posts)) })
.catch(err => {
console.log(err)
})
}
function request () {
return {
type: REQUEST_NEWS,
payload: {
populating: true
}
}
}
function receive (posts) {
return {
type: RECEIVE_NEWS,
payload: {
populating: false,
posts
}
}
}
As you can see above I'm setting the store with a field called populating which starts as false and changes to true when the 'request' is dispatched and then back to false when 'received' is dispatched.
Then my component looks something like the following:
import { populateNews } from '../modules/news'
class News extends React.Component {
constructor (props) {
super(props)
//this.mapPosts = this.mapPosts.bind(this)
}
componentWillMount () {
populateNews(this.props.dispatch)
}
render () {
if (!this.props.news.populating) {
return (
<div>
{this.props.news.posts[0].title}
</div>
)
} else {
return (
<div>loading</div>
)
}
}
}
On initial load render is being called before the store is populated with the fetched posts even though my populate switch changes back and forth as expected.
I've tried dealing with it using a local state on the component, so it's constructor has: this.state = {populating: false} and then the action creator changes that, but got the same result.
So at the moment my solution is to instead check if the state has a slice called 'posts' which is being created after the content is fetched and if it does to render it. like so:
render () {
return (
<div>
{this.props.news.posts ? <div>{this.props.news.posts[0].title}</div> : null }
</div>
)
}
This of course just renders the component and then renders it again after the store is updated with the posts, and is not an optimal solution like waiting with the render until the fetch is completed and the store is populated.
There's a long discussion about it here:
https://github.com/reactjs/react-redux/issues/210
How can I delay or better put condition the render itself?
Hmm, you might want to change your if statement. Unless your store is initializing populating to true, it will be undefined on the initial load and will pass your if (!this.props.news.populating) validation which will try to render the post title. Change your condition to look for a truthy value rather than a falsey value and you should have more control over it.

props are undefined inside componentwillmount but shown inside render in typescript?

When props data are passed as props then it's undefined inside componentWillMount but defined inside render.
What might be the problem???
render:
public state: any = {
authority: [],
cid: "1",
data: [],
id: [],
menuTitle: []
};
public componentWillMount() {
var temp: any;
let url: String = "http://localhost:9000/getFieldTitle/";
fetch(url + this.state.cid + "")
.then((response) => response.text()).then((value) => {
let jsonObject = JSON.parse(value);
for (let index in jsonObject) {
for (let header in jsonObject[index]) {
temp = [];
if (header === "id") {
temp = this.state.id;
temp.push(jsonObject[index][header])
this.setState({ id: temp })
}
if (header === "menuTitle") {
temp = this.state.menuTitle;
temp.push(jsonObject[index][header])
this.setState({ menuTitle: temp })
}
if (header === "dataFormat") {
temp = this.state.data;
temp.push(jsonObject[index][header])
this.setState({ data: temp })
}
if (header === "authority") {
temp = this.state.authority;
temp.push(jsonObject[index][header])
this.setState({ authority: temp })
}
}
}
})
.catch(e => console.log(e));
}
public render() {
let row = []
for (let i = 0; i < this.state.authority.length; i++) {
row.push(<FormSection
key={i}
id={this.state.id[i]}
cid={this.state.cid[i]}
menuTitle={this.state.menuTitle[i]}
data={this.state.data[i]}
/>
)
}
return (
<div className="container-fluid">
{row}
</div>
);
}
FormSection.tsx:
<MenuSectionStructure data={this.props.data} check="check" />
MenuSectionStructure.tsx:
import * as React from "react";
export class MenuSectionStructure extends React.Component<any, any> {
public state: any = {
authority: [],
dataType: [],
fieldName: [],
};
constructor(props: any) {
super(props);
}
public componentWillMount() {
console.log(this.props.data) // Gives undefined
}
public render() {
return (
<div>{this.props.data}</div> //Gives value of this.props.data
);
}
}
I have shown all data
I think that your problem is definitely the one Max Sidwani commented. When you first load the parent component, you launch various setState in componentDidMount. Probably the header authority goes before the dataFormat one. This means that your component will re-render (and all its children) twice. The first time, authority.length will be an integer bigger than 0 and so the render will loop and try to render FormSection components where the data prop will be undefined because the dataFormat header hasn't already been processed and the data state is still []. Then, the data state is set and in the second re-render the data is not undefined. You can't watch two renders because the first one renders nothing and the second one happens inmediately after, but since you are using setState twice, render is being called twice (the first time with authority set and the second with data set). You can probably check this out with:
public componentWillUpdate() {
console.log(this.props.data) // Not undefined
}
in the MenuSectionStructure component.
You can solve it by setting both states at the same setState at the initial fetch or checking if data is not empty in the render.
Yes, I found the answer by sending the whole data as a single props and then parsing at the lowest component so that i could display all objects as a props in component as required. The problem was with sending multiple data as props and choosing one props as props length in loop which cause all problem since they all were all array and set state was hitting randomly causing the loop with unwanted sequence. However, sending the whole data as a single props and then looping inside came as a solution to my problem.
Thanks all for your contribution, which help me allot to visualize the reason of the scenario.

Resources