Component Won't Rerender After Record Has Been Updated - javascript

I'm trying to show an icon based on whether or not the current user Id equals the accordion Id.
i.e. I'm trying to let users who made a card be the ones that can edit the card.
This starts to happen where it says //Update icon in code.
The record successfully updates (I can see in the database, and when you refresh the page, it shows correctly without errors), but when the component tries to re-render, I get this error: https://imgur.com/a/YVTDg82
I feel like this is something obvious, and I'm missing something easy.
import React from "react"
import PropTypes from "prop-types"
import { Accordion, Icon, Input, Form, Button, Modal, Dropdown } from 'semantic-ui-react'
//components
import UpdateCard from "./UpdateCard"
import LikeUnlike from "./LikeUnlike"
class Card extends React.Component {
constructor(props) {
super(props)
this.state = {
activeIndex: null,
search: ''
}
}
updateSearch = (event) => {
this.setState({ search: event.target.value.substr(0, 20) })
}
//Opens and closes the accordion
handleClick = (e, titleProps) => {
const { index } = titleProps
const { activeIndex } = this.state
const newIndex = activeIndex === index ? -1 : index
this.setState({ activeIndex: newIndex })
}
render() {
// const options = [
// { key: 'css', text: 'CSS', value: 'css' },
// { key: 'html', text: 'HTML', value: 'html' },
// { key: 'javascript', text: 'Javascript', value: 'javascript' },
// { key: 'rails', text: 'Rails', value: 'rails' },
// { key: 'react', text: 'React', value: 'react' },
// { key: 'ruby', text: 'Ruby', value: 'ruby' },
// ]
const { activeIndex } = this.state
const { showEditMenu } = this
let filteredCards = this.props.librarys.filter(
(library) => {
return library.title.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1 || library.desc.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
}
)
return (
<React.Fragment>
<div className='search-bar'>
<Input fluid icon={<Icon name='search' inverted circular link />} value={this.state.search} onChange={this.updateSearch} placeholder="Search Syntaxes" />
</div>
<ul>
{filteredCards.map((librarys, index) => {
return (
<div key={index} >
<Accordion>
<Accordion.Title >
<Icon
name='dropdown'
active={activeIndex === index}
index={index}
onClick={this.handleClick}
/>
<Icon name='trash alternate'
onClick={() => {
this.props.handleDelete(librarys.id)
}}
/>
{
//Update Icon
librarys.user.id === this.props.currentUser.id ?
<UpdateCard
handleUpdate={this.props.handleUpdate}
libraryId={librarys.id}
likes={librarys.likes}
librarys={librarys}
/>
: ''
}
Title: {librarys.title} Likes: {librarys.likes}
<LikeUnlike
handleUpdate={this.props.handleUpdate}
libraryId={librarys.id}
librarys={librarys}
/>
</Accordion.Title>
<Accordion.Content active={activeIndex === index}>
Description: <br></br>
{librarys.desc} <br></br>
Markdown: <br></br>
{librarys.markdown}
</Accordion.Content>
</Accordion>
</div>
)
})}
</ul>
</React.Fragment>
);
}
}
export default Card

You need to do a null check before comparing the values. Like below:
librarys.user && librarys.user.id === this.props.currentUser.id ?
// same stuff here

Related

Error: Element type is invalid: expected a string in react ,when trying to update state

I got this error when updating state from fetch.
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
This is my code below
import React from "react"
import useState from "react"
import {
Button,
Progress,
UncontrolledDropdown,
DropdownMenu,
DropdownToggle,
DropdownItem,
Input,
Badge,
Row,
Col,
ListGroup,
ListGroupItem
} from "reactstrap";
import Draggable from 'react-draggable'
import StatisticsCard from "../../components/#vuexy/statisticsCard/StatisticsCard"
import DataTable from "react-data-table-component"
import classnames from "classnames"
import ReactPaginate from "react-paginate"
import { history } from "../../history"
import {
Edit,
Trash,
ChevronDown,
Plus,
Check,
ChevronLeft,
ChevronRight,
Monitor,
UserCheck,
Mail,
Eye,
MessageSquare,
ShoppingBag,
Heart,
Smile,
Truck,
Cpu,
Server,
Activity,
AlertOctagon,
CreditCard,
Share,
Image
} from "react-feather"
import { connect } from "react-redux"
import {
getData,
getInitialData,
deleteData,
updateData,
addData,
filterData
} from "../../redux/actions/data-list/"
import Sidebar from "./DataListSidebar"
import Chip from "../../components/#vuexy/chips/ChipComponent"
import Checkbox from "../../components/#vuexy/checkbox/CheckboxesVuexy"
import "../../assets/scss/plugins/extensions/react-paginate.scss"
import "../../assets/scss/pages/data-list.scss"
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import NumberFormat from 'react-number-format';
import $ from 'jquery'
import tc from 'thousands-counter';
class DataListConfig extends React.Component {
constructor(props){
super(props)
this.state = {
data: [],
campaigns: [],
tt: 0,
totalPages: 0,
currentPage: 0,
columns: [
{
name: "Name",
selector: "name",
sortable: true,
cell: row => (
<p title={row.name} className="text-truncate text-bold-500 mb-0">
{sessionStorage.Name}
</p>
)
},
{
name: "Title",
selector: "title",
sortable: true,
cell: row => (
<p className="text-bold-500 text-truncate mb-0">{row.title}</p>
)
},
{
name: "Total Donation",
selector: "totaldonor",
sortable: true,
cell: row => (
<p className="text-bold-500 text-truncate mb-0"> <NumberFormat value={row.amounts} displayType={'text'} thousandSeparator={true} prefix={'₦'} /></p>
)
},
{
name: "Status",
selector: "status",
sortable: true,
cell: row => (
<Badge
color={row.status === null || row.status === '' ? "light-danger" : "light-success"}
pill>
{row.status === null || row.status === '' ? "Pending":"Active"}
</Badge>
)
},
{
name: "Start Date",
selector: "startdate",
sortable: true,
cell: row => <p className="text-bold-500 mb-0">{row.startdate}</p>
},
{
name: "Category",
selector: "category",
sortable: true,
cell: row => <p className="text-bold-500 mb-0">{row.category}</p>
},
{
name: "Actions",
sortable: true,
cell: row => (
<this.ActionsComponent
row={row}
getData={this.props.getData}
parsedFilter={this.props.parsedFilter}
currentData={this.handleCurrentData}
deleteRow={this.handleDelete}
/>
)
}
],
allData: [],
value: "",
comments:[],
rowsPerPage: 4,
sidebar: false,
currentData: null,
selected: [],
totalRecords: 0,
sortIndex: [],
addNew: ""
}
const headers = { 'Content-Type': 'application/json' , 'Authorization':`Bearer ${sessionStorage.jwt}`}
fetch(' ', { headers })
.then(response => response.json())
.then(data => {
this.setState({ data: data.posts })
}).catch((error) => {
console.log(error)
});
}
componentDidMount()
{
function ActionsComponent(props) {
const {
buttonLabel,
className
} = props;
const [modal, setModal] = useState(false);
const [modal2, setModal2] = useState(false);
const [items, setItems] = useState([]);
const [comments,setCom] = useState([])
const Details = (id)=> {
$.post(' ',{
postid: id
},function(data,status){
setItems([
...items,
{
datas: data
}
]);
})
}
$.post(' ',{
postid: props.row.id
},function(data,status){
this.setState({commets: data})
})
let datas
datas = {totaldonors: "",totalcomments:""}
items.map((key)=>{
datas = key.datas
})
const dashdata = datas
const toggleModal = (id) =>{ setModal(!modal); };
const toggleModal2 = (id) =>{ setModal2(!modal2); };
return (
<div>
<Draggable>
<Modal
isOpen={modal}
toggle={toggleModal}
className="modal-dialog-centered"
backdrop={false}
>
<ModalHeader toggle={toggleModal}>
Campaign Dashboard!
</ModalHeader>
<ModalBody className="modal-dialog-centered">
<div className="container">
<div className="row">
<div className="col-sm">
<StatisticsCard
hideChart
iconBg="primary"
icon={<CreditCard className="primary" size={22} />}
stat={tc(dashdata.totaldonors)}
statTitle="Total Donors"
/>
</div>
<div className="col-sm">
<StatisticsCard
hideChart
iconBg="primary"
icon={<Share className="primary" size={22} />}
stat="36.9k"
statTitle="Total Shares"
/>
</div>
<div className="col-sm">
<StatisticsCard
hideChart
iconBg="primary"
icon={<MessageSquare className="primary" size={22} />}
stat={tc(dashdata.totalcomments)}
statTitle="Total Comments"
/>
</div>
<div className="container" style={{'max-height':'300px','overflow-y': 'scroll'}}>
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary">
Request Withdrawal
</Button>{" "}
</ModalFooter>
</Modal>
</Draggable>
<div className="data-list-action">
{" "}
{" "}
<Edit
className="cursor-pointer mr-1"
size={20}
onClick={() => {
return props.currentData(props.row)
}}
/>
<Activity
className="cid cursor-pointer mr-1"
size={20}
onClick={()=>{return toggleModal(),Details(props.row.id);}}
/>
</div>
</div>
)
}
}
thumbView = this.props.thumbView
componentDidUpdate(prevProps, prevState) {
if (this.thumbView) {
this.thumbView = false
let columns = [
{
name: "Name",
selector: "name",
sortable: true,
cell: row => (
<p title={row.name} className="text-truncate text-bold-500 mb-0">
{sessionStorage.Name}
</p>
)
},
{
name: "Title",
selector: "title",
sortable: true,
cell: row => (
<p className="text-bold-500 text-truncate mb-0">{row.title}</p>
)
},
{
name: "Status",
selector: "status",
sortable: true,
cell: row => (
<Badge
color={row.status === null || row.status === '' ? "light-danger" : "light-success"}
pill>
{row.status === null || row.status === '' ? "Pending":"Active"}
</Badge>
)
},
{
name: "Total Donation",
selector: "totaldonor",
sortable: true,
cell: row => (
<p className="text-bold-500 text-truncate mb-0">{row.amounts}</p>
)
},
{
name: "Start Date",
selector: "startdate",
sortable: true,
cell: row => <p className="text-bold-500 mb-0">{row.startdate}</p>
},
{
name: "Category",
selector: "category",
sortable: true,
cell: row => <p className="text-bold-500 mb-0">{row.category}</p>
},
{
name: "Actions",
sortable: true,
cell: row => (
<this.ActionsComponent
row={row}
getData={this.props.getData}
parsedFilter={this.props.parsedFilter}
currentData={this.handleCurrentData}
deleteRow={this.handleDelete}
/>
)
}
]
this.setState({ columns })
}
}
handleFilter = e => {
this.setState({ value: e.target.value })
this.props.filterData(e.target.value)
}
handleRowsPerPage = value => {
let { parsedFilter, getData } = this.props
let page = parsedFilter.page !== undefined ? parsedFilter.page : 1
history.push(`/funding/?page=${page}&perPage=${value}`)
this.setState({ rowsPerPage: value })
getData({ page: parsedFilter.page, perPage: value })
}
handleSidebar = (boolean, addNew = false) => {
this.setState({ sidebar: boolean })
if (addNew === true) this.setState({ currentData: null, addNew: true })
}
handleDelete = row => {
this.props.deleteData(row)
this.props.getData(this.props.parsedFilter)
if (this.state.data.length - 1 === 0) {
let urlPrefix = this.props.thumbView
? "/funding/"
: "/funding/"
history.push(
`${urlPrefix}list-view?page=${parseInt(
this.props.parsedFilter.page - 1
)}&perPage=${this.props.parsedFilter.perPage}`
)
this.props.getData({
page: this.props.parsedFilter.page - 1,
perPage: this.props.parsedFilter.perPage
})
}
}
handleCurrentData = obj => {
this.setState({ currentData: obj })
this.handleSidebar(true)
}
handlePagination = page => {
let { parsedFilter, getData } = this.props
let perPage = parsedFilter.perPage !== undefined ? parsedFilter.perPage : 4
let urlPrefix = this.props.thumbView
? "/funding/b"
: "/funding/"
history.push(
`${urlPrefix}?page=${page.selected + 1}&perPage=${perPage}`
)
getData({ page: page.selected + 1, perPage: perPage })
this.setState({ currentPage: page.selected })
}
render() {
let {
columns,
data,
allData,
totalPages,
value,
rowsPerPage,
currentData,
sidebar,
totalRecords,
sortIndex
} = this.state
return (
<div
className={`data-list ${
this.props.thumbView ? "thumb-view" : "list-view"
}`}>
<CustomHeader
handleSidebar={this.handleSidebar}
handleFilter={this.handleFilter}
handleRowsPerPage={this.handleRowsPerPage}
rowsPerPage="5"
total={totalRecords}
index={sortIndex}
/>
<DataTable
columns={columns}
data={data}
pagination
noHeader
selectableRows
responsive
pointerOnHover
selectableRowsHighlight
onSelectedRowsChange={data =>
this.setState({ selected: data.selectedRows })
}
customStyles={selectedStyle}
subHeaderComponent={
<CustomHeader
handleSidebar={this.handleSidebar}
handleFilter={this.handleFilter}
handleRowsPerPage={this.handleRowsPerPage}
rowsPerPage="5"
total={totalRecords}
index={sortIndex}
/>
}
sortIcon={<ChevronDown />}
selectableRowsComponent={Checkbox}
selectableRowsComponentProps={{
color: "primary",
icon: <Check className="vx-icon" size={12} />,
label: "",
size: "sm"
}}
/>
<Sidebar
show={sidebar}
data={currentData}
updateData={this.props.updateData}
addData={this.props.addData}
handleSidebar={this.handleSidebar}
thumbView={this.props.thumbView}
getData={this.props.getData}
dataParams={this.props.parsedFilter}
addNew={this.state.addNew}
/>
<div
className={classnames("data-list-overlay", {
show: sidebar
})}
onClick={() => this.handleSidebar(false, true)}
/>
</div>
)
}
}
const mapStateToProps = state => {
return {
dataList: state.dataList
}
}
const handleSidebar = (boolean, addNew = false) => {
this.setState({ sidebar: boolean })
if (addNew === true) this.setState({ currentData: null, addNew: true })
}
export default connect(mapStateToProps, {
getData,
deleteData,
updateData,
addData,
getInitialData,
filterData
})(DataListConfig)
i removed my api endpoint for security purpose
bug image
The DataListConfig component tries to render <this.ActionsComponent>. Because ActionsComponent is defined inside the componentDidMount method, instead of at the class level, this.ActionsComponent is undefined. Which causes the error.
Ultimately though defining a component inside another component is a bad idea. Inside a class component's class as a field, it will mean that each instance of that class component will have their own version of the function component. This smells fishy at best, and is not very React-like.
Move ActionsComponent outside the DataListConfig component entirely, and pass in data it needs through its props.
function ActionsComponent(props) {
// ...
}
class DataListConfig extends React.Component {
// ...
}

Re-Render in component on State Change in Redux Store

I wanted to show logoff button after user signs in and hide visibility of button after the user logged out. Menu items are coming from Menu.js file.
The current workflow of the app is, if user signed in, the auth state is updated in the store, based on that state, I wanted to show a logoff Button in Menu.
The auth state is correctly updated in Redux store on login and I can get the value of auth in Menu.js File from Redux store, but the change is not rendered in Siderbar.js until i refresh the page.
What should i need to do in sidebar.js to fetch the menu on auth state change in Store.
Siderbar.js
<pre><code>
import React, { Component } from 'react';
import { withNamespaces, Trans } from 'react-i18next';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import { Collapse, Badge } from 'reactstrap';
import SidebarRun from './Sidebar.run';
import SidebarUserBlock from './SidebarUserBlock';
import Menu from '../../Menu';
/** Component to display headings on sidebar */
const SidebarItemHeader = ({item}) => (
<li className="nav-heading">
<span><Trans i18nKey={item.translate}>{item.heading}</Trans></span>
</li>
)
/** Normal items for the sidebar */
const SidebarItem = ({item, isActive}) => (
<li className={ isActive ? 'active' : '' }>
<Link to={item.path} title={item.name}>
{item.label && <Badge tag="div" className="float-right" color={item.label.color}>{item.label.value}</Badge>}
{item.icon && <em className={item.icon}></em>}
<span><Trans i18nKey={item.translate}>{item.name}</Trans></span>
</Link>
</li>
)
/** Build a sub menu with items inside and attach collapse behavior */
const SidebarSubItem = ({item, isActive, handler, children, isOpen}) => (
<li className={ isActive ? 'active' : '' }>
<div className="nav-item" onClick={ handler }>
{item.label && <Badge tag="div" className="float-right" color={item.label.color}>{item.label.value}</Badge>}
{item.icon && <em className={item.icon}></em>}
<span><Trans i18nKey={item.translate}>{item.name}</Trans></span>
</div>
<Collapse isOpen={ isOpen }>
<ul id={item.path} className="sidebar-nav sidebar-subnav">
{ children }
</ul>
</Collapse>
</li>
)
/** Component used to display a header on menu when using collapsed/hover mode */
const SidebarSubHeader = ({item}) => (
<li className="sidebar-subnav-header">{item.name}</li>
)
class Sidebar extends Component {
state = {
collapse: {},
isLoggedIn: ''
}
componentDidMount() {
// pass navigator to access router api
SidebarRun(this.navigator.bind(this));
// prepare the flags to handle menu collapsed states
this.buildCollapseList()
}
/** prepare initial state of collapse menus. Doesnt allow same route names */
buildCollapseList = () => {
let collapse = {};
Menu
.filter(({heading}) => !heading)
.forEach(({name, path, submenu}) => {
collapse[name] = this.routeActive(submenu ? submenu.map(({path})=>path) : path)
})
this.setState({collapse});
}
navigator(route) {
this.props.history.push(route);
}
routeActive(paths) {
paths = Array.isArray(paths) ? paths : [paths];
return paths.some(p => this.props.location.pathname.indexOf(p) > -1)
}
toggleItemCollapse(stateName) {
for (let c in this.state.collapse) {
if (this.state.collapse[c] === true && c !== stateName)
this.setState({
collapse: {
[c]: false
}
});
}
this.setState({
collapse: {
[stateName]: !this.state.collapse[stateName]
}
});
}
getSubRoutes = item => item.submenu.map(({path}) => path)
/** map menu config to string to determine what element to render */
itemType = item => {
if (item){
// console.log(item)
if (item.heading) return 'heading';
if (!item.submenu) return 'menu';
if (item.submenu) return 'submenu';
}}
render() {
return (
<aside className='aside-container'>
{ /* START Sidebar (left) */ }
<div className="aside-inner">
<nav data-sidebar-anyclick-close="" className="sidebar">
{ /* START sidebar nav */ }
<ul className="sidebar-nav">
{ /* START user info */ }
<li className="has-user-block">
<SidebarUserBlock/>
</li>
{ /* END user info */ }
{ /* Iterates over all sidebar items */ }
{
Menu.map((item, i) => {
// heading
if(this.itemType(item) === 'heading')
return (
<SidebarItemHeader item={item} key={i} />
)
else {
if(this.itemType(item) === 'menu')
return (
<SidebarItem isActive={this.routeActive(item.path)} item={item} key={i} />
)
if(this.itemType(item) === 'submenu')
return [
<SidebarSubItem item={item} isOpen={this.state.collapse[item.name]} handler={ this.toggleItemCollapse.bind(this, item.name) } isActive={this.routeActive(this.getSubRoutes(item))} key={i}>
<SidebarSubHeader item={item} key={i}/>
{
item.submenu.map((subitem, i) =>
<SidebarItem key={i} item={subitem} isActive={this.routeActive(subitem.path)} />
)
}
</SidebarSubItem>
]
}
return null; // unrecognized item
})
}
</ul>
{ /* END sidebar nav */ }
</nav>
</div>
{ /* END Sidebar (left) */ }
</aside>
);
}
}
Sidebar.propTypes = {
isLoggedIn: PropTypes.object.isRequired
};
const mapStateToProps = (state, ownProps) => {
// console.log("Sidebar: ", state.auth);
return{
isLoggedIn: state.auth,
}}
export default connect(mapStateToProps)(withRouter(Sidebar));
What should i do in Sidebar.js, that it re-renders the menu on auth state change.
Any help would be highly appreciated.
Menu.js File
import configureStore from './store/store';
const store = configureStore();
function select(state) {
return state.auth
}
let loggedIn = select(store.getState());
let Meu = [
{
name: 'Analytics',
icon: 'icon-speedometer',
translate: 'sidebar.nav.DASHBOARD',
submenu: [{
name: 'Overview',
path: '/dashboard',
translate: 'sidebar.nav.element.HOME'
},
{
name: 'Revenue',
path: '/revenue',
translate: 'sidebar.nav.element.REVENUE'
},
{
name: 'Source',
path: '/source',
translate: 'sidebar.nav.element.SOURCE'
},
{
name: 'Marketing',
path: '/marketing',
translate: 'sidebar.nav.element.MARKETING'
}
]
},
{
name: 'Tools',
icon: 'fa fa-briefcase',
translate: 'sidebar.nav.TOOLS',
submenu: [
{
name: 'Customer Service',
path: '/customer-service',
translate: 'sidebar.nav.element.CUSTOMER-SERVICE'
}
]
},
{
name: 'Settings',
icon: 'icon-settings',
translate: 'sidebar.nav.SETTINGS',
submenu: [{
name: 'Profile Settings',
path: '/profile',
translate: 'sidebar.nav.element.PROFILE-SETTINGS'
},
{
name: 'Company Settings',
path: '/company-settings',
translate: 'sidebar.nav.element.SETTINGS'
}
]
},
{(loggedIn.authResponse) ?
({
name: 'Logoff',
icon: 'icon-lock',
path: '/logoff'
}) : null
}
];
const Menu = Meu.filter(word => word !== null)
export default (Menu)
Inside the parent component of your log out button you can conditionally render the log out button; take note this means your parent component is connected to redux via connect(mapStateToProps)
{ this.props.isLoggedIn ? <Button>Log Out</Button> : null }

Don't know how to handle input in my project

I'm working under the to-do list project and have got a problem with adding a comment to my list item. This is what I have right now:
App flow
After adding a list item you should be able to click in this item and a new window with comments will appear. In the comments section, comments should be added with Ctrl+Enter combination.
I've got a problem with adding comments to the list item (they should be added to the "comments" array of a particular list item).
Could you please explain what I'm doing wrong and why my comments aren't adding.
UPDATE: I've updated my code but the following mistake appears when I press Ctr+Enter to add a comment: [Error] (http://joxi.ru/Q2KR1G3U4lZJYm)
I've tried to bind the addItem method but no result. What's wrong with the addItem method?
Here is my main component:
App.js
import React, { Component } from 'react';
import './App.css';
import ListInput from './components/listInput'
import ListItem from './components/listItem'
import SideBar from './components/sideBar'
import CommentsSection from './components/commentsSection'
class App extends Component {
constructor(props){
super(props);
this.state = {
items: [
{
id: 0,
text: 'First item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 1,
text: 'Second item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 2,
text: 'Third item',
commentsCount: 0,
comments: [
'Very first comment',
'Second comment',
],
displayComment: false
},
],
nextId: 3,
activeComment: [],
}
}
// Add new item to the list
addItem = inputText => {
let itemsCopy = this.state.items.slice();
itemsCopy.push({id: this.state.nextId, text: inputText});
this.setState({
items: itemsCopy,
nextId: this.state.nextId + 1
})
}
// Remove the item from the list: check if the clicked button id is match
removeItem = id =>
this.setState({
items: this.state.items.filter((item, index) => item.id !== id)
})
setActiveComment = (id) => this.setState({ activeComment: this.state.items[id] });
addComment = (inputComment, activeCommentId ) => {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
commentCopy.push({comments: inputComment})
this.setState({
comments: commentCopy
})
}
render() {
return (
<div className='App'>
<SideBar />
<div className='flex-container'>
<div className='list-wrapper'>
<h1>Items</h1>
<ListInput inputText='' addItem={this.addItem}/>
<ul>
{
this.state.items.map((item) => {
return <ListItem item={item} key={item.id} id={item.id} removeItem={this.removeItem} setActiveComment={() => this.setActiveComment(item.id)}/>
})
}
</ul>
</div>
<CommentsSection inputComment='' items={this.state.activeComment}/>
</div>
</div>
);
}
}
export default App;
and my Comments Section component:
commentsSection.js
import React from 'react';
import './commentsSection.css';
import CommentInput from './commentInput'
import CommentsItem from './commentsItem'
export default class CommentsSection extends React.Component {
constructor(props){
super(props);
this.state = {value: this.props.inputComment};
this.handleChange = this.handleChange.bind(this);
this.handleEnter = this.handleEnter.bind(this);
this.addComment = this.addComment.bind(this)
}
handleChange = event => this.setState({value: event.target.value})
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
this.addComment(this.state.value)
}
}
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
this.props.addComment(comment, this.props.activeComment);
this.setState({value: ''});
}
}
render() {
return (
<div className='component-section'>
<h1>{this.props.items.text}</h1>
<ul>
{ this.props.items.comments &&
this.props.items.comments.map((comment, index) => <p key={index}>{comment}</p>)
}
</ul>
<CommentsItem />
{/*<CommentInput />*/}
<div className='comment-input'>
<input type='text' value={this.state.value} onChange={this.handleChange} onKeyPress={this.handleEnter}/>
</div>
</div>
)
}
}
Change your CommentSection component addComment method and handleEnter method
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
// pass another argument to this.props.addComment
// looking at your code pass "this.props.items"
this.props.addComment(comment, this.props.items.id);
this.setState({value: ''});
}
}
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
// passing component state value property as new comment
this.addComment(this.state.value)
}
}
Change your App Component addComment method
addComment = (inputComment, activeCommentId )=> {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
// if you want to push comments as object
// but looking at your code this is not you want
// commentCopy.push({comments: inputComment})
// if you want to push comments as string
commentCopy.push( inputComment)
this.setState({
comments: commentCopy
})
}

React.js not re-rendering app on state update

I'm working on a React.js based outliner (in similar vein to workflowy). I'm stuck at a point. When I press enter on any one item, I want to insert another item just below it.
So I have used setState function to insert an item just below the current item as shown by the code snippet below:
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
However, the app is re-rendering as blank without any error showing up.
My full source so far:
import './App.css';
import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';
const items = [
{
_id: 0,
content: 'Item 1 something',
note: 'Some note for item 1'
},
{
_id: 5,
content: 'Item 1.1 something',
note: 'Some note for item 1.1'
},
{
_id: 1,
content: 'Item 2 something',
note: 'Some note for item 2',
subItems: [
{
_id: 2,
content: 'Sub Item 1 something',
subItems: [{
_id: 3,
content: 'Sub Sub Item 4'
}]
}
]
}
];
class App extends Component {
render() {
return (
<div className="App">
<Page items={items} />
</div>
);
}
}
class Page extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.items
};
this.insertItem = this.insertItem.bind(this);
}
render() {
return (
<div className="page">
{
this.state.items.map(item =>
<Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
insertItem(itemID) {
console.log(this.state.items);
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
console.log(this.state.items);
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
content: this.props.content,
note: this.props.note
};
this.saveItem = this.saveItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
render() {
return (
<div key={this.props._id} className="item">
<ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />
<div className="note">{this.state.note}</div>
{
this.props.subItems &&
this.props.subItems.map(item =>
<Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
saveItem(event) {
this.setState({
content: event.target.value
});
}
handleKeyPress(itemID, event) {
if (event.key === 'Enter') {
event.preventDefault();
console.log(itemID);
console.log(event.key);
this.props.insertItem(itemID);
}
}
}
export default App;
The github repo: https://github.com/Hirvesh/mneme
Can anybody help me understand as to why it's not rendering again after I update the items?
Edit:
I have update the code to add keys, as suggested below, still rendering as blank, despite the state updating:
import './App.css';
import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';
const items = [
{
_id: 0,
content: 'Item 1 something',
note: 'Some note for item 1'
},
{
_id: 5,
content: 'Item 1.1 something',
note: 'Some note for item 1.1'
},
{
_id: 1,
content: 'Item 2 something',
note: 'Some note for item 2',
subItems: [
{
_id: 2,
content: 'Sub Item 1 something',
subItems: [{
_id: 3,
content: 'Sub Sub Item 4'
}]
}
]
}
];
class App extends Component {
render() {
return (
<div className="App">
<Page items={items} />
</div>
);
}
}
class Page extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.items
};
this.insertItem = this.insertItem.bind(this);
}
render() {
return (
<div className="page">
{
this.state.items.map(item =>
<Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
insertItem(itemID) {
console.log(this.state.items);
this.setState({
items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
})
console.log(this.state.items);
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
content: this.props.content,
note: this.props.note
};
this.saveItem = this.saveItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
render() {
return (
<div className="item">
<ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />
<div className="note">{this.state.note}</div>
{
this.props.subItems &&
this.props.subItems.map(item =>
<Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
}
</div>
);
}
saveItem(event) {
this.setState({
content: event.target.value
});
}
handleKeyPress(itemID, event) {
if (event.key === 'Enter') {
event.preventDefault();
console.log(itemID);
console.log(event.key);
this.props.insertItem(itemID);
}
}
}
export default App;
Edit 2:
As per the suggestions below, added key={i._id}, still not working, pushed latest code to github if anybody wants to take a look.
this.state.items.map((item, i) =>
<Item _id={item._id} key={i._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
)
You miss to put a key on Item, because of that React could not identify if your state change.
<div className="page">
{
this.state.items.map((item, i) =>
<Item _id={item._id}
key={i} // <---- HERE!!
content={item.content}
note={item.note}
subItems={item.subItems}
insertItem={this.insertItem} />
)}
</div>
There are multiple issues with your code:
in you are passing this.insertItem into SubItems (which does not exists)
your this.state.items.splice, changes this.state.items, but returns []. So your new State is []
try this:
this.state.items.splice(/* your splice argument here */)
this.setState({ items: this.state.items }, () => {
// this.setState is async! so this is the proper way to check it.
console.log(this.state.items);
});
To accomplish what you actually want, this will not be enough.. But at least, when you have fixed it, you can see some results.

HowTo: update the value of an element in a property

So, I have a property (fields), within which I wish to change the value of an element (countries). Alerting the value of countries currently displays the value 2, but I want to change the value to 100, so that re-alerting fields.countries.value, after the change, displays the new value.
How do I do this?
import type { State } from '../../common/types';
import DynamicField from './DynamicField';
import R from 'ramda';
import React from 'react';
import buttonsMessages from '../../common/app/buttonsMessages';
import linksMessages from '../../common/app/linksMessages';
import { FormattedMessage } from 'react-intl';
import { ValidationError } from '../../common/lib/validation';
import { connect } from 'react-redux';
import { fields } from '../../common/lib/redux-fields';
import {
Block,
Box,
Button,
Checkbox,
FieldError,
Flex,
Form,
Heading,
Input,
PageHeader,
Pre,
Radio,
Select,
Space,
Title,
View,
} from '../app/components';
// The example of dynamically loaded editable data.
// cato.org/publications/commentary/key-concepts-libertarianism
const keyConceptsOfLibertarianism = [
'Individualism',
'Individual Rights',
'Spontaneous Order',
'The Rule of Law',
'Limited Government',
'Free Markets',
'The Virtue of Production',
'Natural Harmony of Interests',
'Peace',
].map((concept, index) => ({
id: index,
name: concept,
}));
// Proof of concept. Country list will be read from firebase
const countryArray = [
{ label: 'Select Country', value: 0 },
{ label: 'France', value: 2 },
{ label: 'England', value: 4 },
{ label: 'Swizterland', value: 8 },
{ label: 'Germany', value: 16 },
{ label: 'Lithuania', value: 32 },
{ label: 'Romania', value: 64 },
].map((countryName, index) => ({
id: index,
name: countryName,
}));
// Dynamically create select list
const countryOptions = [];
countryArray.map(countryItem =>
countryOptions.push({ label: countryItem.name.label, value: countryItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const cityArray = [
{ label: 'Select City', value: 0 },
{ label: 'London', value: 50 },
{ label: 'Paris', value: 75 },
].map((cityName, index) => ({
id: index,
name: cityName,
}));
// Dynamically create select list
const cityOptions = [];
cityArray.map(cityItem =>
cityOptions.push({ label: cityItem.name.label, value: cityItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const gymArray = [
{ label: 'Select Gym', value: 0 },
{ label: 'Virgin Sport', value: 23 },
{ label: 'Sports Direct', value: 45 },
].map((gymName, index) => ({
id: index,
name: gymName,
}));
// Dynamically create select list
const gymOptions = [];
gymArray.map(gymItem =>
gymOptions.push({ label: gymItem.name.label, value: gymItem.name.value }),
);
type LocalState = {
disabled: boolean,
error: ?Object,
submittedValues: ?Object,
};
class FieldsPage extends React.Component {
static propTypes = {
fields: React.PropTypes.object.isRequired,
dynamicFields: React.PropTypes.object,
// getCities: React.PropTypes.object,
};
state: LocalState = {
disabled: false,
error: null,
submittedValues: null,
};
onFormSubmit = () => {
const { dynamicFields, fields } = this.props;
const values = {
...fields.$values(),
concepts: {
...dynamicFields,
},
};
// This is just a demo. This code belongs to Redux action creator.
// Disable form.
this.setState({ disabled: true });
// Simulate async action.
setTimeout(() => {
this.setState({ disabled: false });
const isValid = values.name.trim();
if (!isValid) {
const error = new ValidationError('required', { prop: 'name' });
this.setState({ error, submittedValues: null });
return;
}
this.setState({ error: null, submittedValues: values });
fields.$reset();
}, 500);
};
handleSelectedCountryChange = () => {
// Pass in the selected country value to get associated cites
const { fields, getCities } = this.props;
getCities('country', fields.$values());
};
/*
handleSelectedCityChange = (event => {
// Pass in the selected city value to get associated gyms
this.setState({secondLevel: event.target.value});
});
*/
render() {
const { fields } = this.props;
const { disabled, error, submittedValues } = this.state;
return (
<View>
<Title message={linksMessages.fields} />
<PageHeader
description="New clients enter their gym details here."
heading="New user entry form."
/>
<Form onSubmit={this.onFormSubmit}>
<Input
{...fields.name}
aria-invalid={ValidationError.isInvalid(error, 'name')}
disabled={disabled}
label="Your Name"
maxLength={100}
type="text"
/>
<FieldError error={error} prop="name" />
<Heading alt>Key Concepts of Libertarianism</Heading>
<Block>
<Flex wrap>
{keyConceptsOfLibertarianism.map(item =>
<Box mr={1} key={item.id}>
<DynamicField
disabled={disabled}
item={item}
path={['fieldsPage', 'dynamic', item]}
/>
</Box>,
)}
</Flex>
</Block>
<Block>
<Checkbox
{...fields.isLibertarian}
checked={fields.isLibertarian.value}
disabled={disabled}
label="I'm libertarian"
/>
<Checkbox
{...fields.isAnarchist}
checked={fields.isAnarchist.value}
disabled={disabled}
label="I'm anarchist"
/>
</Block>
<Block>
<Flex>
<Radio
{...fields.gender}
checked={fields.gender.value === 'male'}
disabled={disabled}
label="Male"
value="male"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'female'}
disabled={disabled}
label="Female"
value="female"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'other'}
disabled={disabled}
label="Other"
value="other"
/>
</Flex>
</Block>
<Block>
<Select
{...fields.countries}
disabled={disabled}
label="Countries"
onChange={this.handleSelectedCountryChange}
options={countryOptions}
/>
</Block>
<Block>
<Select
{...fields.cities}
disabled={disabled}
label="Cities"
// onChange={this.handleSelectedCityChange}
options={cityOptions}
/>
</Block>
<Block>
<Select
{...fields.gyms}
disabled={disabled}
label="Gyms"
// onChange={this.handleSelectedCityChange}
options={gymOptions}
/>
</Block>
{/*
Why no multiple select? Because users are not familiar with that.
Use checkboxes or custom checkable dynamic fields instead.
*/}
<Button disabled={disabled} type="submit">
<FormattedMessage {...buttonsMessages.submit} />
</Button>
{submittedValues &&
<Pre>
{JSON.stringify(submittedValues, null, 2)}
</Pre>
}
</Form>
</View>
);
}
}
FieldsPage = fields({
path: 'fieldsPage',
fields: [
'countries',
'cities',
'gyms',
'gender',
'isAnarchist',
'isLibertarian',
'name',
],
getInitialState: () => ({
countries: '0',
cities: '0',
gyms: '0',
gender: 'male',
isAnarchist: false,
isLibertarian: false,
}),
})(FieldsPage);
export default connect(
(state: State) => ({
dynamicFields: R.path(['fieldsPage', 'dynamic'], state.fields),
}),
)(FieldsPage);
=====================================================================
fields.js
/* #flow weak */
import R from 'ramda';
import React from 'react';
import invariant from 'invariant';
import { resetFields, setField } from './actions';
type Path = string | Array<string> | (props: Object) => Array<string>;
type Options = {
path: Path,
fields: Array<string>,
getInitialState?: (props: Object) => Object,
};
const isReactNative =
typeof navigator === 'object' &&
navigator.product === 'ReactNative'; // eslint-disable-line no-undef
// Higher order component for huge fast dynamic deeply nested universal forms.
const fields = (options: Options) => (WrappedComponent) => {
const {
path = '',
fields = [],
getInitialState,
} = options;
invariant(Array.isArray(fields), 'Fields must be an array.');
invariant(
(typeof path === 'string') ||
(typeof path === 'function') ||
Array.isArray(path)
, 'Path must be a string, function, or an array.');
return class Fields extends React.Component {
static contextTypes = {
store: React.PropTypes.object, // Redux store.
};
static getNormalizePath(props) {
switch (typeof path) {
case 'function': return path(props);
case 'string': return [path];
default: return path;
}
}
static getFieldValue(field, model, initialState) {
if (model && {}.hasOwnProperty.call(model, field)) {
return model[field];
}
if (initialState && {}.hasOwnProperty.call(initialState, field)) {
return initialState[field];
}
return '';
}
static lazyJsonValuesOf(model, props) {
const initialState = getInitialState && getInitialState(props);
// http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
return options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.getFieldValue(field, model, initialState),
}), Object.create(null));
}
static createFieldObject(field, onChange) {
return isReactNative ? {
onChangeText: (text) => {
onChange(field, text);
},
} : {
name: field,
onChange: (event) => {
// Some custom components like react-select pass the target directly.
const target = event.target || event;
const { type, checked, value } = target;
const isCheckbox = type && type.toLowerCase() === 'checkbox';
onChange(field, isCheckbox ? checked : value);
},
};
}
state = {
model: null,
};
fields: Object;
values: any;
unsubscribe: () => void;
onFieldChange = (field, value) => {
const normalizedPath = Fields.getNormalizePath(this.props).concat(field);
this.context.store.dispatch(setField(normalizedPath, value));
};
createFields() {
const formFields = options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.createFieldObject(field, this.onFieldChange),
}), {});
this.fields = {
...formFields,
$values: () => this.values,
$setValue: (field, value) => this.onFieldChange(field, value),
$reset: () => {
const normalizedPath = Fields.getNormalizePath(this.props);
this.context.store.dispatch(resetFields(normalizedPath));
},
};
}
getModelFromState() {
const normalizedPath = Fields.getNormalizePath(this.props);
return R.path(normalizedPath, this.context.store.getState().fields);
}
setModel(model) {
this.values = Fields.lazyJsonValuesOf(model, this.props);
options.fields.forEach((field) => {
this.fields[field].value = this.values[field];
});
this.fields = { ...this.fields }; // Ensure rerender for pure components.
this.setState({ model });
}
componentWillMount() {
this.createFields();
this.setModel(this.getModelFromState());
}
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() => {
const newModel = this.getModelFromState();
if (newModel === this.state.model) return;
this.setModel(newModel);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.props} fields={this.fields} />
);
}
};
};
export default fields;

Categories

Resources