Child component not updating after array prop updated - javascript

When updating an array (inside an object), by adding an object to it, the child component is not re-rendered. The parent component is, however.
I tried updating a non-array property of the object, while also updating the array property of the object, the child component will then update. E.g:
Does not work:
obj.arr.push(user);
Works:
obj.arr.push(user);
obj.test = "wow";
My problem exists with the users prop, passed to the Users component from the Lobby component. When a user joins, the socket event lobby_player_joined is triggered, modifying the users array.
Lobby component (parent):
...
const StyledTabs = styled(Tabs)`${TabsStyle};`;
class Lobby extends Component {
constructor(props) {
super(props);
this.state = {
tab: 0,
};
this.props.setTitle('Lobby');
}
static get propTypes() {
return {
history: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
location: PropTypes.shape({ state: PropTypes.object }).isRequired,
setTitle: PropTypes.func.isRequired,
initializeSocket: PropTypes.func.isRequired,
onceSocketMessage: PropTypes.func.isRequired,
onSocketMessage: PropTypes.func.isRequired,
sendSocketMessage: PropTypes.func.isRequired,
};
}
async componentDidMount() {
await this.props.initializeSocket((error) => {
console.error(error);
});
await this.props.onSocketMessage('exception', (error) => {
console.log(error);
});
await this.props.onceSocketMessage('lobby_joined', (lobby) => {
this.setState({ lobby });
});
await this.props.sendSocketMessage('lobby_join', {
id: this.props.location.state.id,
password: this.props.location.state.password,
});
await this.props.onSocketMessage('lobby_player_joined', (user) => {
const { lobby } = this.state;
lobby.users.push(user);
return this.setState({ lobby });
});
await this.props.onSocketMessage('lobby_player_left', (user) => {
const { lobby } = this.state;
const userIndex = lobby.users.findIndex(u => u.id === user.id);
if (userIndex !== -1) {
lobby.users.splice(userIndex, 1);
this.setState({ lobby });
}
});
await this.props.onSocketMessage('lobby_new_host', (host) => {
const { lobby } = this.state;
lobby.host = host;
return this.setState({ lobby });
});
}
handleTab = (event, value) => {
console.log(this.state.lobby);
this.setState({ tab: value });
};
handleSwipe = (value) => {
this.setState({ tab: value });
};
render() {
if (!this.state.lobby) {
return (<div> Loading... </div>);
}
return (
<Container>
<AppBar position="static">
<StyledTabs
classes={{
indicator: 'indicator-color',
}}
value={this.state.tab}
onChange={this.handleTab}
fullWidth
centered
>
<Tab label="Users" />
<Tab label="Info" />
</StyledTabs>
</AppBar>
<SwipeableViews
style={{ height: 'calc(100% - 48px)' }}
containerStyle={{ height: '100%' }}
index={this.state.tab}
onChangeIndex={this.handleSwipe}
>
<TabContainer>
<Users
{...this.state.lobby}
/>
</TabContainer>
<TabContainer>
<Info
{...this.state.lobby}
/>
</TabContainer>
</SwipeableViews>
</Container>
);
}
}
...
Users component (child):
...
class Users extends Component {
state = {
isReady: false,
usersReady: [],
};
async componentDidMount() {
await this.props.onSocketMessage('lobby_user_ready', (data) => {
this.setState(prevState => ({
usersReady: [...prevState.usersReady, data.socketId],
}));
});
await this.props.onSocketMessage('lobby_user_unready', (data) => {
this.setState(prevState => ({
usersReady: prevState.usersReady.filter(id => id !== data.socketId),
}));
});
}
componentWillUnmount() {
this.props.offSocketMessage('lobby_user_ready');
this.props.offSocketMessage('lobby_user_unready');
}
static get propTypes() {
return {
id: PropTypes.number.isRequired,
users: PropTypes.arrayOf(PropTypes.object).isRequired,
userCount: PropTypes.number.isRequired,
host: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
sendSocketMessage: PropTypes.func.isRequired,
onSocketMessage: PropTypes.func.isRequired,
offSocketMessage: PropTypes.func.isRequired,
};
}
readyChange = () => {
this.setState(prevState => ({ isReady: !prevState.isReady }), () => {
if (this.state.isReady) {
return this.props.sendSocketMessage('lobby_user_ready', { id: this.props.id });
}
return this.props.sendSocketMessage('lobby_user_unready', { id: this.props.id });
});
};
renderStar = (user) => {
const { host } = this.props;
if (host.username === user.username) {
return (<Icon>star</Icon>);
}
return null;
}
render() {
return (
<UserContainer>
{ this.props.users.length }
<CardsContainer>
{this.props.users.map(user => (
<UserBlock
className={this.state.usersReady.includes(user.socketId) ? 'flipped' : ''}
key={user.socketId}
>
<BlockContent className="face front">
{ this.renderStar(user) }
<div>{user.username}</div>
<Icon className="icon">
close
</Icon>
</BlockContent>
<BlockContent className="face back">
<Icon>
star
</Icon>
<div>{user.username}</div>
<Icon className="icon">
check
</Icon>
</BlockContent>
</UserBlock>
))}
</CardsContainer>
<InfoContainer>
<p>Players</p>
<p>
{this.props.users.length}
{' / '}
{this.props.userCount}
</p>
<p>Ready</p>
<p>
{this.state.usersReady.length}
{' / '}
{this.props.userCount}
</p>
</InfoContainer>
<StyledButton
variant={this.state.isReady ? 'outlined' : 'contained'}
color="primary"
onClick={this.readyChange}
>
{ this.state.isReady ? 'Unready' : 'ready'}
</StyledButton>
</UserContainer>
);
}
}
...
Could anyone help me out with making the Users component update/re-render when modifying the array prop?

Don't mutate the state. Use something like this
await this.props.onSocketMessage('lobby_player_joined', (user) => {
const { lobby } = this.state;
return this.setState({ lobby : {...lobby, users: lobby.users.concat(user)} });
});
edit: fixed missing bracket

This is because React compares props for equality to determine whether to re-render a component. Instead of
obj.arr.push(user);
Try
const newObj = {...obj, arr: obj.arr.concat(user)};
which creates a new Object.
An alternative is using Immutable.js

Related

React class component. Todo app. How to store data with localStorage

I'd like to store todo data with localStorage so that it won't disappear after refreshing the page.
I used React class component when started creating.
I've added 'handleFormSubmit' and 'ComponentDidMount' methods.
nothing stores in localStorage when I type todo and choose date.
get an error in ComponentDidMount with
Line 'const result = localData ? JSON.parse(localData) : [];'
:SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
how can I set and get items?
It would be really appreciated if I could get help.
I'd like to make this app really work.
import React from "react"
import TodoItem from "./components/TodoItem"
import todosData from "./components/todosData"
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
todos: todosData,
//setTodos: todosData,
newItem: "",
deadline: "",
editing: false
}
this.handleChange = this.handleChange.bind(this)
this.addTodo = this.addTodo.bind(this)
this.updateInput = this.updateInput.bind(this)
this.deleteItem = this.deleteItem.bind(this)
this.updateItem = this.updateItem.bind(this)
this.updateDeadline = this.updateDeadline.bind(this)
this.updateInputDeadline = this.updateInputDeadline.bind(this)
this.editItem = this.editItem.bind(this)
this.handleFormSubmit = this.handleFormSubmit.bind(this)
}
handleChange(id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
} else {
return todo;
}
});
return { todos: updatedTodos };
});
}
addTodo(e) {
e.preventDefault();
const newTodo = {
id: this.state.todos.length + 1,
text: this.state.newItem,
completed: false,
deadline: this.state.deadline
}
const newTodos = this.state.todos.concat([newTodo]);
this.setState({
todos: newTodos
})
}
updateInput(value, id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if(todo.id === id) {
return {...todo, text: value}
}else {
return todo;
}
})
return {todos: updatedTodos}
})
}
updateInputDeadline(value, id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if(todo.id === id) {
console.log(value, id);
return {...todo, deadline: value}
}else {
return todo;
}
})
return {todos: updatedTodos}
})
}
updateItem(e) {
this.setState({
newItem: e.target.value
})
}
updateDeadline(e) {
this.setState({
deadline: e.target.value
})
}
deleteItem(id){
const filteredItems= this.state.todos.filter(item =>
item.id!==id);
this.setState({
todos: filteredItems
})
}
editItem(id) {
this.setState({
editing: id
})
}
handleFormSubmit() {
const { todo, deadline } = this.state;
localStorage.setItem('todo', JSON.stringify(todo));
localStorage.setItem('deadline', deadline);
};
componentDidMount() {
const localData = localStorage.getItem('todo');
const result = localData ? JSON.parse(localData) : [];
const deadlineData = localStorage.getItem('deadline');
this.setState({ result, deadlineData });
}
render() {
const todoItems = this.state.todos.map
(item =>
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
addTodo={this.addTodo}
deleteItem={this.deleteItem}
updateInput={this.updateInput}
updateInputDeadline={this.updateInputDeadline}
isEdited={this.state.editing === item.id}
editItem={this.editItem}
/>)
return (
<div className="todo-list">
<Timer />
<form onSubmit={this.handleFormSubmit}>
<div className="add-todo">
<label>Add an item...</label>
<input
type="text"
name="todo"
placeholder="Type item here..."
value={this.state.newItem}
onChange={this.updateItem}
/>
</div>
<div className="date">
<label htmlFor="deadline">Deadline</label>
<input
type="date" id="start" name="deadline"
min="2021-01-01"
max="2024-12-31"
value={this.state.deadline}
onChange={this.updateDeadline}
/>
</div>
<button type="submit" onClick={this.addTodo}>Add to the list</button>
</form>
{todoItems.length === 0 ? <p>No items</p> : null}
<div className="todoitems">
{todoItems}
</div>
</div>
)
}
}
export default App
When you press the button, there are two events that you are trying to call - addTodo and handleFormSubmit. Since you are calling e.preventDefault() in addTodo, the submit event is never called. You could do all of the actions you need in one of the methods.
My guess is that you are either trying to JSON.parse an array instead of an object, or the value of todo is undefined. You are trying to get todo out of this.state, but you only have todos in your state, so it might be a typo. The same goes for deadline.
You are doing the setting and getting correctly. You could actually get data from localStorage even when you are first setting the state in constructor. But the componendDidMount approach you tried is also good.
constructor(props) {
super(props)
const cachedTodos = localStorage.getItem("todo")
this.state = {
todos: cachedTodos ?? todosData,
...
}

Using 2 dropdowns in React. Cannot read property 'map' of undefined

I'm having an issue with 2 dropdowns. I'm getting the following error: TypeError: Cannot read property 'map' of undefined.
Both dropdowns should work at the same time depending if the user wants to select a value from one of them or wants to select values from both dropdowns.
This is how I have implemented my 2 Dropdowns and how they should work. If I click the button I will get the error:
dropdown.js
import * as actionTypes from '../actions';
const initialState = {
selection: 'Ambos',
dropdownValues: ['Chatbot', 'Agente'],
selectionME: 'Todos',
dropdownValuesME: ['Messenger', 'Web', 'WhatsApp']
};
//
const reducer = ( state = initialState, action ) => {
if (typeof state === 'undefined') {
return initialState
}
switch ( action.type ) {
case actionTypes.UPDATE_SELECTION:
{
return {
...state,
selection: action.dataSelected,
dropdownValues: action.dropdownValues,
selectionME: action.dataSelectedME,
dropdownValuesME: action.dropdownValuesME
}
}
default: {
//statements;
break;
}
}
return state;
};
export default reducer;
DropdownSelect.js
import React, { Component } from 'react';
import { ButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import {connect} from 'react-redux';
import * as actionTypes from '../../store/actions'
class DropdownSelect extends Component {
constructor(props)
{
super(props);
console.log(props)
this.state = {
dropdownOpen: false,
solved: this.props.selectData,
dropdownValues: this.props.dropdownValues,
dropdownOpenME: false,
solvedME: this.props.selectDataME,
dropdownValuesME: this.props.dropdownValuesME,
}
}
componentDidMount() {
if(this.props.onRef)
this.props.onRef(this);
}
toggleDropdown = () => {
this.setState({
dropdownOpen: !this.state.dropdownOpen,
});
}
toggleDropdown2 = () => {
this.setState({
dropdownOpenME: !this.state.dropdownOpenME,
});
}
changeDropdownValue = async (event) => {
const currentSolved = this.state.solved
let newdropdownValues = this.state.dropdownValues.concat(currentSolved)
newdropdownValues = newdropdownValues.filter(item => item !== event.target.innerText)
await this.setState({
dropdownOpen: !this.state.dropdownOpen,
solved: event.target.innerText,
dropdownValues: newdropdownValues,
}, () => {
this.props.onUpdate(this.state.solved, this.state.dropdownValues);
});
}
changeDropdownValueME = async (event) => {
const currentSolvedME = this.state.solvedME
let newdropdownValuesME = this.state.dropdownValuesME.concat(currentSolvedME)
newdropdownValuesME = newdropdownValuesME.filter(item2 => item2 !== event.target.innerText)
await this.setState({
dropdownOpenME: !this.state.dropdownOpenME,
solvedME: event.target.innerText,
dropdownValuesME: newdropdownValuesME
}, () => {
this.props.onUpdate(this.state.solvedME, this.state.dropdownValuesME);
});
}
render() {
return (
<>
<ButtonDropdown isOpen={this.state.dropdownOpen} toggle={this.toggleDropdown}>
<DropdownToggle caret>
{this.state.solved}
</DropdownToggle>
<DropdownMenu>
{this.state.dropdownValues.map(e => {return <DropdownItem key={e} onClick={this.changeDropdownValue}>{e}</DropdownItem>})}
</DropdownMenu>
</ButtonDropdown>
<p className="text-uppercase text-left font-weight-bold -label">Tipo de atención</p>
<ButtonDropdown isOpen={this.state.dropdownOpenME} toggle={this.toggleDropdown2}>
<DropdownToggle caret>
{this.state.solvedME}
</DropdownToggle>
<DropdownMenu>
{this.state.dropdownValuesME.map(e => {return <DropdownItem key={e} onClick={this.changeDropdownValueME}>{e}</DropdownItem>})}
</DropdownMenu>
</ButtonDropdown>
</>
);
}
}
const mapStateToProps = state => ({
//selectData: state.dropDownReducer.selection || [],
//dropdownValues: state.dropDownReducer.dropdownValues || ['Ambos', 'Chatbot', 'Agente'],
selectData: state.dropDownReducer.selection,
dropdownValues: state.dropDownReducer.dropdownValues,
selectDataME: state.dropDownReducer.selectionME,
dropdownValuesME: state.dropDownReducer.dropdownValuesME
});
const mapDispatchToProps = dispatch => {
return {
onUpdate: (selected, dropdownValues) => dispatch({type: actionTypes.UPDATE_SELECTION, dataSelected: selected, dropdownValues:dropdownValues})
}
};
export default connect(mapStateToProps, mapDispatchToProps)(DropdownSelect);
hey Jorge try to add conditions before mapping the data
<DropdownMenu>
{this.state.dropdownValues && this.state.dropdownValues.map(e => {return <DropdownItem key={e} onClick={this.changeDropdownValue}>{e}</DropdownItem>})}
</DropdownMenu>
the same here
{this.state.dropdownValuesME && this.state.dropdownValuesME.map(e => {return <DropdownItem key={e} onClick={this.changeDropdownValueME}>{e}</DropdownItem>})}
and I believe you should not assign the state directly from the props, you should create the state as an empty array
then in the component did mount you should set the state with the new values
this.state = {
dropdownOpen: false,
solved: this.props.selectData,
dropdownValues: [],
dropdownOpenME: false,
solvedME: this.props.selectDataME,
dropdownValuesME: [],
}
}
componentDidMount() {
if(this.props.onRef)
this.props.onRef(this);
this.setState(oldState => {...oldState, dropdownValues: this.props.dropdownValues ,dropdownValuesME: this.props.dropdownValuesME})
}

TypeError: this.state.robots.filter is not a function?

Trying to fetch the Jsonplaceholder users name and id and filter them in in the render method. I'm getting this error:
TypeError: this.state.robots.filter is not a function
class App extends React.Component {
constructor() {
super()
this.state = {
robots: [],
searchfield: ''
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
return response.json;
})
.then((users) => {
this.setState({robots: users});
})
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
}
render() {
const filteredRobots = this.state.robots.filter(robot => {
return robot.name.toLowerCase().includes(this.state.searchfield.toLowerCase());
})
return (
<div className="container text-center mt-4">
<h1 className="custom mb-5">RoboFriends</h1>
<SearchBox searchChange={this.onSearchChange} />
<CardList robots={filteredRobots} />
</div>
);
}
}
Could anyone give me a clue how to solve the problem? Thank you in advance!
You have to use .json() not json.
class App extends React.Component {
constructor() {
super()
this.state = {
robots: [],
searchfield: ''
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
return response.json();
})
.then((users) => {
this.setState({robots: users});
})
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
}
render() {
const filteredRobots = this.state.robots.filter(robot => {
return robot.name.toLowerCase().includes(this.state.searchfield.toLowerCase());
})
return (
<div className="container text-center mt-4">
<h1 className="custom mb-5">RoboFriends</h1>
<input onChange={this.onSearchChange} />
<pre>{JSON.stringify(filteredRobots, null, 2)}</pre>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I solved the problem by appending () to response.json:
return response.json();
Check the data type of users below: You are reassigning robots object with users and which should be array, if you want to access filter method. I think you data type of users i not array and that is why its throwing error. try printing users in console.
.then((users) => {
console.log(users);
this.setState({robots: users});
})
and correct below json() method in your code.
return response.json();
you should use fetch rest calling like that
fetch("https://jsonplaceholder.typicode.com/users").then(res => {
res.json().then(users => {
this.setState({ robots: users });
});
});
It should work.
response.json is a promise so you have to call with () something like this.
import React from 'react';
class App extends React.Component {
constructor() {
super()
this.state = {
robots: [],
searchfield: ''
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users').then(response => {
return response.json();
}).then((users) => {
this.setState({ robots: users });
})
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
}
render() {
const filteredRobots = this.state.robots.filter(robot => {
return robot.name.toLowerCase().includes(this.state.searchfield.toLowerCase());
})
return (
<div className="container text-center mt-4">
<h1 className="custom mb-5">RoboFriends</h1>
<SearchBox searchChange={this.onSearchChange} />
<CardList robots={filteredRobots} />
</div>
);
}
}
export default App;
refer this link : https://www.robinwieruch.de/react-fetching-data

Getting Error in testing react component even when test passed

I am writing unit test cases for my React component using Jest and Enzyme. I am receiving an error in this component's testing even when all my test cases are passing.
I am sharing my component code and the error I am getting below
class BidSummary extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
techSpecMetric: [],
isStateChanged: 1
};
}
componentDidMount() {
this.fetchTechSpec();
}
fetchTechSpec = async () => {
this.setState({
isLoading: true
});
const data = {
quote_summary_id: get(this.props.match, 'params.bidId', '')
};
const techSpec = await this.props.fetchAssignedTechspec(data);
if (isSuccess(techSpec)) {
this.setState({
techSpecMetric: (get(techSpec, 'payload.data', [])),
isLoading: false
});
}
}
showmaterialIds = (cell, row) => {
const content = row.Materials.map(item => (
<li key={item.UPCProductConfigurationId}>
<span className="materialName">{item.MaterialCode}</span>
<span className="materialDescription">{item.MaterialName}</span>
</li>
));
return (
<div>
<Popover
trigger="click"
placement="bottomLeft"
content={<ul className="materialIdsList">{content}</ul>}
title={`Material IDs for ${row.TechSpecCode} (${row.Materials.length})`}
overlayClassName="xyz"
>
<div
style={{
fontSize: '11px',
textDecoration: 'underline',
color: '#32C77F',
display: 'inline-block'
}}
>
{row.Materials.length}
</div>
</Popover>
</div>
);
};
/* This function calls when user click on Cancel link in the footer */
onClickCancel = () => {
this.props.history.push('/');
}
/* This function calls when user click on Back link in the footer */
onClickBack = () => {
this.saveAsDraftGroupData('back');
}
/* This function calls when user click on Cancel, Back or Save as draft link in the footer.
It is used to save data.
*/
saveAsDraftGroupData = async (type) => {
const arrayWithGroups = [];
forEach(this.state.techSpecMetric, (data) => {
if (data.GroupName) {
let sendingData = null;
sendingData = {
QuoteSummaryID: data.QuoteSummaryID,
GroupName: data.GroupName
};
arrayWithGroups.push(sendingData);
}
});
const quoteId = get(this.props.match, 'params.bidId', '');
const response = await this.props.saveAssignedTechspec({ data: arrayWithGroups, is_draft: true, quote_summary_id: quoteId });
isSuccess(response);
if (type === 'back') {
this.props.history.push(`/bid/${quoteId}/assign-techspec`);
}
}
saveBidQuote = async () => {
this.setState({
isLoading: true
});
const data = {
QuoteSummaryID: get(this.props.match, 'params.bidId', '')
};
const response = await this.props.updatePqrStatus(data);
if (isSuccess(response)) {
this.setState({
isLoading: false
});
BcgSuccessNotification({
message: response.payload.status,
description: response.payload.message
});
this.props.history.push('/');
} else {
BcgSuccessNotification({
message: response.payload.status,
description: response.payload.message
});
}
}
fetchGroupsCount = (dataArray) => {
const groupCount = uniq(dataArray.filter(data => data.GroupName));
return groupCount.length;
}
techSpecCount = (dataArray, MG2) => {
dataArray = dataArray.filter(data => data.MG2 === MG2);
return dataArray.length;
}
render() {
const techSpecs = sortBy(this.state.techSpecMetric, el => el.GroupName);
return (
<div key="quote-add-group-page" className="testss">
{/* <QuoteHeader /> */}
<Spinner spinning={this.state.isLoading}>
<Row className="quote-add-product-page quote-summary-page">
<Col span={24}>
<Row>
<QuotePageTitle
title={this.context.intl.formatMessage({ id: 'quote.bid.summary.page.heading' })} />
</Row>
<Row className="tech-specs-list-section">
{
(
<BcgCollapse
className="data-table"
bordered={false}
expandIcon={({ isActive }) => <BcgIcon type="caret-right" theme="filled" rotate={isActive ? 90 : 0} />}
>
{
Object.entries(groupBy(techSpecs, 'MG2')).map(mg2Item => (
<BcgCollapse.Panel
header={<div className="collpase-title">{`${mg2Item[0]} (${this.fetchGroupsCount(mg2Item[1])})`}</div>}
key={mg2Item[0] + Math.random()}
className="grouped-mg2"
>
<Row className="mt-8 table-properties">
<BcgCollapse
className="data-table"
bordered={false}
expandIcon={({ isActive }) => <BcgIcon type="caret-right" theme="filled" rotate={isActive ? 90 : 0} />}
>
{
Object.entries(groupBy((mg2Item[1]), 'GroupName')).map((groupItem, index) => {
console.log('sorted array :::::::', sortBy(groupItem[1], el => el.GroupName));
return ((groupItem[1].length > 0) && (mg2Item[1].length > 0 && mg2Item[1].some(el => (el.GroupName === groupItem[0] !== null && el.GroupName === groupItem[0])))
) ? (
<BcgCollapse.Panel
header={`${groupItem[0]} (${this.techSpecCount(groupItem[1], mg2Item[0])})`}
key={index}
>
{
<BcgTable
loading={this.state.tableLoading}
columns={this.state.columns}
rowKey={record => (`${record.QuoteSummaryID}`)}
size="middle"
className="grouped-items-table"
dataSource={groupItem[1]}
pagination={false}
/>
}
</BcgCollapse.Panel>
) : (
<div style={{ paddingRight: '20px' }}>
<BcgTable
loading={this.state.tableLoading}
columns={this.state.columns}
rowKey={record => (`${record.QuoteSummaryID}`)}
size="middle"
dataSource={groupItem[1]}
pagination={false}
/>
</div>
);
})
}
</BcgCollapse>
</Row>
</BcgCollapse.Panel>
))
}
</BcgCollapse>
)
}
</Row>
</Col>
</Row>
</Spinner>
<QuoteFooter
isStateChanged={this.state.isStateChanged}
onClickCancel={this.onClickCancel}
onClickBack={this.onClickBack}
actionProps={{
onProceed: this.saveBidQuote,
onSaveAsDraft: this.saveAsDraftGroupData
}}
showBack />
</div>
);
}
}
/**
* #readonly
*/
BidSummary.propTypes = {
match: PropTypes.object,
saveAssignedTechspec: PropTypes.func,
fetchAssignedTechspec: PropTypes.func,
history: typeOfObject,
getQuoteStatus: PropTypes.func,
updatePqrStatus: PropTypes.func
};
/**
* #readonly
*/
BidSummary.contextTypes = {
intl: PropTypes.object
};
function mapDispatchToProps(dispatch) {
return {
fetchAssignedTechspec: data => dispatch(fetchAssignedTechspec(data)),
saveAssignedTechspec: data => dispatch(saveAssignedTechspec(data)),
updatePqrStatus: data => dispatch(updatePqrStatus(data))
};
}
export {BidSummary};
export default connect('', mapDispatchToProps)(withRouter(BidSummary));
Now below is the test I am running for this component
describe('BidSummary', () => {
const match = {
params: {
bidId: 'asdasdd'
}
};
it('should render correctly', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper).toMatchSnapshot();
});
it('should contains a class', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.exists('.quote-summary-page')).toEqual(true);
});
it('should contains a spinner', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.exists(Spinner)).toEqual(true);
});
it('should contains Collapse component', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.find(BcgCollapse)).toHaveLength(1);
});
it('should fetch data from api', () => {
const getData = {
quote_summary_id: ''
};
let fetchAssignedTechspec = jest.fn();
const wrapper = shallowWithIntl(<BidSummary fetchAssignedTechspec={fetchAssignedTechspec} />);
expect(wrapper).toBeDefined();
expect(fetchAssignedTechspec).toHaveBeenCalled();
expect(fetchAssignedTechspec.mock.calls[0]).toEqual([getData]);
});
it('should fetch data from api', () => {
const getData = {
quote_summary_id: ''
};
let fetchAssignedTechspec = jest.fn();
const wrapper = shallowWithIntl(<BidSummary fetchAssignedTechspec={fetchAssignedTechspec} />);
expect(wrapper).toBeDefined();
expect(fetchAssignedTechspec).toHaveBeenCalled();
expect(fetchAssignedTechspec.mock.calls[0]).toEqual([getData]);
});
});
Now these tests are getting passed but I am also receiving the error mentioned below
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 69): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 70): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 71): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 72): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 73): TypeError: Cannot use 'in' operator to search for 'error' in undefined
I want to know where did I do the mistake and how to write the test case correctly so I don't get this error?
Please let me know thanks.

ReactJS | Loading State in component doesn't render Spinner

I am trying to make a React component that displays multiple renders based on props and state. So, while I wait for the promise to be resolved, I want to display a spinner Component
Main Renders:
NoResource Component => When the user is not valid
Spinner Component => When is loading on all renders
BasicRender Component => When data are fetched and is not loading
Below is my component:
/* eslint-disable react/prefer-stateless-function */
import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getUser, listUsers } from '../../config/service';
export class UserDetailsScreen extends Component {
static propTypes = {
match: PropTypes.shape({
isExact: PropTypes.bool,
params: PropTypes.object,
path: PropTypes.string,
url: PropTypes.string
}),
// eslint-disable-next-line react/forbid-prop-types
history: PropTypes.object,
label: PropTypes.string,
actualValue: PropTypes.string,
callBack: PropTypes.func
};
state = {
user: {},
error: '',
isloading: false
};
componentDidMount() {
this.fetchUser();
this.setState({ isLoading: true})
}
getUserUsername = () => {
const { match } = this.props;
const { params } = match;
return params.username;
};
fetchUser = () => {
getUser(this.getUserUsername())
.then(username => {
this.setState({
user: username.data,
isloading: false
});
})
.catch(({ message = 'Could not retrieve data from server.' }) => {
this.setState({
user: null,
error: message,
isLoading: false
});
});
};
validateUsername = () =>
listUsers().then(({ data }) => {
const { match } = this.props;
if (data.includes(match.params.username)) {
return true;
}
return false;
});
// eslint-disable-next-line no-restricted-globals
redirectToUsers = async () => {
const { history } = this.props;
await history.push('/management/users');
};
renderUserDetails() {
const { user, error } = this.state;
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}
renderNoResourceComponent = () => {
const { match } = this.props;
return (
<div className="center-block">
<NoResource
icon="exclamation-triangle"
title="Ooops.."
primaryBtn="« Back to Users"
primaryCallback={this.redirectToUsers}
>
<h5>404: USER NOT FOUND</h5>
<p>
Sorry, but the User with username:
<strong>{match.params.username}</strong> does not exists
</p>
</NoResource>
</div>
);
};
renderSpinner = () => {
const { isLoading, error } = this.state;
if (isLoading && error === null) {
return <ContentSpinner />;
}
return null;
};
render() {
return (
<div className="container-fluid mt-2">
{this.renderSpinner()}
{this.renderUserDetails()}
</div>
);
}
}
export default withRouter(UserDetailsScreen);
The problem is:
I get the spinner along with the main component, and I am getting this error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.. Can you please tell me what I am doing wrong.
The error is because you are running the renderUserDetailsComponent even when your API call is in loading state. You must only render the spinner on loading state
renderUserDetails() {
const { user, error, isLoading } = this.state;
if(isLoading) {
return null;
}
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}

Categories