I have small ReactJS chat application, messages are stored inside global array client.chat as object with propery txt, which contains text. The problem is perfomance, on ~2K messages I have to wait for few seconds after each new message even if networking is commented out, so its simply rerendeing of HTML. I tried to implement same app in AngularJS and it has no delay at all. So where is the bottleneck here?
var client = {user: {}, chat: []};
class App extends React.Component {
constructor() {
super();
this.socketio = ...
// here i perform some network initialization and
// call client.updateChat when data has arrived over network
client.updateChat = function () {
me.setState({chat: client.chat.concat([]).reverse()});
};
client.addMessage = function (msg) {
me.setState(prevState=>({
chat:[msg].concat(prevState.chat)
}));
};
this.updateState = this.updateState.bind(this);
this.state = {chat: []};
}
updateState(e) {
this.setState({data: e.target.value});
}
render() {
return (
<span>
<Input socketio={this.socketio} visitorId={this.visitorId}/>
<table>
<Chat data={this.state.chat} socketio={this.socketio}>
</Chat>
</table>
</span>
);
}
}
This is Input component, representing essentially input box:
class Input extends React.Component {
constructor() {
super();
this.state = {inputValue: ''};
this.updateInputValue = this.updateInputValue.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.getFile = this.getFile.bind(this);
}
getFile(e) {/* attachment handling */ }
handleKeyPress(target) {
if (target.charCode == 13) {
if (this.state.inputValue.length == 0) return;
var inputValue = this.state.inputValue;
this.setState({inputValue: ''});
var ts = Date.now();
var elem = {
txt: inputValue, file: null, ts: ts, from: 'support', tsStr: formateDate(ts), name: client.user.name, attachmentName: null, dataType: null
};
client.addMessage(elem);
}
}
updateInputValue(evt) {
this.setState({inputValue: evt.target.value});
}
render() {
return (<div className="input">
<table width="100%">
<tbody>
<tr>
<td>
<label className="customUpload btnUpload btnM">
<span><img width="15px" src="images/attach.png"/></span>
<input onChange={this.getFile} id="fileUpload" type="file" className="upload"/>
</label>
</td>
<td>
<input value={this.state.inputValue}
onKeyPress={this.handleKeyPress}
onChange={this.updateInputValue}
className="textInput"/>
</td>
</tr>
</tbody>
</table>
</div>);
}
}
This is chat component, representing chat itself.
class Chat extends React.Component {
constructor() {super();}
render() {
return (
<tbody>
{this.props.data.map((p, i) => <Msg socketio={this.props.socketio} key={i} data={p}/>)}
</tbody>
);
}
}
This is a single message:
class Msg extends React.Component {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.data.txt == nextProps.data.txt) {
return false;
}
return true;
}
render() {
var words = (this.props.data.txt || "").split(" ");
// this piece of code splits this.props.data.txt by spaces
// and converts it to array of words warr
// ...
return (
<tr className={this.trStyle()}>
<td>
<div className="msg">
{
warr.map((word, k) => <Word socketio={this.props.socketio} key={k} data={word.txt}/>)
}
</div>
</td>
</tr>
);
}
}
This class is for a single word inside a message. If a word is too long, function shortened() returns abbreviated version.
class Word extends React.Component {
shortened() { //....
render() {
return (
<span className={this.wordClass()} onClick={this.click}>{this.shortened()} </span>
);
}
}
I have implemented adding new messages using concat() instead of push(), following perfomance guidelines from Facebook and also implemented shouldComponentUpdate to avoid txt.split(" ") recalculation. But it didn't give me any perfomance boost. Can anybody give me some more advice or idea?
What is the size of response object which your are binding with react? we had similar issue,we had earlier 120kb size of json object which we were binding with react but later on optimize the json object and devided the json object in two part. 1. Pageload JSON object -It was having complete Json object with all fields and values(default)
2. Delta object- It was having only changed object field and it's value which we use to merge in React
Related
hope you all are safe. here im stuck in a problem.here i fetched users and their profile links from the site drupal.org using RegEx. but i got another links also.i want to fetch their profile links only and match the users and their profile links and show data in table is it possible in React js? currently it look like this
i want the data look like this :
please help me, thank you.. here is my git repo https://gitlab.com/darshankoli2397/react-practice.git
import React,{ Component } from "react"
import Axios from "axios"
class UserList extends Component {
constructor(prop) {
super(prop);
this.onChangeUserName = this.onChangeUserName.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = { users: null, profile:"", name:"" }
}
onChangeUserName(e) {
this.setState({ name: e.target.value })
}
onSubmit(e) {
e.preventDefault()
const userObject = {
name: this.state.name,
};
Axios.get(`https://www.drupal.org/search/user/${this.state.name}`).then(
response => {
if (response.status === 200) {
// all is good
console.log('all is good');
console.log(response);
var olList = response.data.match(/(?<=\<ol class="search-results user-results"\>\s+).*?(?=\s+\<\/ol)/gs);
var links = response.data.match(
/(\b(https?):\/\/[-A-Z0-9+&##\\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/gi
);
//preg_match_all('|https?://(?:www\.)?twitter.com/#!/[a-z0-9_]+|im', $text, $matched)
this.setState({ users: olList });
this.setState({ profile: links });
} else {
console.log('something is wrong');
// something is wrong
}
}
);
}
render() {
return ( <React.Fragment>
<h1 > Search Here users of Drupal < /h1>
<form align = "center" onSubmit = { this.onSubmit } >
<input type = "search" name = "name" onChange = { this.onChangeUserName } placeholder = "Search" required / >
<button type = "submit" > Search < /button >
</form >
<h3>Relevent Search Results For Your Keywords : </h3>
<table border="2" height="100" width="300">
<tr>
<td align="center"><h2>Username</h2></td>
<td><h2>Profile Links</h2></td>
</tr>
<tr>
<td dangerouslySetInnerHTML = {
{ __html: this.state.users }
} />
<td align="char" dangerouslySetInnerHTML = {
{ __html: this.state.profile }
} />
</tr>
</table>
</React.Fragment>
);
}
}
export default UserList;
You can extract the users as anchors, to be sure that every user has it's own link:
Example: asdf
let userAnchors = response.data.match(/\<a href="(https?):\/\/www.drupal.org\/user\/\w*">[\w\s]*\<\/a\>/gi)
I found this 2 regex only that you can use to extract the name an url:
const nameExtractorRegex = /<a[^>]*>([^<]+)<\/a>/g
const linkExtractorRegex = /href="(.*?)"/g
The only thing remained is to parse the anchors
const users = userAnchors.map(anchor => {
return {
name: nameExtractorRegex(anchor)[1],
link: linkExtractorRegex(anchor)[1]
}
})
Hope it helped!
I haven't seen a post to answer this question yet, so forgive me if I overlooked something. I'm trying to populate a table with ONLY the nested array from my data on the page. this.state.data SHOULD contain a subCollection of "masterTicketItems" from the "master ticket" that is being into this React JSX script from the Razor View.
The trouble comes when I'm trying to optimistically update a given table, and ONLY focusing on that, rather than actively posting data. I've been trying look around to understand this ReactJS and ES6 "spread" mapping technique, and I'm not sure if that applies here or something different.
In essence, I'm trying to ONLY map a "LogList" component with a collection contained INSIDE "this.state.data" inside the parent "Ticket" component. I currently have if/else logic to detect if there is nothing in the collection yet, and if not, try to populate it with a SINGLE object from the child component that was "posted" from the LogForm component. However, I can't seem to get the page to render with the single child in the table! Here's the Ticket JSX file snippet for review:
class Ticket extends React.Component {
constructor(props) {
super(props);
this.state = { data: this.props.initialData };
this.handleLogSubmit = this.handleLogSubmit.bind(this);
}
loadLogsFromServer() {
const xhr = new xmlhttprequest();
xhr.open('get', this.props.geturl + this.props.id, true);
xhr.onload = () => {
const data = json.parse(xhr.responsetext);
this.setState({ data: data });
};
xhr.send();
}
handleLogSubmit(log) {
const logs = this.state.data.apptConfirmItems;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// use a more robust system for ID generation.
if (!logs) {
log.ticketItemId = 1;
const newLogs = log;
let masterItemsAccess = Object.assign({}, this.state.data);
masterItemsAccess.masterTicketItems = log;
this.setState((data) => Object.assign({}, data, { masterTicketItems: [log] }));
}
else {
log.ticketItemId = logs.length + 1;
const newLogs = logs.concat([log]);
this.setState({ data: newLogs });
}
const data = new FormData();
data.append('ItemType', log.itemType);
data.append('ItemDescription', log.text);
const xhr = new XMLHttpRequest();
//xhr.open('post', this.props.submitUrl, true);
//xhr.onload = () => this.loadLogsFromServer();
//xhr.send(data);
}
componentDidMount() {
window.setInterval(() => this.loadLogsFromServer(), this.props.pollInterval);
}
render() {
if (!this.state.data.masterTicketItems || this.state.data.masterTicketItems.Count == 0) {
return (
<div className="queue">
<h1>Affirm Logs</h1>
<h2>{this.state.data.summary}</h2>
<h5><i>{this.state.data.description}</i></h5>
<div><h3>No logs at this time!</h3></div>
<LogForm onLogSubmit={this.handleLogSubmit} />
</div>
);
}
else {
return (
<div className="queue">
<h1>Affirm Logs</h1>
<h2>{this.state.data.summary}</h2>
<h5><i>{this.state.data.description}</i></h5>
<LogList data={this.state.data.masterTicketItems}/>
<LogForm onLogSubmit={this.handleLogSubmit} />
</div>
);
}
}
}
class LogList extends React.Component {
render() {
const logNodes = this.props.data.masterTicketItems.map(log => (
<Log key={log.ticketItemId}>
<td>{log.itemType}</td> < td > {log.itemDescription}</td>
</Log>
));
const logRaw = this.props.data.masterTicketItmes.map(log => (
<tr>
<td>{log.itemType}</td><td>{log.itemDescription}</td>
</tr>
));
return (
<div className="logList">
<table id="affirmTable" className="table table-bordered table-hover table-striped">
<tbody>
{logNodes}
</tbody>
</table>
</div>
);
}
}
class LogForm extends React.Component {
constructor(props) {
super(props);
this.state = { itemType: 'Test Log', itemDescription: '' };
this.handleItemTypeChange = this.handleItemTypeChange.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleItemTypeChange(e) {
this.setState({ itemType: e.target.value });
}
handleTextChange(e) {
this.setState({ itemDescription: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
const itemType = this.state.itemType.trim();
const itemDescription = this.state.itemDescription.trim();
if (!itemType || !itemDescription) {
return;
}
this.props.onLogSubmit({ ItemType: itemType, ItemDescription: itemDescription });
this.setState({ itemType: '', text: '' });
}
render() {
return (
<form className="logForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Your name"
value={this.state.itemType}
onChange={this.handleItemTypeChange}
/>
<input
type="text"
placeholder="Enter working log here..."
value={this.state.itemDescription}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
}
I purposely left out the child Log component, since I'm confident that when this is addressed that component WILL populate with the table data as expected.
Should I use nested mapping with ES6? Is there a simpler way? I'm trying to understand React state logic a little bit better.
I've been working through the ReactJS demo for ASP.NET Core and I'm struggling with an error message:
Warning: Each child in an array or iterator should have a unique "key" prop.
Check the render method of CommentList. See url for more information.
in Comment (created by CommentList)
in CommentList (created by CommentBox)
in div (created by CommentBox)
in CommentBox
The message is clear, every child of an array needs a key. The code assigns a key however and having downloaded the react console for Chrome I can also see the array and all the data that is added.
In my code I have the following:
class CommentList extends React.Component {
render() {
const commentNodes = this.props.data.map(comment => (
<Comment author={comment.author} key={comment.id}>
{comment.author}
</Comment>
));
return (
<div className="commentList">
{commentNodes}
</div>
);
}
}
You can see that the key is assigned to the comment component and returned to the comment list. The id doesn't appear to be null so I'm confused as to why I'm still getting this error message.
Can help me with where I am going wrong with this?
Here is my complete source code so far:
js/app.jsx
class CommentBox extends React.Component {
constructor(props) {
super(props);
this.state = { data: [] };
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
}
loadCommentsFromServer() {
const xhr = new XMLHttpRequest();
xhr.open('get', this.props.url, true);
xhr.onload = () => {
const data = JSON.parse(xhr.responseText);
this.setState({ data: data });
};
xhr.send();
}
handleCommentSubmit(comment) {
const comments = this.state.data;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// use a more robust system for ID generation.
comment.Id = comments.length + 1;
const newComments = comments.concat([comment]);
this.setState({ data: newComments });
const data = new FormData();
data.append('author', comment.author);
data.append('text', comment.text);
const xhr = new XMLHttpRequest();
xhr.open('post', this.props.submitUrl, true);
xhr.onload = () => this.loadCommentsFromServer();
xhr.send(data);
}
componentDidMount() {
this.loadCommentsFromServer();
window.setInterval(() => this.loadCommentsFromServer(), this.props.pollInterval);
}
render() {
return (
<div className="commentBox card">
<h4>Comments</h4>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
}
class CommentList extends React.Component {
render() {
const commentNodes = this.props.data.map(comment => (
<Comment author={comment.author} key={comment.id}>
{comment.author}
</Comment>
));
return (
<div className="commentList">
{commentNodes}
</div>
);
}
}
class CommentForm extends React.Component {
constructor(props) {
super(props);
//Initial state?
this.state = { author: '', text: '' };
//Event handlers
this.handleAuthorChange = this.handleAuthorChange.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleAuthorChange(e) {
this.setState({ author: e.target.value });
}
handleTextChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
const author = this.state.author.trim();
const text = this.state.text.trim();
//If inputs are null then return nothing.
if (!text || !author) {
return;
}
//Post data to the server
this.props.onCommentSubmit({ author: author, text: text });
//Clear form
this.setState({ author: '', text: '' });
}
render() {
return (
<div className="commentForm">
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" value={this.state.author} onChange={this.handleAuthorChange} />
<input type="text" placeholder="Say something..." value={this.state.text} onChange={this.handleTextChange} />
<input type="submit" value="Post" />
</form>
</div>
);
}
}
class Comment extends React.Component {
render() {
return (
<div className="comment">
<p className="commentAuthor">
{this.props.author}
</p>
</div>
);
}
}
ReactDOM.render(
<CommentBox url="/comments" submitUrl="/comments/new" pollInterval={2000} />,
document.getElementById('content')
);
I am using a model for my data as I'll be introducing this to a repository later.
Models/CommentModel
namespace ReactDemo.Models
{
public class CommentModel
{
public int Id { get; set; }
public string Author { get; set; }
public string Text { get; set; }
}
}
Controllers/HomeController
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ReactDemo.Models;
namespace ReactDemo.Controllers
{
public class HomeController : Controller
{
private static readonly IList<CommentModel> _comments;
static HomeController()
{
_comments = new List<CommentModel>
{
new CommentModel
{
Id = 1,
Author = "Daniel Lo Nigro",
Text = "Hello ReactJS.NET World!"
},
new CommentModel
{
Id = 2,
Author = "Pete Hunt",
Text = "This is one comment"
},
new CommentModel
{
Id = 3,
Author = "Jordan Walke",
Text = "This is *another* comment"
},
};
}
public IActionResult Index()
{
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
[Route("comments")]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public ActionResult Comments()
{
return Json(_comments);
}
[Route("comments/new")]
[HttpPost]
public ActionResult AddComment(CommentModel comment)
{
// Create a fake ID for this comment
comment.Id = _comments.Count + 1;
_comments.Add(comment);
return Content("Success :)");
}
}
}
You could also use shortid for unique keys as an alternate to id, this way even if you don't have unique id from json even then key would be unique.
var shortid = require('shortid');
function createNewTodo(text) {
return {
completed: false,
id: shortid.generate(),
text
}
}
Use Id instead of id for your key value, as javascript is case sensitive. Also add .toString()
'Id' is being set here:
comment.Id = comments.length + 1;
Updated jsx code:
<Comment author={comment.author} key={comment.Id.toString()}> {comment.author} </Comment> ));
From React documentation:
"The best way to pick a key is to use a string that uniquely identifies a list item among its siblings. Most often you would use IDs from your data as keys:"
Your id's are probably not unique. React mostly doesn't recommend using incremental ID's. The ID's are used in the reconciliation process, see here:
https://reactjs.org/docs/reconciliation.html#keys
Try using author as a key, or even better, try generating unique ones (with a package like node-uuid, or see the method provided in #tarzen chugh his answer.
As suggested by the good people in this question, the issue was being caused by the uppercase Id being set in handleCommentSubmit.
Was
handleCommentSubmit(comment) {
comment.Id = comments.length + 1;
}
Change to
handleCommentSubmit(comment) {
comment.id = comments.length + 1;
}
The confusion between uppercase and lowercase was being caused by my declaration of values in my model, I had written those in uppercase, however, the results returned by ReactJS are, of course, lowercase.
I'm trying to load items from JSON and toggle a dropdown div with description on click. While I can display elements sequentially (ex: loc1 & desc1, loc2 & desc2) on static divs I'm having trouble finding out how to render it properly when the second part (desc) is hidden and only shows when the loc div is clicked.
What would be the best way to map the result so it doesn't show as loc1 & loc2, desc1 & desc2 but as loc1 & desc1, loc2 & desc2?
Code:
var places = {
library: {
location: [
{
loc_name: "library1",
"desc": "desc1 : Modern and spacious building"
},
{
loc_name: "library2",
"desc": "desc2 : A cosy small building"
}
]
}
};
function contentClass(isShow) {
if (isShow) {
return "content";
}
return "content invisible";
}
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isShow: false };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(function (prevState) {
return { isShow: !prevState.isShow };
});
}
render() {
const libraries_desc = places.library.location.map((libr) =>
<div>
<p>{libr.desc}</p>
</div>
);
const lib_names = places.library.location.map((libr) =>
<div>
<p>{libr.loc_name}</p>
</div>
);
return (
<div>
<div className='control' onClick={this.handleClick}>
<h4>{lib_names}</h4>
<div className={contentClass(this.state.isShow)}>{libraries_desc}</div>
</div>
</div>
);
}
}
render((
<Toggle />
), document.getElementById('root'));
Current result:
library1
library2
desc1 : Modern and spacious building
desc 2 : A cosy small building
Desired Result:
library1
desc1 : Modern and spacious building (hidden but shown when clicked)
library2
desc 2 : A cosy small building (hidden but shown when clicked)
Codesandbox
I might try extracting a location into a separate component. By extracting it, each location is responsible for knowing its state. In your case, that means its visibility (controlled by this.state.isShow).
Here's how you could do it:
import React from 'react';
import { render } from 'react-dom';
var places = {
library: {
location: [
{
loc_name: "library1",
"desc": "Modern and spacious building"
},
{
loc_name: "library2",
"desc": "A cosy small building"
}
]
}
};
class Location extends React.Component {
constructor(props) {
super(props);
this.state = { isShow: false };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(function (prevState) {
return { isShow: !prevState.isShow };
});
}
contentClass(isShow) {
if (isShow) {
return "content";
}
return "content invisible";
}
render() {
return (
<div className='control' onClick={this.handleClick}>
<h4>{this.props.desc}</h4>
<div className={this.contentClass(this.state.isShow)}>{this.props.loc_name}</div>
</div>
)
}
}
class Toggle extends React.Component {
constructor(props) {
super(props);
}
render() {
const locations = places.library.location.map(location => {
return <Location {...location} />
})
return (
<div>
{locations}
</div>
);
}
}
render((
<Toggle />
), document.getElementById('root'));
Your Toggle Component should be like this.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {
isShow: false,
id: -1, // initial value
};
}
handleClick = (id) => {
this.setState({
isShow: !this.state.isShow,
id: id
});
}
render() {
const { location } = places.library;
const { isShow, id } = this.state;
return (
<div className="control">
{location.map((libr, index) => (
<div key={index} onClick={() => { this.handleClick(index) }}>
<p>{libr.loc_name}</p>
{(isShow && (id === index)) && <p>{libr.desc}</p>}
</div>
))}
</div>
);
}
}
So when you click on the div element. A click event will be triggered called handleClick which will pass the index as a param to the function. which will set isShow to false or truth and vice versa along with the current element you want to show which will be selected through this.state.id. So everytime isShow is true and this.state.id matched index element of the array. Your description will show otherwise it will be hidden as you want.
So your desired result will be something like this.
library1
desc1 : Modern and spacious building (hidden but shown when clicked)
library2
desc 2 : A cosy small building (hidden but shown when clicked)
I'm building a component which proceeds according to the selections of the users. I have completed it successfully but facing some issues when trying to implement a back button to go back.
My code is like follows.
class ReportMainCat extends Component {
constructor(props) {
super(props);
this.state = {
postType: null,
}
this.changeType = this.changeType.bind(this);
this.report_next = this.report_next.bind(this);
};
report_next() {
if (this.state.postType == null) {
return <ReportFirst changeType={this.changeType}/>
}
else if (this.state.postType === 'sexual') {
return <ReportXContent changeType={this.changeType}/>
} else if (this.state.postType === 'selfharm') {
return <ReportThreatContent changeType={this.changeType}/>
}
}
changeType = (postType) => {
this.setState({postType})
this.setState({
showMainReportCats: false,
})
}
render() {
return (
<div className="top_of_overlay">
<div className="section_container text_align_center padding_10px">
<a className="">Report Category</a>
{this.report_next()}
</div>
</div>
)
}
}
I'm binding the postType value as follows.
class ReportXContent extends Component {
constructor(props) {
super(props);
this.state = {
postType: '',
}
};
textType(postType) {
this.props.changeType(postType);
}
render() {
return (
<div className="text_align_left">
<div>
<div className="width_100 margin_bottom10px">
<input type="checkbox" ref="nudity" onClick={this.textType.bind(this,'nudity')}/>
<a>Nudity or Pornography</a>
</div>
<div className="width_100 margin_bottom10px">
<input type="checkbox" ref="minor" onClick={this.textType.bind(this,'minor')}/>
<a>Includes Minors</a>
</div>
</div>
<ReportButtons/>
</div>
)
}
}
My back button
<div>
<button className="float_right margin_left5px" onClick={this.props.back_report}>Back</button>
</div>
So basically what i'm trying to do is this.
Ex: If the user selects postType as sexual it will return the ReportXContent component. How can i return to the first page when the user clicks the back button.
Thanks.
You could implement the back button click handler like this in the ReportMainCat component:
handleBackClick() {
this.setState({ postType: null });
}
, and that would show the ReportFirst view again.
If you don't want the first view, but the last view, simply change your changeType implementation to save lastPostType to state like this:
changeType = (postType) => {
this.setState({
lastPostType: this.state.postType,
postType,
showMainReportCats: false,
});
}
Edit
If you want full history of changes - let's say if you want to implement a full back button history - you can simply rename lastPostType to postTypeHistory and implement it like a stack (like the browser history is):
changeType = (postType) => {
this.setState({
postTypeHistory: [...this.state.postTypeHistory, this.state.postType],
postType,
showMainReportCats: false,
});
}
handleBackClick() {
const { postTypeHistory } = this.state;
const postType = postTypeHistory.pop();
this.setState({
postType,
postTypeHistory,
});
}