React component not carrying over functions from class when re-rendering - javascript

I'm attempting to build a Minesweeper game as practice using React. I have a main Minesweeper.js file with all of the functionality.
export class Tile {
constructor(board, pos) {
this.board = board;
this.pos = pos;
this.bombed = false;
this.explored = false;
this.flagged = false;
}
adjacentBombCount() {
let bombCount = 0;
this.neighbors().forEach(neighbor => {
if (neighbor.bombed) {
bombCount++;
}
});
return bombCount;
}
explore() {
if (this.flagged || this.explored) {
return this;
}
this.explored = true;
if (!this.bombed && this.adjacentBombCount() === 0) {
this.neighbors().forEach(tile => {
tile.explore();
});
}
}
neighbors() {
const adjacentCoords = [];
Tile.DELTAS.forEach(delta => {
const newPos = [delta[0] + this.pos[0], delta[1] + this.pos[1]];
if (this.board.onBoard(newPos)) {
adjacentCoords.push(newPos);
}
});
return adjacentCoords.map(coord => this.board.grid[coord[0]][coord[1]]);
}
plantBomb() {
this.bombed = true;
}
toggleFlag() {
if (!this.explored) {
this.flagged = !this.flagged;
return true;
}
return false;
}
}
Tile.DELTAS = [[-1, -1], [-1, 0], [-1, 1], [ 0, -1],
[ 0, 1], [ 1, -1], [ 1, 0], [ 1, 1]];
export class Board {
constructor(gridSize, numBombs) {
this.gridSize = gridSize;
this.grid = [];
this.numBombs = numBombs;
this.generateBoard();
this.plantBombs();
}
generateBoard() {
for (let i = 0; i < this.gridSize; i++) {
this.grid.push([]);
for (let j = 0; j < this.gridSize; j++) {
const tile = new Tile(this, [i, j]);
this.grid[i].push(tile);
}
}
}
onBoard(pos) {
return (
pos[0] >= 0 && pos[0] < this.gridSize &&
pos[1] >= 0 && pos[1] < this.gridSize
);
}
plantBombs() {
let totalPlantedBombs = 0;
while (totalPlantedBombs < this.numBombs) {
const row = Math.floor(Math.random() * (this.gridSize - 1));
const col = Math.floor(Math.random() * (this.gridSize - 1));
let tile = this.grid[row][col];
if (!tile.bombed) {
tile.plantBomb();
totalPlantedBombs++;
}
}
}
lost() {
let lost = false;
this.grid.forEach(row => {
row.forEach(tile => {
if (tile.bombed && tile.explored) {
lost = true;
}
});
});
return lost;
}
won() {
let won = true;
this.grid.forEach(row => {
row.forEach(tile => {
if (tile.flagged === tile.revealed || tile.flagged !== tile.bombed) {
won = false;
}
});
});
return won;
}
}
Then I have three React components: Board.jsx
import React from "react";
import Tile from "./tile";
class Board extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
{this.props.board.grid.map((row, idx) => {
return (<div key={idx}>
{row.map((tile, idx) => {
return (
<div key={idx}>
<Tile
tile={tile}
updateGame={this.props.updateGame}/>
</div>
)
})}
</div>)
})}
</div>
)
}
}
export default Board;
Tile.jsx
import React from "react";
class Tile extends React.Component {
constructor(props) {
super(props)
this.checkState = this.checkState.bind(this)
}
checkState() {
console.log(this)
if (this.props.tile.bombed) {
return "💣"
} else if (this.props.tile.explored) {
return (this.props.tile.adjacentBombCount())
} else if (this.props.tile.flagged) {
return "⚐"
} else {
return ""
}
}
render() {
return(
<div>{this.checkState()}</div>
)
}
}
export default Tile;
and finally a Game.jsx
import React from "react";
import * as Minesweeper from "../minesweeper"
import Board from "./board";
class Game extends React.Component {
constructor(props) {
super(props);
let board = new Minesweeper.Board(10, 5)
this.state = {
board: board
}
this.updateGame = this.updateGame.bind(this);
}
updateGame() {
}
render() {
return(
<Board
board={this.state.board}
updateGame={this.updateGame}/>
)
}
}
export default Game
Within Tile.jsx, I want the render to display adjacent bomb counts using the function from the main Tile js class (in minesweeper.js). This function is accessible when I first render my Tiles as React Components; however, if I change something (by going into components and updating), the function is no longer accessible. To help explain, here is the console.log of this.props.tile on the first render:
However, after re-rendering from updating a component, this is what this.props.tile looks like .
Therefore, it will cause an error when I try to add the bomb count, as it cannot find the function. Can someone explain why the function is disappearing and how I can access it when Components change? Thanks!

This was an issue with the React DevTools specifically, and it will be addressed in an update, see: https://github.com/facebook/react/issues/24781

Related

Cant Render React Componet, Error In my understanding or code?

So I have built a graph generator and have all the correct data on the props the problem is when I go to render the build component It logs
Object { "$$typeof": Symbol(react.element), type: createElement(), key: "1", ref: null, props: {…}, _owner: null, _store: {…}, … }
Here is the Code I am sure its something silly I am not understanding about the render.
i
mport React, { Component } from 'react'
import * as action from '../../actions'
import { connect } from 'react-redux'
import jsx from 'react-jsx'
import { bindActionCreators } from 'redux'
import {Modal} from './Modal'
#connect(
(flux, props) => {
return {
filters: flux.FilterStore,
ready: flux.FilterStore.ready,
presets: flux.DataStore.preSelectList,
graphs: flux.GraphStore.graphList
}
},
(dispatch, props) => {
dispatch(action.fetchGraphList())
return {
addDataReportGraphDetails: bindActionCreators(action.addDataReportGraphDetails, dispatch)
}
}
)
class RenderGraphPreview extends Component {
constructor(props) {
super(props)
this.state = {
running: {},
show:false,
graph:{}
}
this.data = 0;
}
//Error function for shorthand errors
throwError = (string = "Error", err = null) => {
throw new Error(`${string}:${err}`)
}
//simple print function to print objects and strings
p = (string, variable) => {
typeof variable === `object` ? variable = JSON.stringify(variable) : variable
console.log(`${string}:${variable}`)
}
showModal = e => {
this.state.show = true
}
generateGraph = ({ presets, filters, graphDetails }) => {
var reportProps = {
wasRunning: true,
companyName: filters.company_name,
companyVertical: filters.company_vertical,
graphTitle: jsx.client(`<div>${graphDetails.title}</div>`)(reportProps).props.children
}
this.data++
if (typeof reportProps.graphTitle == "object") {
reportProps.graphTitle = reportProps.graphTitle.join("")
}
if (!this.state.running) {
reportProps.wasRunning = false
this.state.running = true
}
if (graphDetails.graph) {
var Graph = React.createFactory(require(`../graphs/${graphDetails.graph}`).default);
var newGraphProps = {}
var graphPropKeys = Object.keys(graphDetails.props || {})
graphPropKeys.map((graphKey) => {
if (graphDetails.props[graphKey] && graphDetails.props[graphKey].toString().length > 0)
newGraphProps[graphKey] = graphDetails.props[graphKey]
})
if (graphDetails.timeframe) {
newGraphProps[timeframe] = graphDetails[timeframe]
}
if (graphDetails.props.attackIndexFilterPreset) {
let preset;
for (let j = 0, jEnd = presets.length; j < jEnd; j++) {
if (presets[j]._id == graphDetails.props.attackIndexFilterPreset) {
return preset = presets[j]
}
}
if (preset) {
console.log(`In presents`)
newGraphProps = { ...preset, ...newGraphProps }
}
}
}
// console.log(<Graph key={this.state.count++} isDocument={true} reportKey={graphDetails.key} onImageCreated={this.props.addDataReportGraphDetails} {...filters} {...reportProps} {...newGraphProps}/>)
return (
<Graph key={this.data} isDocument={true} reportKey={graphDetails.key} onImageCreated={this.props.addDataReportGraphDetails} {...filters} {...reportProps} {...newGraphProps}/>
)
}
//Verifies we have the correct data to build the graph
startGraphGeneration = async (e,{ props }) => {
e.preventDefault()
let require = this.props.filters && this.props.presets && props
if (!require) {
this.throwError()
}
let graphProps = {
presets: this.props.presets,
filters: this.props.filters,
graphDetails: props,
}
let g = await this.generateGraph(graphProps)
this.setState({
graph: g
});
console.log(g)
}
render() {
var x = this.state.graph
return (
<div>
<button onClick={(e) => this.startGraphGeneration(e,this.props)}>Preview Graph</button>
{this.state.graph ? <x/> : `Doing Noting`}
</div>
)
}
}
export default connect()(RenderGraphPreview)
In your render method you use this.state.graph. You set this variable to the value returned from generateGraph function, which returns rendered node, not a component
. And then you try to render this node as a component(<x/>), which doesn't work. Also in
generateGraph function console.log(g) shows you rendered component. So just return x in you render method instead:
render() {
var x = this.state.graph
return (
<div>
<button onClick={(e) => this.startGraphGeneration(e,this.props)}>Preview Graph</button>
{this.state.graph ? x : `Doing Noting`}
</div>
)
}

React infinite scroll component performance

I have written the following infinite scroll component in React:
import React from 'react'
import { uniqueId, isUndefined, hasVerticalScrollbar, hasHorizontalScrollbar, isInt, throttle } from '../../../js/utils';
export default class BlSimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props)
this.handleScroll = this.handleScroll.bind(this)
this.itemsIdsRefsMap = {}
this.isLoading = false
this.node = React.createRef()
}
componentDidMount() {
const {
initialId
} = this.props
let id
if (initialId) {
if (typeof initialId === "function") {
id = initialId()
}
else {
id = initialId
}
this.scrollToId(id)
}
}
componentDidUpdate(prevProps) {
if (
this.isLoading
&&
prevProps.isInfiniteLoading
&&
!this.props.isInfiniteLoading
) {
const axis = this.axis()
const scrollProperty = this.scrollProperty(axis)
const offsetProperty = this.offsetProperty(axis)
this.scrollTo(scrollProperty, this.node.current[offsetProperty])
this.isLoading = false
}
}
itemsRenderer(items) {
const length = items.length
let i = 0
const renderedItems = []
for (const item of items) {
renderedItems[i] = this.itemRenderer(item.id, i, length)
i++
}
return renderedItems
}
itemRenderer(id, i, length) {
const {
itemRenderer,
isInfiniteLoading,
displayInverse
} = this.props
let renderedItem = itemRenderer(id, i)
if (isInfiniteLoading) {
if (!displayInverse && (i == length - 1)) {
renderedItem = this.standardLoadingComponentWrapperRenderer(id, renderedItem)
}
else if (i == 0) {
renderedItem = this.inverseLoadingComponentWrapperRenderer(id, renderedItem)
}
}
const ref = this.itemsIdsRefsMap[id] || (this.itemsIdsRefsMap[id] = React.createRef())
return (
<div className="bl-simple-infinite-scroll-item"
key={id}
ref={ref}>
{renderedItem}
</div>
)
}
loadingComponentRenderer() {
const {
loadingComponent
} = this.props
return (
<div className="bl-simple-infinite-scroll-loading-component"
key={uniqueId()}>
{loadingComponent}
</div>
)
}
loadingComponentWrapperRenderer(id, children) {
return (
<div className="bl-simple-infinite-scroll-loading-component-wrapper"
key={id}>
{children}
</div>
)
}
standardLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
renderedItem,
this.loadingComponentRenderer()
])
}
inverseLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
this.loadingComponentRenderer(),
renderedItem
])
}
axis() {
return this.props.axis === 'x' ? 'x' : 'y'
}
scrollProperty(axis) {
return axis == 'y' ? 'scrollTop' : 'scrollLeft'
}
offsetProperty(axis) {
return axis == 'y' ? 'offsetHeight' : 'offsetWidth'
}
scrollDimProperty(axis) {
return axis == 'y' ? 'scrollHeight' : 'scrollWidth'
}
hasScrollbarFunction(axis) {
return axis == 'y' ? hasVerticalScrollbar : hasHorizontalScrollbar
}
scrollToStart() {
const axis = this.axis()
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ?
0
:
this.scrollDimProperty(axis)
)
}
scrollToEnd() {
const axis = this.axis()
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ?
this.scrollDimProperty(axis)
:
0
)
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(scrollPositionOrPropertyOfScrollable) ?
scrollPositionOrPropertyOfScrollable
:
scrollableContentNode[scrollPositionOrPropertyOfScrollable]
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView()
}
}
handleScroll() {
const {
isInfiniteLoading,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props
if (
this.props.onInfiniteLoad
&&
!isInfiniteLoading
&&
this.node.current
&&
!this.isLoading
) {
const axis = this.axis()
const scrollableContentNode = this.node.current
const scrollProperty = this.scrollProperty(axis)
const offsetProperty = this.offsetProperty(axis)
const scrollDimProperty = this.scrollDimProperty(axis)
const currentScroll = scrollableContentNode[scrollProperty]
const currentDim = scrollableContentNode[offsetProperty]
const scrollDim = scrollableContentNode[scrollDimProperty]
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(infiniteLoadBeginEdgeOffset) ?
infiniteLoadBeginEdgeOffset
:
currentDim / 2
let thresoldWasReached = false
let memorizeLastElementBeforeInfiniteLoad = () => { }
if (!displayInverse) {
thresoldWasReached = currentScroll >= (scrollDim - finalInfiniteLoadBeginEdgeOffset)
}
else {
memorizeLastElementBeforeInfiniteLoad = () => {
// TODO
}
thresoldWasReached = currentScroll <= finalInfiniteLoadBeginEdgeOffset
}
if (thresoldWasReached) {
this.isLoading = true
memorizeLastElementBeforeInfiniteLoad()
this.props.onInfiniteLoad()
}
}
}
render() {
const {
items
} = this.props
return (
<div className="bl-simple-infinite-scroll"
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}>
{this.itemsRenderer(items)}
</div>
)
}
}
And I use it like this in a chat app I am writing:
...
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
...
The problem is that as soon as I scroll until the thresold and onInfiniteLoad is called, before the loading spinner is showed and after the data has been loaded the scroll freezes and the component becomes unresponsive.
How can I resolve this issue?
When I render the spinner container and after the previous loaded messages, shouldn't React just append the new divs retaining the previously added items in order to maintain the component performant?
If not, what key concepts of React I am missing?
Thank you for your attention!
UPDATE: Here are the additional components:
BlOrderChat represents a chat window and renders BlSimpleInfiniteScroll:
import React from 'react'
import BlOrderChatMessage from './BlOrderChatMessage';
import { isEmpty, uniqueId } from '../../../js/utils';
import { chatSelector } from '../selectors';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';
import BlSimpleInfiniteScroll from '../../core/scroll/BlSimpleInfiniteScroll';
export default class BlOrderChat extends React.Component {
static BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX = 'blOrderChatMessage'
constructor(props) {
super(props)
this.messageRenderer = this.messageRenderer.bind(this)
this.infiniteScrollComponentRef = React.createRef()
}
scrollToBottom() {
this.infiniteScrollComponentRef.current && this.infiniteScrollComponentRef.current.scrollToStart()
}
messageRenderer(messageId, index, messages) {
const {
currentUser, chat
} = this.props
const message = messages[index]
const length = messages.length
const fromUser = chat.users.items[message.from_user_id]
const itemComponentRender = (children) => (
<div key={messageId}>
{children}
</div>
)
const messageIdAttr = `${BlOrderChat.BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX}${message.id}`
const renderMessageComponent = () => (
<BlOrderChatMessage id={messageIdAttr}
key={uniqueId() + message.id}
message={message.message}
sentUnixTs={message.sent_unix_ts}
currentUser={currentUser}
fromUser={fromUser}
usersInvolvedInChatLength={chat.users.order.length} />
)
let children = []
if (index === 0) {
// First message.
children = [
<div key={uniqueId()} className="bl-padding"></div>,
renderMessageComponent()
]
}
else if (index === length - 1) {
// Last message.
children = [
renderMessageComponent(onComponentDidMount),
<div key={uniqueId()} className="bl-padding"></div>
]
}
else {
// Message in the middle.
children = [
renderMessageComponent()
]
}
return itemComponentRender(children)
}
render() {
const {
chat: propsChat, isChatLoading,
currentUser
} = this.props
const chat = chatSelector(propsChat, currentUser)
const chatHasMessages = chat && !isEmpty(chat.messages)
return (
<div className="bl-order-chat">
<div className="bl-order-chat-header">
// ...
</div>
<div className="bl-order-chat-content">
{
(chatHasMessages &&
<div className="bl-order-chat-content-inner">
<div className="bl-order-chat-infinite-scroll">
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
</div>
</div>
)
||
(isChatLoading &&
<BlLoadingSpinnerContainer />
)
}
</div>
<div className="bl-order-chat-footer">
// ...
</div>
</div>
)
}
}
BlOrderChatBox, contains BlOrderChat:
import React from 'react'
import BlOrderChat from './BlOrderChat';
import BlAlert from '../../core/alert/BlAlert';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';
export default class BlOrderChatBox extends React.Component {
constructor(props) {
super(props)
this.node = React.createRef()
}
render() {
const {
ordId, currentChat,
isCurrentChatLoading, currentUser,
err
} = this.props
return (
<div className="bl-order-chat-box" ref={this.node}>
<div className="bl-order-chat-box-inner">
{
(err &&
<BlAlert type="error" message={err} />)
||
(currentChat && (
// ...
<div className="bl-order-chat-box-inner-chat-content">
<BlOrderChat ordId={ordId}
chat={currentChat}
isChatLoading={isCurrentChatLoading}
onLoadPreviousChatMessages={this.props.onLoadPreviousChatMessages}
currentUser={currentUser} />
</div>
))
||
<BlLoadingSpinnerContainer />
}
</div>
</div>
)
}
}
And here is the component which renders BlOrderChatBox (it is the topmost stateful component):
import React from 'react'
import { POSTJSON } from '../../../js/ajax';
import config from '../../../config/config';
import { newEmptyArrayAble, arrayToArrayAbleItemsOrder, arrayAbleItemsOrderToArray, mergeArrayAbles, newArrayAble, firstOfArrayAble, isArrayAble } from '../../../js/data_structures/arrayable';
export default class BlOrderChatApp extends React.Component {
static NEW_CHAT_ID = 0
static MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX = 30
constructor(props) {
super(props)
this.currentUser = globals.USER
this.lastHandleSendMessagePromise = Promise.resolve()
this.newMessagesMap = {}
this.typingUsersDebouncedMap = {}
// Imagine this comes from a database.
const chat = {
// ...
}
const initialState = {
chats: newArrayAble(this.newChat(chat)),
currentChatId: null,
shouldSelectUserForNewChat: false,
newChatReceivingUsers: newEmptyArrayAble(),
isChatListLoading: false,
isCurrentChatLoading: false,
popoverIsOpen: false,
popoverHasOpened: false,
err: void 0,
focusSendMessageTextarea: false,
newChatsIdsMap: {},
currentChatAuthActs: {},
BlOrderChatComponent: null,
}
this.state = initialState
this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
}
POST(jsonData, callback) {
let requestJSONData
if (typeof jsonData === "string") {
requestJSONData = {
action: jsonData
}
}
else {
requestJSONData = jsonData
}
return POSTJSON(config.ORDER_CHAT_ENDPOINT_URI, {
...requestJSONData,
order_chat_type: this.props.orderChatType,
}).then(response => response.json()).then(json => {
this.POSTResponseData(json, callback)
})
}
POSTResponseData(data, callback) {
if (data.err) {
this.setState({
err: data.err
})
}
else {
callback && callback(data)
}
}
newChat(chat) {
const newChat = {
id: (chat && chat.id) || BlOrderChatApp.NEW_CHAT_ID,
ord_id: this.props.ordId,
users: (chat && chat.users && (isArrayAble(chat.users) ? chat.users : arrayToArrayAbleItemsOrder(chat.users))) || newEmptyArrayAble(),
messages: (chat && chat.messages && (isArrayAble(chat.messages) ? chat.messages : arrayToArrayAbleItemsOrder(chat.messages))) || newEmptyArrayAble(),
first_message_id: (chat && chat.first_message_id) || null,
typing_users_ids_map: (chat && chat.typing_users_ids_map) || {},
}
return newChat
}
isChatNew(chat) {
return (
chat
&&
(chat.id == BlOrderChatApp.NEW_CHAT_ID || this.state.newChatsIdsMap[chat.id])
)
}
loadPreviousChatMessages(chatId, lowestMessageIdOrNull, makeChatIdCurrent) {
this.POST({
act: 'loadPreviousChatMessages',
chat_id: chatId,
lowest_message_id: lowestMessageIdOrNull,
max_number_of_messages_to_load: BlOrderChatApp.MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX
}, json => {
this.setState(prevState => {
const chat = prevState.chats.items[chatId]
const messages = arrayToArrayAbleItemsOrder(json.messages)
const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages)
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat))
return {
...(makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: prevState.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
}
})
})
}
loadPreviousChatMessagesIfNotAllLoaded(chatId) {
let lowestMessageIdOrNull
const chat = this.state.chats.items[chatId]
if (
!this.isChatNew(chat)
&&
(lowestMessageIdOrNull = (chat.messages.order.length && firstOfArrayAble(chat.messages).id) || null)
&&
lowestMessageIdOrNull != chat.first_message_id
) {
this.setState({
isCurrentChatLoading: true
}, () => {
this.loadPreviousChatMessages(chat.id, lowestMessageIdOrNull)
})
}
}
handleLoadPreviousChatMessages(chatId) {
this.loadPreviousChatMessagesIfNotAllLoaded(chatId)
}
// ...
render() {
const currentChat = this.state.chats.items[this.state.currentChatId] || null
const err = this.state.err
return (
<div className="bl-order-chat-app">
<BlOrderChatBox currentUser={this.currentUser}
chats={arrayAbleItemsOrderToArray(this.state.chats)}
currentChat={currentChat}
isCurrentChatLoading={this.state.isCurrentChatLoading}
onLoadPreviousChatMessages={this.handleLoadPreviousChatMessages}
err={err} />
</div>
)
}
}
I tried to remove all the irrelevant code to simplify the reading. Also here is the file which contains the chatSelector function (normalizes the chat array-able object) and the *ArrayAble* functions (an array-able object to me is basically an object which maps objects through their ids in items and has an order property which keeps the sort):
import { isUndefined, unshiftArray, findIndex } from "../utils";
export function chatSelector(chat, currentUser) {
const newChat = { ...chat }
newChat.messages = arrayAbleItemsOrderToArray(chat.messages).sort((a, b) => {
const sortByUnixTs = a.sent_unix_ts - b.sent_unix_ts
if (sortByUnixTs == 0) {
return a.id - b.id
}
return sortByUnixTs
})
newChat.users = arrayAbleItemsOrderToArray(chat.users).filter(user => user.id != currentUser.id)
return newChat
}
/**
* Given an array-able object, returns its array representation using an order property.
* This function acts as a selector function.
*
* The array-able object MUST have the following shape:
*
* {
* items: {},
* order: []
* }
*
* Where "items" is the object containing the elements of the array mapped by values found in "order"
* in order.
*
* #see https://medium.com/javascript-in-plain-english/https-medium-com-javascript-in-plain-english-why-you-should-use-an-object-not-an-array-for-lists-bee4a1fbc8bd
* #see https://medium.com/#antonytuft/maybe-you-would-do-something-like-this-a1ab7f436808
*
* #param {Object} obj An object.
* #param {Object} obj.items The items of the object mapped by keys.
* #param {Array} obj.order The ordered keys.
* #return {Array} The ordered array representation of the given object.
*/
export function arrayAbleItemsOrderToArray(obj) {
const ret = []
for (const key of obj.order) {
if (!isUndefined(obj.items[key])) {
ret[ret.length] = obj.items[key]
}
}
return ret
}
export function arrayToArrayAbleItemsOrder(array, keyProp = "id") {
const obj = newEmptyArrayAble()
for (const elem of array) {
const key = elem[keyProp]
obj.items[key] = elem
obj.order[obj.order.length] = key
}
return obj
}
export function newEmptyArrayAble() {
return {
items: {},
order: []
}
}
export function isEmptyArrayAble(arrayAbleObj) {
return !arrayAbleObj.order.length
}
export function mergeArrayAbles(arrayAbleObj1, arrayAbleObj2, prependObj2 = false) {
const obj = newEmptyArrayAble()
for (const key of arrayAbleObj1.order) {
if (isUndefined(arrayAbleObj1.items[key])) {
continue
}
obj.items[key] = arrayAbleObj1.items[key]
obj.order[obj.order.length] = key
}
for (const key of arrayAbleObj2.order) {
if (isUndefined(arrayAbleObj2.items[key])) {
continue
}
if (!(key in obj.items)) {
if (!prependObj2) {
// Default.
obj.order[obj.order.length] = key
}
else {
unshiftArray(obj.order, key)
}
}
obj.items[key] = arrayAbleObj2.items[key]
}
return obj
}
export function newArrayAble(initialItem = void 0, keyProp = "id") {
const arrayAble = newEmptyArrayAble()
if (initialItem) {
arrayAble.items[initialItem[keyProp]] = initialItem
arrayAble.order[arrayAble.order.length] = initialItem[keyProp]
}
return arrayAble
}
export function lastOfArrayAble(obj) {
return (
(
obj.order.length
&&
obj.items[obj.order[obj.order.length - 1]]
)
||
void 0
)
}
Thank you for your help. If there's something missing which I should have included, please, let me know!
UPDATE: Thanks to Sultan H. it has improved, though the scroll still blocks as soon as I get the reply from the server. See it here: https://streamable.com/3nzu0
Any idea on how to improve this behaviour further?
Thanks!
Here is an attempt to resolve the performance issue, it's not preferrable to do tasks inside the Arrow Function that calculates the new state, in this case, at loadPreviousChatMessages you are calculating stuff in the callback, which may yeild to a load while setting the state on that context.
Preferrable Changes, replace this.setState inside your function with this code, all I've done here is clear the context by moving all the tasks out:
const chat = this.state.chats.items[chatId];
const messages = arrayToArrayAbleItemsOrder(json.messages);
const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages);
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat));
const newState = {
...(
makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: this.state.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
};
this.setState(() => newState);
If that doesn't entirely solve the issue, can you tell if there was at least an improvment?

React - Why is componentDidMount event called instantly

I've playing around with animation implemented with reactjs.
In the app I created a car which drives around a track. On this track there are obstacles, which the car should recognize.
I'm using window.setInterval for the repeating events. Maybe this is not the best option, but actually I don't know how to do else.
Since some changes, there are multiple intervals running.
But actually I don't know the reason for it. Can anybody give me a hint, why the racer component is instantly running in componentdidmount event?
The Racer component is giving the current position and degree / ankle to the Track component. The Track component is storing these values in states and giving it to the Racer component as props. But this should not lead to instantly firing componentdidmount event of Racer component, or?
Here is my code:
App.js
import React, { Component } from 'react';
import Track from './components/track.js';
const uuidv1 = require('uuid/v1');
class App extends Component {
constructor(props) {
super(props);
this.state = {
obstacles: [
{
key: uuidv1(),
position: {
left: 500,
top:10,
},
width: 25,
height: 25,
},
{
key: uuidv1(),
position: {
left: 650,
top:60,
},
width: 25,
height: 25,
}
],
};
}
render() {
return (
<div className="App">
<Track width={800} height={100} obstacles={this.state.obstacles}>
</Track>
</div>
);
}
}
export default App;
Track.js
import React, { Component } from 'react';
import styled from 'styled-components';
import Racer from './racer.js';
import Obstacle from './obstacle';
import centralStrip from '../images/centralStrip.png';
const uuidv1 = require('uuid/v1');
class Track extends Component {
constructor(props) {
super(props);
this.state = {
racerCurrentPosition: {
top: 60,
left:150
},
racerDegree: 0,
};
}
componentDidMount() {
}
handleObstacleCheck(position, racerPosition) {
let obstacleFound = false;
obstacleFound = this.props.obstacles.map((obstacle) => {
let returnValue = false;
let obstacleRect = document.getElementById(obstacle.key).getBoundingClientRect();
if( position.right >= obstacleRect.left && position.right <= obstacleRect.right && racerPosition.top >= obstacleRect.top && racerPosition.bottom <= obstacleRect.bottom) {
returnValue = true;
}
return returnValue;
});
let isObstacleFound = false;
if(obstacleFound.indexOf(true) !== -1) {
isObstacleFound = true;
}
return isObstacleFound;
}
handleRacerPositionChange(position) {
this.setState({
racerCurrentPosition: position,
});
}
handleRacerDegreeChange(newDegree) {
this.setState({
racerDegree: newDegree,
});
}
render() {
return (
<TrackImage key={uuidv1()}
id="track"
width={this.props.width}
height={this.props.height}>
<Racer key={uuidv1()}
position={this.state.racerCurrentPosition}
onRacerPositionChange={this.handleRacerPositionChange.bind(this)}
degree={this.state.racerDegree}
onRacerDegreeChange={this.handleRacerDegreeChange.bind(this)}
obstacleFound={this.state.obstacleFound}
trackWidth={this.props.width}
trackHeight={this.props.height}
onObstacleCheck={this.handleObstacleCheck.bind(this)}
/>
{
this.props.obstacles.map((obstacle) => {
return (
<Obstacle key={obstacle.key}
id={obstacle.key}
position={obstacle.position}
width={obstacle.width}
height={obstacle.height}
/>
);
})
}
</TrackImage>
);
}
}
export default Track;
Racer.js
import React, { Component, Fragment } from 'react';
import styled from 'styled-components';
import HelperDistance from './helpers/distance.js';
import HelperCenterCar from './helpers/centerCar.js';
import racerImage from '../images/racer.png';
const uuidv1 = require('uuid/v1');
class Racer extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
intervalId: 0,
speed: 0,
helperForLeftPositioning: 0,
helperForTopPositioning: 0,
isMoving: false,
collision: false,
centerOfCarCoordinates: {
x: 25,
y: 12.5
},
obstacleFound: false,
};
this.start = this.start.bind(this);
this.move = this.move.bind(this);
}
componentDidMount() {
if(this.state.intervalId === 0) {
this.start();
}
}
componentWillUnmount() {
window.clearInterval(this.state.intervalId);
}
start() {
this.setState({
speed: 3,
isMoving: true,
}, () => {
this.createInterval();
});
}
stop() {
this.setState({
speed: 0,
isMoving: false,
}, () => {
window.clearInterval(this.state.intervalId);
});
}
move() {
if(this.state.obstacleFound === true) {
let newDegree;
if(this.props.degree === 0) {
newDegree = 360;
}
newDegree--;
this.props.onRacerDegreeChange(newDegree);
}
this.step();
}
step() {
if(this.state.isMoving) {
//...calculate new position
this.setState({
helperForTopPositioning: helperForTopPositioning,
helperForLeftPositioning: helperForLeftPositioning,
},() => {
let position = {
left: positionNewLeft,
top: positionNewTop
};
this.props.onRacerPositionChange(position);
});
}
}
createInterval = () => {
let intervalId = window.setInterval(() => {
this.move();
console.log("IntervalId: " + intervalId);
},100);
this.setState({
intervalId: intervalId,
})
}
handleDistanceChange(position) {
let racerRect = document.getElementById(this.state.key).getBoundingClientRect();
let obstacleFound = this.props.onObstacleCheck(position, racerRect);
if(this.state.obstacleFound !== obstacleFound) {
this.setState({
obstacleFound: obstacleFound
});
}
}
render() {
return (
<Fragment>
<Car key={this.state.key} id={this.state.key} position={this.props.position} degree={this.props.degree}>
<HelperCenterCar key={uuidv1()} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
<HelperDistance key={uuidv1()} onChange={this.handleDistanceChange.bind(this)} position={this.state.centerOfCarCoordinates} degree={this.props.degree} />
</Car>
</Fragment>
);
}
}
export default Racer;
The HelperCenterCar and HelperDistance are components, which helps to identify, if there is an obstacle in the way. I'll post just the code of HelperDistance, because here instantly state updates are fired.
HelperDistance.js
import React, { Component } from 'react';
import styled from 'styled-components';
const uuidv1 = require('uuid/v1');
class HelperDistance extends Component {
constructor(props) {
super(props);
this.state = {
key: uuidv1(),
};
}
componentDidMount() {
this.handleOnChange();
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.handleOnChange();
}
handleOnChange() {
let position = document.getElementById(this.state.key).getBoundingClientRect();
this.props.onChange(position);
}
render() {
return (
<Line id={this.state.key} key={this.state.key} position={this.props.position} degree={this.props.degree} />
);
}
}
export default HelperDistance;

Issue with Saving and Reloading a Value Locally

So in one of my projects, I am attempting to add the functionality of saving track elements from the Spotify API to the session, then pulling them again. I inserted a console log to find out that even when the element from the session storage is undefined, it still enters the if block in the checkPlaylistName() method. The console log in the render statement is run twice, the first time, it passes an empty array, which is what I want when no elements are passed, the second time, it passes undefined for some reason. That's what causes an error in a different component, saying it is passed undefined. If you need the whole repo, you can find it here. Otherwise, this is the code causing the issues:
import React from 'react';
import './App.css';
import SearchBar from '../SearchBar/SearchBar';
import SearchResults from '../SearchResults/SearchResults';
import Playlist from '../Playlist/Playlist';
import Spotify from '../../util/Spotify.js';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
playlistName: 'New Playlist',
playlistTracks: [],
term: ''
}
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
this.updatePlaylistName = this.updatePlaylistName.bind(this);
this.savePlaylist = this.savePlaylist.bind(this);
this.search = this.search.bind(this);
}
addTrack(track) {
const addingTrack = (track) => this.setState({playlistTracks: [...this.state.playlistTracks, track]});
addingTrack(track);
this.removeTrack(track, false);
sessionStorage.setItem("playlistTracks", this.state.playlistTracks);
}
removeTrack(track, removePlaylist) {
if(removePlaylist) {
const ids = this.collectIds(true);
let trackIndex = -1;
for(let i = 0; i < ids.length; i++) {
if (ids[i] === track.id) {
trackIndex = i;
}
}
if (trackIndex !== -1) {
const newPlaylist = this.state.playlistTracks;
newPlaylist.splice(trackIndex, 1);
this.setState({playlistTracks: newPlaylist});
this.search(this.state.term);
}
} else {
const ids = this.collectIds(false);
let trackIndex = -1;
for(let i = 0; i < ids.length; i++) {
if (ids[i] === track.id) {
trackIndex = i;
}
}
if (trackIndex !== -1) {
const newResults = this.state.searchResults;
newResults.splice(trackIndex, 1);
this.setState({searchResults: newResults});
}
}
sessionStorage.setItem("playlistTracks", this.state.playlistTracks);
}
collectIds(removePlaylist) {
let ids = [];
if(removePlaylist) {
this.state.playlistTracks.map(track => ids.push(track.id));
} else {
this.state.searchResults.map(track => ids.push(track.id));
}
return ids;
}
updatePlaylistName(name) {
this.setState({playlistName: name});
sessionStorage.setItem("playlistName", name);
}
savePlaylist() {
let trackURIs = [];
for(let i = 0; i < this.state.playlistTracks.length; i++) {
trackURIs.push(this.state.playlistTracks[i].uri);
}
Spotify.savePlaylist(this.state.playlistName, trackURIs);
this.setState({playlistName: 'New Playlist', playlistTracks: []});
sessionStorage.removeItem("playlistTracks");
sessionStorage.removeItem("playlistName");
}
async search(term) {
const results = await Spotify.search(term);
this.setState({searchResults: results});
const resultIds = this.collectIds(false);
const playlistIds = this.collectIds(true);
let indexes = [];
for(let i = 0; i < resultIds.length; i++) {
for(let j = 0; j < playlistIds.length; j++) {
if (resultIds[i] === playlistIds[j]) {
indexes.push(i);
}
}
}
if(indexes.length > 0) {
for (let k = 0; k < indexes.length; k++) {
results.splice(indexes[k], 1);
}
}
this.setState({searchResults: results});
this.setState({term: term});
}
checkTracks() {
if (sessionStorage.getItem("playlistTracks") !== undefined) {
const tracks = sessionStorage.getItem("playlistTracks");
this.setState({playlistTracks: tracks});
}
return this.state.playlistTracks;
}
checkPlaylistName() {
const savedName = sessionStorage.getItem("playlistName");
if (savedName !== null || savedName !== undefined) {
this.setState({playlistName: savedName});
console.log("hi");
}
return this.state.playlistName;
}
render() {
return (
<div id="root">
<h1>Ja<span className="highlight">mmm</span>ing</h1>
<div className="App">
<SearchBar onSearch={this.search} />
<div className="App-playlist">
<SearchResults searchResults={this.state.searchResults} onAdd={this.addTrack} onRemove={this.removeTrack} />
{console.log(this.checkTracks())}
<Playlist
playlistName={this.checkPlaylistName()}
playlistTracks={this.checkTracks()}
onRemove={this.removeTrack}
onNameChange={this.updatePlaylistName}
onSave={this.savePlaylist}
/>
</div>
</div>
</div>
);
}
}
export default App;
setState function is asynchronous (https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b), therefore you are returning old state in checkTracks method.
Solution:
Don't setState in your checkTracks method, make it:
if (sessionStorage.getItem("playlistTracks") !== undefined) {
return sessionStorage.getItem("playlistTracks");
}
return [];
And use this method in constructor to define state.
Also in your way you are setting state (via checkTracks method) in render function which causes an infinite loop.

ReactJS: prepare data before render

I am creating a pagination component and it's have piece of prepare logic before render.
import React, {PropTypes, Component} from "react";
import PaginationItemComponent from "scripts/components/paginationItemComponent";
const MAX_ITEMS = 10;
const prepareShiftItems = (activeShift, pagesCount) => {
let toLeftCaption, toRightCaption, endPage;
const startPage = activeShift * MAX_ITEMS + 1;
const end = (activeShift + 1) * MAX_ITEMS;
if (end >= pagesCount) {
toRightCaption = null;
endPage = pagesCount;
} else {
endPage = end;
toRightCaption = "-->";
}
if (activeShift === 0) {
toLeftCaption = null;
} else {
toLeftCaption = "<--";
}
let activePages = [];
for (let i = startPage; i <= endPage; i++) {
activePages.push(i);
}
return {
toLeftCaption,
toRightCaption,
activePages
};
};
const getActiveShiftByPageNumber = (activePageNumber) => {
const div = (activePageNumber - 1) / MAX_ITEMS;
return Math.floor(div);
};
class PaginationComponent extends Component {
constructor(props) {
super(props);
this.state = {
activePageNumber: props.activePageNumber,
activeShift: getActiveShiftByPageNumber(props.activePageNumber)
};
}
componentWillReceiveProps(nextProps) {
this.setState({
activePageNumber: nextProps.activePageNumber,
activeShift: getActiveShiftByPageNumber(nextProps.activePageNumber)
});
}
setActivePage(activePageNumber) {
if (this.state.activePageNumber === activePageNumber) {
return;
}
const {onChange} = this.props;
if (onChange) {
onChange(activePageNumber);
}
this.setState({
activePageNumber
});
}
moveToRight() {
this.setState({
activeShift: this.state.activeShift + 1
});
}
moveToLeft() {
this.setState({
activeShift: this.state.activeShift - 1
});
}
render() {
if (this.props.pagesCount < 2) {
return null;
}
const {activePages, toLeftCaption, toRightCaption} =
prepareShiftItems(
this.state.activeShift,
this.props.pagesCount
);
return (
<ul className="pagination-component">
{
toLeftCaption &&
<li onClick={this.moveToLeft.bind(this)} className="to-left">
{toLeftCaption}
</li>
}
{
activePages.map((pageNumber) =>
<PaginationItemComponent
key={pageNumber}
pageNumber={pageNumber}
onClick={this.setActivePage.bind(this, pageNumber)}
isActive={pageNumber === this.state.activePageNumber}/>)
}
{
toRightCaption &&
<li onClick={this.moveToRight.bind(this)} className="to-right">
{toRightCaption}
</li>
}
</ul>
);
}
}
const {number, func} = PropTypes;
PaginationComponent.propTypes = {
pagesCount: number.isRequired,
activePageNumber: number.isRequired,
onChange: func
};
export default PaginationComponent;
My question is: do i need to call prepareShiftItems first time in constructor and then in componentWillReceiveProps or i can leave it as it is. I want to know the best practice for such cases. Thanks for answers.

Categories

Resources